Catalog API

Read your Structured Pages catalog over HTTPS — one product at a time, by category, by keyword, or by asking a plain-language question. Every response comes from the same records that power your public pages, so the numbers and names stay consistent.

Authentication

Every request needs an API key for this site. Create, label, and revoke keys in Control Deck → Settings → Integrations → Catalog API keys. The full key is shown once at creation — store it somewhere safe; only a masked value appears afterward.

Operators with shell access can still issue keys from the server:

node scripts/issue-api-key.js --label "my integration"

Send the key on every request with either header:

Authorization: Bearer hvce_…

or

X-Api-Key: hvce_…

Rate limits

Each key can make up to 60 requests per 60 seconds. If you go over, the API returns 429 with a Retry-After header. Use a separate key per integration so one client cannot starve another.

Errors you will see

  • 401 — missing, malformed, unknown, or revoked key (unauthorized)
  • 403 — key belongs to a different site (forbidden_tenant)
  • 404 — no matching product or category on this site (not_found)
  • 429 — rate limited (rate_limited)

Get one product

GET /v1/products/:slug returns a single catalog entity, including externalId when your data has a natural business key such as a SKU.

curl -sS -H "Authorization: Bearer $API_KEY" \
  "https://example.com/v1/products/starter-entity"
{
  "slug": "starter-entity",
  "title": "Starter Entity",
  "summary": "…",
  "dataset": "starter-catalog",
  "type": "Product",
  "categories": ["starter"],
  "externalId": "SKU-123",
  "externalIdType": "sku",
  "attributes": { "Status": "Sample" },
  "metrics": {}
}

List a category

GET /v1/categories/:slug returns products in that category. Optional limit (default 20, max 100).

curl -sS -H "Authorization: Bearer $API_KEY" \
  "https://example.com/v1/categories/starter?limit=10"
{
  "slug": "starter",
  "total": 1,
  "products": [ { "slug": "starter-entity", "title": "Starter Entity" } ]
}

GET /v1/search?q= finds entities whose titles, summaries, or attributes match. Query must be at least two characters.

curl -sS -H "Authorization: Bearer $API_KEY" \
  "https://example.com/v1/search?q=starter"
{
  "query": "starter",
  "total": 1,
  "results": [ { "slug": "starter-entity", "title": "Starter Entity" } ]
}

Ask in plain language

POST /v1/ask takes a question, maps it onto the same search and category filters your site already uses, and returns matching entities plus the interpreted intent.

curl -sS -X POST -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question":"herbicides safe for corn"}' \
  "https://example.com/v1/ask"
{
  "question": "herbicides safe for corn",
  "intent": { "q": "", "categories": ["herbicides"] },
  "total": 2,
  "results": [ { "slug": "corn-safe-spray", "title": "Corn Safe Herbicide" } ]
}

Machine-readable specification

Integrators can load the OpenAPI 3 document at /docs/openapi.json. It describes the same four endpoints, auth headers, and error codes shown here.

Clear