Bodek API documentation
Every Bodek API follows the same rules: one GET per collection, JSON in
and out, and the same four query conventions for filtering, searching, sorting and paging. Learn them
once and every dataset on this site works the same way.
Bodek API is part of your Bodek subscription. Every endpoint needs a key, and a key works for as long as the account that owns it has access to the API.
Quickstart
Create a key on your keys page, then call a collection. Every endpoint takes the same bearer header.
export BODEK_API_KEY=bk_live_… curl "https://api.bodek.us/v1/vehicles/makes?limit=3" \ -H "Authorization: Bearer $BODEK_API_KEY"
The catalogue is the one endpoint that needs no key: it lists every published API, so you can see what is available before you have one.
curl "https://api.bodek.us/v1"
Authentication
Every endpoint except /v1 and /v1/health
needs a key, sent as a bearer token.
Sign in to create one.
curl "https://api.bodek.us/v1/vehicles/makes" \ -H "Authorization: Bearer $BODEK_API_KEY"
X-API-Key: <your_key> works too. A key in the query string does not:
query strings end up in server logs, browser history and Referer headers, so
?key= is rejected with a 400 rather than quietly accepted.
A key carries the list of APIs it may read, an optional expiry, its own rate limits, and an optional restriction to specific domains or IP addresses. Treat it like a password: it is shown once, stored only as a hash, and can be revoked at any time.
A key is only as live as the account behind it. If that account's access to Bodek API ends — the
subscription lapses, the plan changes, or an owner revokes the grant — its keys stop working at once
and answer 402 subscription_required, without anyone having to go and
revoke them one by one.
Response shape
A collection returns a data array and a pagination object.
{
"data": [
{ "id": 1, "name": "Acura", "slug": "acura" }
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 163,
"has_more": true,
"next": "https://api.bodek.us/v1/vehicles/makes?limit=50&offset=50"
}
}A single record returns just data, as an object.
{
"data": { "id": 1, "name": "Acura", "slug": "acura" }
}Add ?pretty=1 to any request to get indented JSON while you explore.
Filtering
Any field marked filterable on an API's reference page can be used as a query parameter.
Bare equality needs no operator; everything else uses a __ suffix.
| Operator | Example | Meaning |
|---|---|---|
(none) | ?make=toyota | exactly equal |
__ne | ?make__ne=toyota | not equal |
__gt __gte | ?year__gte=2015 | greater than (or equal) |
__lt __lte | ?year__lte=1999 | less than (or equal) |
__in | ?body_type__in=SUBN,PICK | any of a comma-separated list |
__nin | ?body_type__nin=MCY,ATV | none of a list |
__contains | ?name__contains=civ | substring (text fields) |
__starts __ends | ?name__starts=cam | prefix / suffix (text fields) |
__null | ?years__null=true | is (or is not) empty |
Filters combine with AND. A parameter that names an unknown or non-filterable field is an error rather than being ignored, so a typo never silently returns the whole table. At most 12 filters and 50 values per list.
Search
q matches a substring across every field marked searchable on
that collection. Use it for type-ahead and lookup boxes; use filters when you know the exact value.
curl "https://api.bodek.us/v1/vehicles/makes?q=accord&limit=5"
Sorting
sort takes up to 3 comma-separated field
names. Prefix a name with - for descending. Results are always given a
stable tiebreaker, so the same record never appears on two pages.
curl "https://api.bodek.us/v1/vehicles/makes?sort=-year_max,name"
Choosing fields
fields trims the response to the columns you actually use.
curl "https://api.bodek.us/v1/vehicles/makes?fields=id,name&limit=3"
Pagination
limit (max 200, default
50) with either offset or
page. Follow pagination.next until it is
null rather than building the URL yourself.
Paging stops at offset 50,000. Past that, narrow the query with a filter — deep offsets get slower for everyone and there is almost always a better filter available.
Caching
Reference data changes rarely, so responses carry Cache-Control and an
ETag. Send the ETag back as If-None-Match and
an unchanged response comes back as an empty 304, which does not count
against your rate limit's payload.
curl "https://api.bodek.us/v1/vehicles/makes" \ -H 'If-None-Match: "a1b2c3…"'
Rate limits
Every response carries the current ceilings:
| Header | Meaning |
|---|---|
X-RateLimit-Limit-Minute | requests allowed per minute |
X-RateLimit-Limit-Day | requests allowed per day |
Retry-After | seconds to wait, sent with a 429 |
Limits are set per key when you create it, with a ceiling of 600 requests a minute and 200,000 a day. Give each application its own key so one noisy job cannot use up another's budget.
Errors
Every failure uses the same envelope. code is stable and safe to branch on; message is for humans.
{
"error": {
"code": "unknown_parameter",
"message": "Unknown query parameter 'colour'.",
"details": { "filterable": ["make", "body_type", "year_min"] },
"request_id": "3f9c1a2b7d4e5f60"
}
}| Status | When |
|---|---|
400 | A parameter is unknown, the wrong type, or out of range. |
401 | A key is required, missing, malformed, revoked or expired. |
402 | The account that owns the key no longer has access to Bodek API. |
403 | The key is valid but not allowed to read this API, origin or IP. |
404 | No such API, collection or record. |
405 | Anything other than GET, HEAD or OPTIONS. |
429 | Rate limited. Wait for Retry-After seconds. |
503 | The dataset's source is briefly unavailable. Retry. |
A 500 carries a request_id, also sent as the
X-Request-Id header. Quote it to support and we can find the exact request
in our logs.
OpenAPI
Each API publishes an OpenAPI 3.1 description, generated from the same definition that serves the data.
curl "https://api.bodek.us/v1/vehicles/openapi.json"
Point a client generator at it, or load it into Postman or Insomnia.