The product API
/product/v1. Responses are JSON with snake_case fields. This site reads the same catalog; the catalog routes below, and the two sample downloads, also answer without auth from a mock at /api/product/v1 so you can see real shapes.Overview
Two credentials, sent as headers:
- Product token.
Authorization: Bearer <token>on every route. It identifies the product (your application) and sets its rate limit. Without it every route answers 401unauthenticated. - Entitlement assertion.
X-Trove-Entitlement: <assertion>on delivery routes. A short-lived signed statement of what one customer may read: datasets, the feed or exports, fields, and partitions. Your customer backend signs it; Trove verifies it on each request.
Public: the catalog. Every product sees the same datasets, schemas, coverage, roadmaps, the approved sample, and the feed preview. Customer-specific: delivery. The feed, exports, and usage answer only within what the assertion grants, and are recorded per customer.
Catalog routes
Catalog reads need the product token only. The examples are this site's mock responses, shortened.
GET/product/v1/datasetsTry it ↗ (opens the mock in a new tab)
Every dataset with its kinds, partition count, current primary records, and latest release.
{
"datasets": [
{
"slug": "us-food-inspections",
"name": "US food inspections",
"schema_version": 1,
"sample_release_id": "a1c0e7d2-55b4-4f0e-9d3a-8b1f2c6e4d70",
"kinds": [
"establishment",
"... 2 more"
],
"partition": "Jurisdiction",
"partitions": 8,
"records": 18315,
"latest_release": {
"id": "6f2d4c1e-8f1a-4b61-9c0a-2a7f3d9e1b55",
"version": 41,
"published_at": "2026-10-07T02:14:09Z"
}
},
"... 2 more"
]
}GET/product/v1/datasets/{slug}Try it ↗ (opens the mock in a new tab)
One dataset: coverage definition, freshness target, schema by kind (each field typed and described), coverage per partition, roadmap, and the sample's release and counts.
{
"slug": "us-food-inspections",
"name": "US food inspections",
"schema_version": 1,
"sample_release_id": "a1c0e7d2-55b4-4f0e-9d3a-8b1f2c6e4d70",
"kinds": [
"establishment",
"inspection",
"... 1 more"
],
"partition": "Jurisdiction",
"partitions": 8,
"records": 18315,
"latest_release": {
"id": "6f2d4c1e-8f1a-4b61-9c0a-2a7f3d9e1b55",
"version": 41,
"published_at": "2026-10-07T02:14:09Z"
},
"coverage_definition": "A run declares its scope (jurisdiction, full scan or since-checkpoint) and ends complete, partial, or failed. Only a com...",
"freshness_target_hours": 72,
"schema": [
{
"kind": "establishment",
"label": "Establishment",
"fields": [
{
"name": "establishment.id",
"type": "id",
"description": "Trove's stable id of the establishment; inspections name it."
},
{
"name": "establishment.jurisdiction_id",
"type": "text",
"description": "The local health district that licenses and inspects it (oh-phdmc: Public Health - Dayton & Montgomery County)."
},
"... 1 more"
]
},
{
"kind": "inspection",
"label": "Inspection",
"parent": "establishment",
"parent_field": "inspection.establishment_id",
"fields": [
{
"name": "inspection.id",
"type": "id",
"description": "Trove's stable id of the inspection; violations name it."
},
{
"name": "inspection.establishment_id",
"type": "id",
"description": "The establishment inspected."
},
"... 1 more"
]
},
"... 1 more"
],
"coverage": [
{
"id": "oh-phdmc",
"name": "Public Health - Dayton & Montgomery County",
"state": "enabled",
"records": 3412,
"last_complete_scan": "2026-10-06T03:10:00Z",
"in_latest_release": true,
"release_coverage": "complete",
"release_coverage_words": "complete scan of the portal"
},
{
"id": "oh-cincinnati",
"name": "Cincinnati Health Department",
"state": "enabled",
"records": 5890,
"last_complete_scan": "2026-10-07T01:05:00Z",
"in_latest_release": true,
"release_coverage": "complete",
"release_coverage_words": "complete daily extract"
},
"... 6 more"
],
"roadmap": [
{
"state": "AZ",
"name": "Apache County Public Health Services District",
"status": "planned"
},
{
"state": "AZ",
"name": "Arizona Department of Health Services",
"status": "in_progress"
},
"... 288 more"
],
"sample": {
"release_id": "a1c0e7d2-55b4-4f0e-9d3a-8b1f2c6e4d70",
"version": 7,
"published_at": "2026-10-01T15:02:00Z",
"schema_version": 1,
"selection": {
"rule": "stratified_by_partition",
"per_kind": 200,
"description": "Up to 200 rows per kind, stratified by partition, children of sampled parents only."
},
"counts": {
"establishment": 200,
"inspection": 200,
"violation": 200
}
}
}GET/product/v1/datasets/{slug}/sampleTry it ↗ (opens the mock in a new tab)
The published approved sample: up to 200 rows per kind, stratified by partition, children of sampled parents only. 404 no_sample when none is published. The workbench queries exactly this.
{
"slug": "us-food-inspections",
"schema_version": 1,
"sample_release_id": "a1c0e7d2-55b4-4f0e-9d3a-8b1f2c6e4d70",
"kinds": [
{
"kind": "establishment",
"label": "Establishment",
"fields": [
"establishment.id",
"establishment.jurisdiction_id",
"establishment.name",
"... 7 more"
],
"rows": [
{
"establishment.id": "e555bd90-17aa-4df9-bc3e-f0827b29b7b4",
"establishment.jurisdiction_id": "oh-phdmc",
"establishment.name": "Harvest Cafe #8",
"establishment.license_no": "OH1104-278",
"establishment.address": "658 Wilmington Pike",
"establishment.city": "Vandalia",
"jurisdiction.state": "OH",
"establishment.zip": "45402",
"establishment.license_type": "Retail Food Establishment, Commercial, Risk Level III",
"establishment.license_status": "Licensed"
}
]
},
{
"kind": "inspection",
"label": "Inspection",
"fields": [
"inspection.id",
"inspection.establishment_id",
"inspection.inspected_on",
"... 2 more"
],
"rows": [
{
"inspection.id": "57fa37b9-01ea-4859-bac4-4777dc482247",
"inspection.establishment_id": "e555bd90-17aa-4df9-bc3e-f0827b29b7b4",
"inspection.inspected_on": "2026-07-23",
"inspection.type": "Complaint",
"inspection.status": "Complete"
}
]
},
{
"kind": "violation",
"label": "Violation",
"fields": [
"violation.inspection_id",
"violation.code",
"violation.description",
"... 1 more"
],
"rows": [
{
"violation.inspection_id": "dc535b5e-c038-480b-b18c-91ee8467fec2",
"violation.code": "3717-1-02.4(A)(2)",
"violation.description": "Person in charge not certified in food protection.",
"violation.critical": false
}
]
}
]
}GET/product/v1/datasets/{slug}/feed-previewTry it ↗ (opens the mock in a new tab)
The newest events of a dataset with an events feed, from its latest release. Other datasets answer with no events and a reason.
{
"slug": "ohio-food-business-profiles",
"release_id": "9e1f3a7c-4b2d-4e6f-8a9c-0d1e2f3a4b5c",
"release_version": 19,
"events": [
{
"seq": 19392,
"op": "upsert",
"kind": "food_business_event",
"record": {
"food_business_event.id": "841e8c77-9407-47c7-9bfe-306fbf9cc79d",
"food_business_event.profile_id": "87470f50-10f5-40ae-ab7f-92d7557c8dde",
"food_business_event.seq": 19392,
"food_business_event.type": "enforcement",
"food_business_event.occurred_on": "2026-08-31",
"food_business_event.summary": "Critical violation cited at Oak Creamery on a standard inspection.",
"food_business_event.business_entity_id": null,
"food_business_event.inspection_id": "96a2ddea-5505-48cd-bee2-95f6fbd1ea11",
"food_business_event.evidence_records": "119808df-e2a2-403a-b782-d9f44d79f852 96a2ddea-5505-48cd-bee2-95f6fbd1ea11",
"food_business_event.evidence_releases": "0b9a7f3c-2e5d-4a1b-8c6f-4d2e9a1c7f30 6f2d4c1e-8f1a-4b61-9c0a-2a7f3d9e1b55"
}
},
"... 19 more"
]
}GET/product/v1/datasets/{slug}/sample.csvTry it ↗ (opens the mock in a new tab)
The sample's primary kind as CSV. See Sample download.
GET/product/v1/datasets/{slug}/sample.jsonlTry it ↗ (opens the mock in a new tab)
The same rows as JSON Lines, one object per row.
Sample download
The approved sample's primary kind (for us-food-inspections, establishment: 200 rows) as a file, for a spreadsheet or a loader. Columns are the catalog's field names in schema order; nulls are empty cells in CSV and null in JSON Lines. For the child kinds, use /sample or the download in the workbench. Both routes are served by this site's mock and need no account.
curl -O https://regolith.astero.engineering/api/product/v1/datasets/us-food-inspections/sample.csv
curl https://regolith.astero.engineering/api/product/v1/datasets/us-food-inspections/sample.jsonl | head -n 2establishment.id,establishment.jurisdiction_id,establishment.name,establishment.license_no,establishment.address,establishment.city,jurisdiction.state,establishment.zip,establishment.license_type,establishment.license_status
e555bd90-17aa-4df9-bc3e-f0827b29b7b4,oh-phdmc,Harvest Cafe #8,OH1104-278,658 Wilmington Pike,Vandalia,OH,45402,"Retail Food Establishment, Commercial, Risk Level III",Licensed
eb30b476-c71a-4ba0-9967-0bad5c611993,oh-phdmc,Copper Deli,OH9206-809,9465 Main St,Dayton,OH,45402,"Food Service Operation, Commercial, Risk Level IV",Closed{"establishment.id":"e555bd90-17aa-4df9-bc3e-f0827b29b7b4","establishment.jurisdiction_id":"oh-phdmc","establishment.name":"Harvest Cafe #8","establishment.license_no":"OH1104-278","establishment.address":"658 Wilmington Pike","establishment.city":"Vandalia","jurisdiction.state":"OH","establishment.zip":"45402","establishment.license_type":"Retail Food Establishment, Commercial, Risk Level III","establishment.license_status":"Licensed"}
{"establishment.id":"eb30b476-c71a-4ba0-9967-0bad5c611993","establishment.jurisdiction_id":"oh-phdmc","establishment.name":"Copper Deli","establishment.license_no":"OH9206-809","establishment.address":"9465 Main St","establishment.city":"Dayton","jurisdiction.state":"OH","establishment.zip":"45402","establishment.license_type":"Food Service Operation, Commercial, Risk Level IV","establishment.license_status":"Closed"}Delivery routes
Delivery needs the product token and an entitlement assertion. Every call is audited with the customer and the assertion's id, never the assertion itself.
GET/product/v1/datasets/{slug}/feed?since=&limit=
The change feed, pinned to the newest published full release. Start with since=0; ask again with next_since until has_more is false; store next_since and resume from it. limit defaults to 100, at most 1,000. Each event is an upsert with the record's entitled fields (and the rows of its children that have no id of their own) or a removed. through is the highest seq the release covers. If the release changes mid-read, the answer is 409 release_changed; ask again.
{
"dataset": "ohio-food-business-profiles",
"schema_version": 1,
"release_id": "9e1f3a7c-4b2d-4e6f-8a9c-0d1e2f3a4b5c",
"release_version": 19,
"through": 19842,
"since": 19700,
"next_since": 19733,
"has_more": true,
"events": [
{
"seq": 19701,
"kind": "food_business_profile",
"id": "7d1c0b6e-2a94-4f53-9e1d-0c8b5a7f2e31",
"partition": "oh-phdmc",
"observed_at": "2026-10-07T04:02:31Z",
"change": "upsert",
"record": {
"food_business_profile.name": "Copper Kitchen",
"food_business_profile.license_status": "Licensed",
"...": "every entitled field"
}
},
{
"seq": 19733,
"kind": "food_business_profile",
"id": "e2b7a9c4-6d18-4a0f-b3e5-91c7d2f4a806",
"partition": "oh-phdmc",
"observed_at": "2026-10-07T04:02:31Z",
"change": "removed"
}
]
}POST/product/v1/datasets/{slug}/exports
Queues a CSV export of the newest published full release. Send an Idempotency-Key (8 to 128 letters, digits, _ . : -): a repeat with the same key and body returns the same export with 200; the same key with a different body is 409 idempotency_conflict. A new export answers 202 with a Location header.
The body: fields (none: every field you are entitled to), filters.partitions, and filters.from / filters.to (inclusive days on the dataset's range field, such as inspected-on), and expires (a date; default 30 days, at most 365). The rows are those of the most detailed kind the fields name, each with its ancestors' fields.
POST /product/v1/datasets/us-food-inspections/exports
Authorization: Bearer <product token>
X-Trove-Entitlement: <assertion>
Idempotency-Key: q3-dayton-violations
Content-Type: application/json
{
"fields": [
"establishment.name",
"establishment.city",
"inspection.inspected_on",
"violation.code",
"violation.critical"
],
"filters": {
"partitions": [
"oh-phdmc"
],
"from": "2026-07-01",
"to": "2026-09-30"
},
"expires": "2026-11-06"
}GET/product/v1/exports/{id}
The export's state: queued, building, ready, delivered, failed, or withdrawn. downloadable says whether a download would be served now. Another customer's export id is 404.
{
"id": "3f6a2c1d-9b8e-4d7f-a1c2-5e4b3a2d1c0f",
"dataset": "us-food-inspections",
"release_id": "6f2d4c1e-8f1a-4b61-9c0a-2a7f3d9e1b55",
"release_version": 41,
"state": "ready",
"downloadable": true,
"kind": "violation",
"fields": [
"establishment.name",
"establishment.city",
"inspection.inspected_on",
"violation.code",
"violation.critical"
],
"filters": {
"partitions": [
"oh-phdmc"
],
"from": "2026-07-01",
"to": "2026-09-30"
},
"rows": 18204,
"bytes": 2310442,
"sha256": "9c1e5a0b7d3f2e8c4a6b1d0f9e8c7b6a5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a",
"expires_at": "2026-11-06T00:00:00Z",
"created_at": "2026-10-07T12:00:04Z",
"delivered_at": null,
"downloads": 0
}GET/product/v1/exports/{id}/download
The CSV body with X-Trove-SHA256 set to the file's SHA-256; check it before you load the file. 409 not_ready while the export is queued or building. 410 expired after its expiry, withdrawn once it or its release is withdrawn, and not_eligible if the dataset stopped distributing one of its fields. A 410 export is not delivered again; create a new one.
HTTP/1.1 200 OK
Content-Type: text/csv
Content-Disposition: attachment; filename="..."
Cache-Control: no-store
X-Trove-SHA256: 9c1e5a0b7d3f2e8c4a6b1d0f9e8c7b6a5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0aGET/product/v1/usage?since=&limit=
The product's usage events, oldest first, paged like the feed: export_created, export_delivered, and feed_read (once per page with changes). Deduplicate by id or idempotency_id; bill your own customers from it.
{
"since": 0,
"next_since": 2,
"has_more": false,
"events": [
{
"id": "b0c1d2e3-f4a5-4b6c-8d7e-9f0a1b2c3d4e",
"seq": 1,
"customer": "acme-foods",
"kind": "export_created",
"idempotency_id": "q3-dayton-violations",
"dataset": "us-food-inspections",
"release_id": "6f2d4c1e-8f1a-4b61-9c0a-2a7f3d9e1b55",
"export_id": "3f6a2c1d-9b8e-4d7f-a1c2-5e4b3a2d1c0f",
"rows": 0,
"bytes": 0,
"created_at": "2026-10-07T12:00:04Z"
},
{
"id": "c1d2e3f4-a5b6-4c7d-9e8f-0a1b2c3d4e5f",
"seq": 2,
"customer": "acme-foods",
"kind": "export_delivered",
"idempotency_id": "...",
"dataset": "us-food-inspections",
"release_id": "6f2d4c1e-8f1a-4b61-9c0a-2a7f3d9e1b55",
"export_id": "3f6a2c1d-9b8e-4d7f-a1c2-5e4b3a2d1c0f",
"rows": 18204,
"bytes": 2310442,
"created_at": "2026-10-07T12:01:10Z"
}
]
}Webhooks
Preview: the contract below is planned, not served yet. You register one HTTPS endpoint per customer; Trove posts a JSON event to it when something you are entitled to changes.
| Type | Sent when |
|---|---|
| release.published | A new full release of a dataset you are entitled to is published. sha256 is the release manifest's. |
| delivery.ready | A scheduled delivery (S3, GCS, Azure, or SFTP) has finished writing its files. sha256 is the delivery manifest's. |
| export.ready | An export you created moved to ready; download it from /product/v1/exports/{id}/download. sha256 is the CSV's. |
Every event has the same envelope: id (unique per event; deduplicate on it), type, created_at, and data with the dataset, the release id and version, and a SHA-256. export.ready adds export_id; delivery.ready adds delivery_id and the destination path.
Content-Type: application/json
X-Trove-Event: release.published
X-Trove-Delivery-Attempt: 1
X-Trove-Signature: t=1791338049,v1=5f0c...e91a
{
"id": "evt_7c2e1a9f4b3d",
"type": "release.published",
"created_at": "2026-10-07T02:14:09Z",
"data": {
"dataset": "us-food-inspections",
"release_id": "6f2d4c1e-8f1a-4b61-9c0a-2a7f3d9e1b55",
"release_version": 41,
"sha256": "4b8e2f0c1a9d7e6b5c3f2a1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c"
}
}Signature. X-Trove-Signature carries a Unix time t and v1, the hex HMAC-SHA256 of <t>.<raw body> under your endpoint's secret. Compute it over the raw bytes before parsing, compare in constant time, and reject a t more than five minutes old.
Retries. Any 2xx within 10 seconds counts as delivered. Anything else is retried with exponential backoff (1 minute, then doubling, at most 6 hours apart) for 24 hours; X-Trove-Delivery-Attempt counts the tries. An event can arrive more than once and out of order: order releases by release_version, not by arrival.
MCP server
Preview, served from this site's mock routes. An agent can read the catalog and query the sample through six Model Context Protocol tools; each reads one route above. The endpoint below is not deployed yet.
| Tool | Input | Returns |
|---|---|---|
| list_datasets | {} | Every dataset with its kinds, partition count, records, and latest release. Reads GET /datasets. |
| get_dataset | { slug } | Schema by kind with each field typed and described, coverage per partition, roadmap. Reads GET /datasets/{slug}. |
| get_sample | { slug, kind?, limit? } | Rows of the published approved sample, optionally one kind and fewer rows. Reads GET /datasets/{slug}/sample. |
| query_sample | { slug, sql } | Runs the workbench's read-only SQL subset over the sample and returns the rows. Errors name the token. Reads GET /datasets/{slug}/sample. |
| estimate_subset | { slug, fields?, partitions?, from?, to? } | The export request body for a subset and the current records in the chosen partitions, read from coverage. No price is invented. Reads GET /datasets/{slug}. |
| feed_preview | { slug } | The newest events of a dataset with an events feed. Reads GET /datasets/{slug}/feed-preview. |
Add it to an MCP client that supports remote servers over HTTP. The product token is the one the API takes.
{
"mcpServers": {
"regolith": {
"url": "https://regolith.astero.engineering/api/mcp",
"headers": {
"Authorization": "Bearer <product token>"
}
}
}
}The tools are read-only. Delivery (the feed and exports) is not exposed to agents: it needs an entitlement assertion your backend signs per customer.
Error codes
Every error is { "code": "...", "message": "..." }. Branch on code; show message to a person.
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A malformed cursor, body, date, partition, or Idempotency-Key. The message names the input. |
| 401 | unauthenticated | No valid product token in Authorization: Bearer. |
| 401 | entitlement_requiredentitlement_expiredentitlement_not_yet_validentitlement_revokedentitlement_replayedentitlement_invalid | A delivery route without a usable assertion in X-Trove-Entitlement: missing, expired, not yet valid, revoked, already used, or not signed for this product. |
| 403 | not_entitled | The assertion is valid but does not grant this dataset, use (feed or exports), field, or partition. |
| 404 | not_found | No dataset with that slug, or no export with that id for this customer. |
| 404 | no_sample | The dataset has no published approved sample. |
| 409 | idempotency_conflict | The Idempotency-Key was used before with a different request. |
| 409 | not_ready | A download of an export that is still queued or building. |
| 409 | no_releaserelease_changed | Nothing is published to export yet, or a release was published while the request was read; ask again. |
| 410 | not_eligible | The dataset no longer distributes a field the export carries; it is not delivered again. |
| 410 | expired | The export passed its expiry date. |
| 410 | withdrawn | The export or its release was withdrawn. |
| 429 | rate_limited | Over the product's rate limit. Retry-After says when to try again. |
Rate limits
Each product token may make 60 requests a minute in steady state, with bursts of 20: a token bucket per product. Over it, the answer is 429 rate_limited with Retry-After in seconds. Page the feed with a larger limit rather than more requests.
Provenance and terms
- Every row traces to a capture. Each value comes from a stored capture of the source page, file, or PDF with its fetch time; it can be audited back to what the source showed.
- Releases are immutable. A published release never changes. Files carry SHA-256 checksums and a manifest. A withdrawal stops serving at the next request, which is why a download can answer 410.
- Absence is not a status. A record is marked unseen only after a complete scan of its partition; a partial crawl cannot invent a closure.
- What is not distributed. Registered agent, associate, and filer names and addresses from the business registers; inspectors' own comments on violations. A derived field is distributed only while every upstream field it is computed from is.
- Redistribution. Rows are publicly available data. What you may do with them follows the source's terms, privacy law where a row is about a person, and your contract.
Changelog
- 2026-10-07
- /product/v1 documented: four catalog routes, five delivery routes. Schema version 1 for all 3 datasets. The mock also serves the sample as
sample.csvandsample.jsonl; webhooks and the MCP server are documented as previews.