JSON:API Format¶
The Beyond API follows the JSON:API specification (v1.1) for all request and response formatting.
Key Conventions¶
- Resource objects include
type,id, andattributes - Field names are dasherized (e.g.,
base-priceinstead ofbase_price) - Query parameter names are dasherized too (e.g.,
filter[start-date],include-body,recent-sync-threshold-minutes). Where an older snake_case spelling is still accepted, the endpoint's page says so; use the dasherized name in new integrations. - Resource types (
type) are plural and dasherized (e.g.,listings,users,calendar-entries). Compound resource types use a singular modifier on a plural head noun —min-stay-customizations, notmin-stays-customizations— even when the endpoint path segment is plural (/customizations/min-stays/). That path-vs-type difference is intentional; treattypeas the stable identifier to match on.
Content-Type¶
Requests should use the JSON:API media type:
The API also accepts application/json for backward compatibility.
Response Format¶
Single Resource¶
Collection¶
Collections include meta with pagination info and links for navigation (see Pagination):
{
"data": [ ... ],
"meta": { "pagination": { "count": 150, "page": 1, "pages": 6 } },
"links": { "first": "...", "last": "...", "next": "...", "prev": null }
}
Error Response¶
{
"errors": [
{
"status": "404",
"detail": "Listing 12345 not found or not owned by this application",
"source": {"pointer": "/data"},
"code": "not_found"
}
]
}
code is the stable identifier to branch on. Note that title is not
present on most errors. See Error Handling for the full
error-object structure, the status codes the API returns, and best practices.
Request Format (POST/PATCH)¶
When creating or updating resources, wrap the payload in a data object:
{
"data": {
"type": "users",
"attributes": {
"first-name": "John",
"last-name": "Doe",
"email": "john@example.com"
}
}
}
Compound Documents (Sideloading)¶
Some endpoints support including related resources in a single request using the ?include= parameter. This eliminates the need for multiple API calls.
Related resources appear in the top-level included array, with the relationship declared in relationships:
{
"data": {
"type": "listings",
"id": "12345",
"attributes": { ... },
"relationships": {
"owner": { "data": {"type": "users", "id": "789"} }
}
},
"included": [
{
"type": "users",
"id": "789",
"attributes": { ... }
}
]
}
See each endpoint's documentation in the Swagger UI for the list of supported include values.
Pagination¶
Collection endpoints return paginated results. Use query parameters to control pagination, sorting, and filtering.
All collection endpoints use page-based pagination:
| Parameter | Description |
|---|---|
page[number] |
Page number (1-indexed). Defaults to 1 |
page[size] |
Items per page. The default and maximum depend on the endpoint |
Page sizes are not the same on every endpoint. Most collections default to 25 items per page with a maximum of 100, but the date-series endpoints return far larger pages so a normal date range fits in one response:
| Endpoint | Default page[size] |
Maximum |
|---|---|---|
| Listings, Compsets, Users, accounts, webhook events | 25 | 100 |
| Calendar | 366 | 731 |
| Market insights | 366 | 731 |
The calendar default is deliberate: a full year of daily entries arrives in a single response, so a default 365-day window needs one request rather than 15. Do not paginate a date range into 25-entry pages — it multiplies your request count for no benefit and will run you into rate limits.
Only page[number] and page[size] are recognized
Any other page[…] key — page[limit] and page[offset] among them — is
silently ignored. The request succeeds with 200 and the default page
size, so a client using the wrong parameter name gets working-looking
responses while paging through far more requests than it intended. If a
page[size] you set does not seem to apply, check the parameter name first.
Responses include pagination info in meta and navigation links in links:
{
"meta": {
"pagination": {
"count": 150,
"page": 1,
"pages": 6
}
},
"links": {
"first": "...?page%5Bnumber%5D=1",
"last": "...?page%5Bnumber%5D=6",
"next": "...?page%5Bnumber%5D=2",
"prev": null
}
}
Use the links URLs to navigate between pages without constructing URLs manually.
Sorting¶
Use the sort parameter with field names, dasherized like every other field
name. Prefix with - for descending order. Multiple sort fields are
comma-separated.
# Newest first
?sort=-created-at
# Multiple fields: by city ascending, then newest first
?sort=city,-created-at
Each endpoint sorts on its own small set of fields, and an unsupported field
is an error, not a no-op. Sorting on a field the endpoint does not support
returns 400 with "invalid sort parameter: <field>" — the request does not
fall back to the default order. Listings, for example, sort on created-at,
title, and city only; a sort=-base-price fails.
Each endpoint documents its own sortable fields in the Swagger UI.
Filtering¶
Use filter[field] parameters to narrow results.
On the collection endpoints that filter through the query layer — listings,
compsets, and users — an unsupported filter is an error, like sort: the API
returns 400 with "invalid filter[<field>]".
Date-range filters are validated differently
The endpoints that expose a date window — calendar,
market insights,
manual overrides,
and webhook events — read those filters
directly rather than through the query layer, so an unrecognized
filter[…] key there is silently ignored instead of rejected. Getting
the name wrong — filter[start] instead of
filter[start-date], say — returns 200 with the endpoint's default
range rather than an error. Check the parameter names on the endpoint's own
page, and verify the dates in the response match what you asked for.
Each endpoint documents its own available filters in the Swagger UI.
Putting It All Together¶
Combine pagination, sorting, and filtering in a single request:
curl -X GET "$BASE_URL/api/v1/listings/?page[number]=1&page[size]=25&sort=-created-at&filter[enabled]=true" \
-H "Authorization: Bearer <token>" \
-H "Accept: application/vnd.api+json"
Types in Webhook Payloads¶
A webhook delivery is a JSON:API document too, and it carries two
fields named type:
{
"meta": { "type": "listing.refreshed", "...": "..." },
"data": { "type": "listing-refreshed-events", "...": "..." }
}
data.type is the ordinary JSON:API resource type — required by the spec, and the key
you deserialize on. JSON:API is deliberately agnostic about inflection (singular vs.
plural, dashes vs. underscores) but demands consistency, so event resource types follow
the same dasherized-plural convention as the rest of the API.
meta.type is a Beyond extension, and it is the field you should route on. JSON:API
reserves the meta object for exactly this: implementation-specific information the spec
itself says nothing about — and the spec says nothing at all about events or webhooks.
Why not a top-level type, as Standard Webhooks
suggests? Because that would be illegal JSON:API. The spec restricts a document's
top-level members to data, errors, meta, jsonapi, links, and included; a
top-level type is not among them. Nesting the event type under meta satisfies both
specifications at once.
Where the event id lives¶
For the same reason, the event's ULID appears in data.id rather than in meta. JSON:API
requires every resource object to carry an id, and the resource this document describes
is the event — so data.id is the event id, not the id of the listing or account the
event concerns. Standard Webhooks, meanwhile, puts the identifier in the webhook-id
header and uses it as the idempotency key. Between them the two specs already account for
the id twice; a third copy under meta would be redundant, so there is not one.