The product API

Trove serves the catalog and delivery on /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 401 unauthenticated.
  • 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.

200 application/json
{
  "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.

200 application/json
{
  "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.

200 application/json
{
  "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.

200 application/json
{
  "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.

request
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 2
200 text/csv; charset=utf-8 (first two rows)
establishment.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
200 application/x-ndjson; charset=utf-8 (first two 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"}
{"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.

200 application/json
{
  "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.

request
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.

200 application/json
{
  "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.

200 text/csv
HTTP/1.1 200 OK
Content-Type: text/csv
Content-Disposition: attachment; filename="..."
Cache-Control: no-store
X-Trove-SHA256: 9c1e5a0b7d3f2e8c4a6b1d0f9e8c7b6a5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a

GET/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.

200 application/json
{
  "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.

Webhook event types
TypeSent when
release.publishedA new full release of a dataset you are entitled to is published. sha256 is the release manifest's.
delivery.readyA scheduled delivery (S3, GCS, Azure, or SFTP) has finished writing its files. sha256 is the delivery manifest's.
export.readyAn 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.

POST <your endpoint>
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.

MCP tools
ToolInputReturns
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.

mcp.json
{
  "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.

Error codes by HTTP status
StatusCodeWhen
400invalid_requestA malformed cursor, body, date, partition, or Idempotency-Key. The message names the input.
401unauthenticatedNo valid product token in Authorization: Bearer.
401entitlement_requiredentitlement_expiredentitlement_not_yet_validentitlement_revokedentitlement_replayedentitlement_invalidA delivery route without a usable assertion in X-Trove-Entitlement: missing, expired, not yet valid, revoked, already used, or not signed for this product.
403not_entitledThe assertion is valid but does not grant this dataset, use (feed or exports), field, or partition.
404not_foundNo dataset with that slug, or no export with that id for this customer.
404no_sampleThe dataset has no published approved sample.
409idempotency_conflictThe Idempotency-Key was used before with a different request.
409not_readyA download of an export that is still queued or building.
409no_releaserelease_changedNothing is published to export yet, or a release was published while the request was read; ask again.
410not_eligibleThe dataset no longer distributes a field the export carries; it is not delivered again.
410expiredThe export passed its expiry date.
410withdrawnThe export or its release was withdrawn.
429rate_limitedOver 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.csv and sample.jsonl; webhooks and the MCP server are documented as previews.