Conventions: envelope, filters, errors, limits
The rules every Sites API endpoint follows, so you learn them once.
The envelope
Every success is { "data": …, "meta": … }. Keys are snake_case. data is an object for one record and an array for a list.
{
"data": [ { "slug": "101", "…": "…" } ],
"meta": {
"pagination": { "total": 1, "page": 1, "per_page": 50, "last_page": 1 },
"cache": { "tags": ["inventory:01000000-0000-4000-8000-000000000002"], "ttl": 300 }
}
}Three promises about shape:
- Every documented key is always present. A value you may not see is
null, never missing. A hidden price isnull; a section you did notincludeisnull. - Empty maps are
{}and empty lists are[]. They are never swapped. - Dates are ISO 8601 with an offset (
2026-01-15T12:00:00+00:00); date-only values areYYYY-MM-DD. Money is a plain number (1500), not a formatted string.
Pagination
List endpoints take page (from 1) and per_page. Defaults and ceilings differ per endpoint and are listed in the endpoint reference: most lists default to 50 with a ceiling of 200; /paths and /availability default to 500 with a ceiling of 2000, because a build reads them whole.
meta.pagination always carries total, page, per_page and last_page. Read until page == last_page.
Filtering
Filters are plain query parameters, documented per endpoint. Four forms:
| Form | Example | Meaning |
|---|---|---|
| Exact | city=Austin | Case-insensitive match |
| Any of | beds=1,2 | Comma-separated; matches any |
| Boolean | available=true | true, false, 1, 0; anything else is a 422 |
| Range | sqft[gte]=700&sqft[lte]=1000 | Operators gte, lte, gt, lt |
property={slug} narrows a website-wide list to one property. An unknown property slug is a 404, not an empty list, so a typo cannot pass for "no results". group={id} takes a group id from GET /groups and cascades to the group's descendants.
A filter that cannot be applied is a 422, never silently ignored. beds=abc or include=nonsense fails loudly:
{ "message": "Must be a number.", "errors": { "beds": ["Must be a number."] } }Sorting
sort takes one or more keys, comma-separated, with a - prefix for descending: sort=-rent,number. Each endpoint allows only the keys it lists; anything else is a 422. When paging a sorted list, add a second unique key such as number or name so ties land in the same order on every page.
Includes
include embeds optional sections (GET /properties/{property}, GET /floor-plans). An unrequested section's key is still present, as null. meta.includes echoes what was applied.
Errors
| Status | Meaning | Body |
|---|---|---|
401 | Missing or revoked token | {message} |
403 | Token is for another website | {message} |
404 | Not found, or outside this website | {message} — /resolve and /entries add type: "not_found" |
409 | An Idempotency-Key still being processed, or a tour slot already taken | {message} |
422 | Validation failed | {message, errors} — errors keyed by parameter |
429 | Rate limited | {message}, with Retry-After |
502 | A tour provider failed; safe to retry | {message} |
Rate limits
Limits are per website token, not per IP, because a build server shares its address with many sites.
| Limiter | Limit | Applies to |
|---|---|---|
| Read | 1,200 / minute | Every GET, plus POST /events and POST /source-observations |
| Write | 60 / minute | POST /forms/{form}/submissions, POST /tours/*, and GET /units/{unit}/price-matrix |
Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After.
Idempotency
POST /forms/{form}/submissions and POST /tours/reservations accept an Idempotency-Key header: any unique string, up to 255 characters, one per submission. A repeat within 24 hours returns the original result with 200 and meta.duplicate: true, and creates nothing. A repeat while the first is still running is a 409. Always send one: it is what makes a retry after a timeout safe.
Compatibility
Additive changes ship continuously: new endpoints, new optional parameters, new keys. Build clients to ignore keys they do not know. Removing or retyping a key is a breaking change and is gated in CI by an OpenAPI diff.
Last updated on