regulations-gov
The Regulations.gov API for Hot, with a free api.data.gov key. Covers documents, dockets and public comments, read-only, with a way past the paging ceiling of one query.
Install
Add it to your project's deps in hot.hot:
hot.project.my-app.deps {
"hot.dev/regulations-gov": "0.1.0"
}
Auth and settings
Each setting comes from per-call opts first, then the Context Variable, then the default (::regulations-gov::api/resolve). Every API function takes a trailing opts Map, so one run can serve many tenants. Per-call credentials are never written to context.
| Context Variable | opts key | Default | Description |
|---|---|---|---|
regulations-gov.api-key | api-key | none | Required (secret). A free key from api.data.gov, sent in the X-Api-Key header |
regulations-gov.base-url | base-url | https://api.regulations.gov/v4 | API root. The API also has a staging host, https://api-staging.regulations.gov |
api-key from opts is masked in call records. Present but blank or null, it is an error naming the key, never a fallback to the Context Variable. A per-call base-url needs an api-key in the same opts: the context key is never sent to a per-call URL.
Quick start
::reg ::regulations-gov
// CFPB documents posted since October, newest first
page ::reg/documents({
filters: {agencyId: "CFPB", postedDate: {ge: "2026-10-01"}},
sort: "-postedDate",
page-size: 25
})
// One document and its comments. commentOnId takes the objectId, not the id
doc ::reg/document("FAA-2018-1084-0001")
comments ::reg/comments({filters: {commentOnId: doc.data.attributes.objectId}, page-size: 50})
comments.meta.totalElements
// A docket, with its RIN
docket ::reg/docket("FAA-2018-1084")
docket.data.attributes.rin
Functions by area
| Namespace | Functions |
|---|---|
::regulations-gov | Lists: documents, comments, dockets. Detail: document, comment, docket. Past the ceiling: all-documents, all-comments, all-dockets, collect-windows. Paging: next-page, page-params, filter-params, list-params. Windows: eastern-date-time, window-opts, next-cursor |
::regulations-gov::api | resolve, require-api-key, request, get-json, get-json-or-null, decode-body, query-string, path-segment, http-error, not-json-error, is-not-found; type HttpError |
filters is a Map of field to value. A Map value gives the range forms, so {postedDate: {ge: "2026-01-01", le: "2026-02-01"}} sends filter[postedDate][ge] and filter[postedDate][le]. postedDate and commentEndDate are yyyy-MM-dd. lastModifiedDate is yyyy-MM-dd HH:mm:ss in Eastern time.
Pagination
Lists take page-size (held to 5 to 250) and page, and next-page(page) reads meta. The OpenAPI spec documents pages 1 to 20, so one query reaches 5,000 records.
To go further, all-documents, all-comments and all-dockets follow the documented procedure: sort on lastModifiedDate, read 20 pages of 250, take the last row's lastModifiedDate, convert it from UTC to Eastern time, and restart with filter[lastModifiedDate][ge]. The overlap at each boundary is dropped.
::reg ::regulations-gov
doc ::reg/document("EOIR-2020-0003-0001")
rows ::reg/all-comments({filters: {commentOnId: doc.data.attributes.objectId}}, 20000)
A full page 20 restarts from the last row whatever hasNextPage says, because that flag can end at the paging ceiling rather than with the data. So exactly 5,000 rows costs one more request. limit is required, and each page is one request against the hourly limit: 20,000 rows is at least 80 requests. If a whole window of 5,000 rows shares one timestamp the walk can't move on, and it stops with an Err rather than dropping rows.
Errors
Non-2xx responses come back as an Err carrying ::regulations-gov::api/HttpError:
source:"regulations-gov"status,url,headers,bodyretry-after: seconds, parsed fromRetry-Afterwhen the response has onemessage: a one-line summary built from the API's own error text, never a credential
A 2xx body that isn't JSON, such as a maintenance page, is an HttpError too, with status 200, so the all-* functions can't read it as an empty page. document, comment and docket return null rather than a 404 Err when there is no such record, and a blank id is an Err. A missing API key is an Err that names the context variable to set, before any request. Nothing retries inside a run. Use a handler or schedule retry policy for 429s and 5xx.
Notes and limits
- Rate limit: the API refers to api.data.gov's limits without giving its own number, and api.data.gov's default is 1,000 requests per hour, which a service can change. The
X-RateLimit-LimitandX-RateLimit-Remainingresponse headers show the real figure. GSA considers increases for GET keys case by case.DEMO_KEYis for demonstrations only. lastModifiedDateis beta: GSA says it "may be removed" once a permanent bulk download exists. Theall-*functions depend on it.- Paging ceiling: the docs say pages 1 to 20. On 4 Nov 2026 the API also served page 21 and reported
totalPages: 40for 418 rows at size 5, so the real ceiling may be 40 pages. The package stays in the documented range. - Personal data: comments are public but carry personal data. The API can return
firstName,lastName,city,organizationand more, as each agency configures it, and titles often name the commenter. The package logs nothing. Store results under your own data-protection policy. - Read-only: posting comments needs a separate key and is not wrapped.
- Documents:
withdrawnmarks a withdrawn document, anddocumentsdoesn't return the RIN: read it fromdocket. File links are infileFormatsondocument. - Attachments are only returned with
{include-attachments: true}, inincluded.
License
Apache-2.0 - see LICENSE