{
"error": {
"code": "tenant_suspended",
"detail": "This account is suspended.",
"request_id": "…",
"fields": { "slug": ["That name is taken."] }
}
}
One versioned API. Scoped keys. Signed webhooks.
Everything the dashboard does, the API does — it is the same surface. Keys carry their own scopes, their own rate limit and a pinned version, so an integration upgrades when its author is ready and not when we ship.
Included from Growth REST · JSON /api/v1
Resolved in this order.
A key names its own agency and is checked first, so one agency's key can never act on another's data.
| Source | Used by |
|---|---|
| Host | Public storefront requests |
| Authorization: Bearer sk_… | API keys and integrations |
| X-Tenant-ID | Dashboard, for agencies you belong to |
| X-Request-ID | Yours, echoed back and kept on the audit trail |
Always this shape.
Field errors arrive keyed by field, and every failure carries the request id you can quote at us.
| Status | When |
|---|---|
| 400 | Validation failed, or a bad one-time code |
| 401 / 403 | Not signed in · not a member · missing a permission |
| 402 | The agency is suspended |
| 409 | Slug taken · seats sold out · coupon exhausted · publish pre-flight failed |
| 429 | Rate limited or a code re-sent too soon — carries the wait |
Seats and coupons are claimed with a single conditional write rather than read-then-write, so two simultaneous bookings cannot both take the last bed — one of them gets the 409.
Six places to work.
Catalogue
/catalogue/packages/ · hotels · flights · prices · itinerary ·
discounts · /catalogue/visa-options/ ·
/catalogue/visa-applications/
Creating a package accepts the whole graph in one call — the same service the sign-up wizard uses.
CRM
/crm/enquiries/ · /crm/leads/{id}/transition/ ·
/crm/quotes/ · /crm/bookings/{id}/confirm/ ·
/crm/coupons/validate/
Stages move through action endpoints, never a field write, so the timeline cannot be bypassed.
Site & comms
/website/domains/{id}/verify/ ·
/website/revisions/{id}/restore/ ·
/comms/providers/{id}/send-test/ · /comms/templates/ ·
/seo/redirects/
Credentials come back masked, never as values — saving them is one way.
Keys, hooks & data
/apps/keys/ · /apps/webhooks/{id}/rotate-secret/ ·
/apps/deliveries/ · /apps/data-jobs/ ·
/billing/usage/
A new key's raw value is shown once. Deliveries log status, attempts and duration.
Public, no key
POST /public/enquiries/
For a form on your own site. The agency comes from the host, never the payload, and it is throttled by address.
Custom fields
/catalogue/metafield-definitions/ · /catalogue/metafields/
Typed fields on packages, visas, customers, bookings and pages — added without a migration, and optionally published to your storefront.
Webhooks that keep trying
Subscribe to booking, lead and visa events. Each delivery is signed, retried with backoff and logged; the secret rotates on demand, and the subscription pins a payload version so an old integration keeps receiving the shape it was written against.
Import and export as jobs
CSV in and out for packages, customers and bookings, with the exact headers published, a row count and an error report for what didn't load. Big imports run in their own lane so they cannot delay anyone's booking confirmation.
Version history you can call
Pages, packages, visa options and theme settings are snapshotted on change and restorable in one call — and the restore is itself recorded as a version.