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:
| Parameter | Type | Default | Notes |
|---|---|---|---|
page | integer | 1 | 1-based page index |
per_page | integer | 50 | max 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))
doneFor 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/ordersIdentifiers
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
/customersand embedded in orders):name,email,phone,company_name, andtax_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
DeprecationandSunsetheaders and aLinkto 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.