Skip to content

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, and attributes
  • Field names are dasherized (e.g., base-price instead of base_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, not min-stays-customizations — even when the endpoint path segment is plural (/customizations/min-stays/). That path-vs-type difference is intentional; treat type as the stable identifier to match on.

Content-Type

Requests should use the JSON:API media type:

Content-Type: application/vnd.api+json

The API also accepts application/json for backward compatibility.

Response Format

Single Resource

{
  "data": {
    "type": "listings",
    "id": "12345",
    "attributes": { ... }
  }
}

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.

GET /api/v1/listings/12345/?include=owner

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.

# Single filter
?filter[enabled]=true

# Multiple filters
?filter[owner]=123&filter[enabled]=true

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.