Docs

Conventions

Last updated: 26 Oct 2026

Core request/response conventions for the Admin API.

Base path

The API is served from the dedicated api subdomain:

https://api.servos.dk/admin/v0

The host is fixed — it is not org-subdomained. The organization is derived from the API key, so there is no organization or location segment in the path.

Content type

Responses are JSON (Content-Type: application/json).

Response envelope

Every successful response wraps its payload in a data key.

Collections also include a meta object with pagination:

{
  "data": [{ "id": "…" }, { "id": "…" }],
  "meta": { "page": 1, "per_page": 50, "total": 1234, "total_pages": 25 }
}

Single resources:

{ "data": { "id": "…" } }

Pagination

Collection endpoints are page-based:

ParameterTypeDefaultNotes
pageinteger11-based page index
per_pageinteger50max 200

Read meta.total_pages (or compare meta.page to it) to know when to stop:

page=1
while :; do
  resp=$(curl -s -H "Authorization: Bearer $KEY" \
    "https://api.servos.dk/admin/v0/orders?page=$page&per_page=100")
  echo "$resp" | jq '.data[]'
  total_pages=$(echo "$resp" | jq '.meta.total_pages')
  [ "$page" -ge "$total_pages" ] && break
  page=$((page + 1))
done

For keeping data current afterwards, don't re-page everything — use the Incremental sync loop.

Conditional requests (ETag)

Every response carries an ETag. Send it back as If-None-Match and you'll get 304 Not Modified (no body) when nothing has changed — cheaper polling:

curl -H "Authorization: Bearer $KEY" -H 'If-None-Match: "abc123"' \
  https://api.servos.dk/admin/v0/orders

Identifiers

Every resource is addressed by id, an opaque UUID string (the resource's public id). Numeric internal ids are never exposed. Use the id from a list response as the path segment for the matching show endpoint.

Dates and times

  • Dates: ISO 8601 date, YYYY-MM-DD.
  • Timestamps: ISO 8601 with offset, e.g. "2026-07-04T09:00:00+02:00".

Money

Monetary fields are objects, not raw numbers:

{ "cents": 25000, "currency": "DKK", "formatted": "250 kr." }

Use cents (integer, minor units) for any calculation; formatted is for display only.

Status labels

Booking status fields (on bookings, stays, tickets, and inside an order's line-item detail) are human-readable display labels such as "Upcoming", "Checked in", or "No-show" — not stable machine codes. The exact set per resource is listed on each resource page, but the wording and set can change, so don't switch on them for business logic. Derive state from explicit fields instead:

  • Cancelled? Use cancelled_at (present once cancelled) rather than matching the string "Cancelled".
  • Visit progress (arrived, departed): derive from the date/time fields (check_in_date, starts_at/ends_at) and the events feed.

Customer privacy (GDPR)

By default the Admin API returns no customer personal data: no name, email, phone, or address. A customer is represented by a stable, pseudonymous id plus non-identifying metadata only. The id is consistent across that customer's bookings, so you can recognise a returning customer and group their activity without learning who they are.

A key granted the customer_data:read permission also receives:

  • on every customer (in /customers and embedded in orders): name, email, phone, company_name, and tax_id;
  • on every order: its billing_address.

The organization decides which keys get it, and grants it only to an integration it has a legal basis (consent, a data processing agreement) to share guest data with. A field without a value is null, including billing_address on an order without one. For a key without the permission, these fields are left out of the response altogether.

Versioning & stability

The API is versioned in the path (/admin/v0); there is no version header. Within a version we treat these as a stable contract:

  • We add fields and endpoints without notice — your client must ignore unknown fields rather than break on them.
  • We do not remove or rename fields, change a field's type, or change an endpoint's meaning within /admin/v0. Breaking changes ship under a new version path (/v2), and the old version is supported through a deprecation window communicated in advance.
  • The exception is retiring an old name. We announce it on the resource's page with the date it goes, and until then responses that use the old name carry Deprecation and Sunset headers and a Link to that page.

Rate limiting

Requests are rate limited per API key. Exceeding the limit returns 429 Too Many Requests with a Retry-After header (seconds to wait); back off and retry after it. Apply sensible client-side throttling and use conditional requests to stay well under the limit.