REST API overview
developerA token-authenticated JSON API over the core resources, scoped to the token owner's domain, sites, and privileges.
The Apolaris AI REST API is a versioned, token-authenticated JSON interface to the same data you see in the app — customers, bookings, sales, services, employees, sites, inventory, and expenses. It lets you build dashboards, sync tools, and automations against live data without driving a browser session or scraping screens, and because it reuses the platform's own tenancy and permission model, your integrations stay safe by construction.
- Base URL:
/api/v1/(for examplehttps://your-domain/api/v1/) - Interactive docs:
/api/docs/(Swagger UI) — click Authorize, paste your token, and try endpoints live. The raw OpenAPI spec is at/api/schema/. The docs require a logged-in account whose organization has the API module enabled, and show only the operations your account can authorize.
The API is a separately-enabled module, off by default. Until an administrator enables it for your organization, every /api/v1/ request returns 403 api_disabled — even with a valid token — and the developer docs stay hidden. Ask your administrator to turn on Developer REST API in Module settings.
Authenticate
The API uses a personal API token, not the session cookie.
- In the app, open Account → API tokens (
/customers/account/api-tokens/) and click Generate token. The raw token is shown once and looks likeztk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx— copy it immediately. - Send it as a bearer token on every request:
curl -H "Authorization: Bearer ztk_your_token_here" \
https://your-domain/api/v1/me/
X-API-Key: ztk_… is also accepted as a fallback.
Scopes
When you generate a token you choose its scopes — an allow-list of resource:action permissions such as customers:read, bookings:write, or sales:read. Grant only what the integration needs. Leaving every scope unchecked with Full access on issues an unrestricted token.
A scope can only ever narrow a token: the effective permission of a request is the intersection of the token's scopes and the owner's can_* privileges, so a token can never do more than its user — and often does less. A call outside the token's scopes returns 403 insufficient_scope. GET /api/v1/me/ echoes the token's effective scopes; GET /api/v1/meta/ lists every available scope with a description.
Treat tokens like passwords. Store them in a secret manager — never in client-side code or version control — and revoke unused ones. The raw value is never stored and cannot be re-displayed; if you lose it, generate a new one.
Every authenticated call stamps the token's last used time, so the API tokens page shows you which tokens are still active. Revoking a token is a soft delete — it stops authenticating immediately but stays in the list for the audit trail.
Tenancy & privilege scoping
A token acts as its owner and inherits exactly what that user can do in the UI:
- Organization (tenant): every response is scoped to the user's organization. You can never see another organization's data.
- Visible sites: non-managers see only their assigned or granted sites. For resources that carry a site (bookings, expenses) this filters directly on the record's site. Sales have no site field, so a non-manager's sales are scoped through the attending employee's sites — you see a sale only if the employee who handled it works at a site you can see. The same rule gates writing a sale: you cannot record one for an employee at a site you cannot access.
can_*privileges: reads require the matchingcan_view_*, writes require the matching add/edit privilege. Managers (is_manager) bypass every read/write gate within their organization.
A token can therefore never do more through the API than its user can in the app, and revoking the token (or disabling the user) stops access immediately. See Users & privileges and the Privileges reference.
Identity & index
GET /api/v1/me/ returns your identity (user, organization), your is_manager flag, the full privileges map, and your visible_site_ids. Start here to confirm a token works.
GET /api/v1/ is a lightweight index you can hit to discover the service and its resource URLs:
{
"service": "Apolaris AI REST API",
"version": "v1",
"authenticated_as": "Jane Doe",
"domain": "Your Organization",
"resources": {
"customers": "/api/v1/customers/",
"bookings": "/api/v1/bookings/"
/* services, sales, employees, sites, inventory, expenses */
},
"me": "/api/v1/me/",
"status": "/api/v1/status/",
"meta": "/api/v1/meta/",
"activity": "/api/v1/activity/",
"docs": "/api/docs/",
"schema": "/api/schema/"
}
Resources
| Resource | List / Create | Detail / Update | Read gate | Write gate |
|---|---|---|---|---|
| Customers | GET, POST /customers/ |
GET, PATCH /customers/{id}/ |
can_view_customer |
can_add_customer / can_edit_customer |
| Bookings | GET, POST /bookings/ |
GET, PATCH /bookings/{id}/ |
can_view_booking |
can_add_booking / can_edit_booking |
| Sales | GET, POST /sales/ |
GET /sales/{id}/ |
can_view_sale |
can_manage_sales |
| Services | GET /services/ |
GET /services/{id}/ |
can_view_service |
— |
| Employees | GET /employees/ |
GET /employees/{id}/ |
can_view_employee |
— |
| Sites | GET /sites/ |
GET /sites/{id}/ |
can_view_site |
— |
| Inventory | GET /inventory/ |
GET /inventory/{id}/ |
can_view_reports |
— |
| Expenses | GET /expenses/ |
GET /expenses/{id}/ |
can_view_reports |
— |
Customers, bookings, and sales support privilege-gated writes; the rest are read-only.
Filters are per-resource, not a shared set — only pass a filter the endpoint actually supports (unknown query parameters are ignored). The interactive /api/docs/ reference lists the exact parameters for each operation:
| Resource | Filters on GET (list) |
|---|---|
| Customers | ?q= (name / email / phone) |
| Bookings | ?site=, ?employee=, ?customer=, ?status=, ?date_from=, ?date_to= |
| Sales | ?employee=, ?customer=, ?payment_status=, ?date_from=, ?date_to= |
| Services | ?q=, ?category=, ?is_active= (0/1) |
| Employees | ?q=, ?site=, ?is_active= (0/1) |
| Sites | ?q= (name / address) |
| Inventory | ?q= (name / SKU), ?category=, ?low_stock=1 |
| Expenses | ?site=, ?category=, ?status=, ?date_from=, ?date_to= |
?status= filters bookings and expenses; it does not apply to sales. Sales use ?payment_status= instead. Passing the wrong one simply has no effect — it does not raise an error.
Creating a booking with customer details (instead of an existing customer_id) and no match found will mint a walk-in customer, exactly as the calendar UI does. This rides on can_add_booking. Pass a customer_id to avoid creating new records.
Module endpoints (Cameras, Contact Center, AI Copilot)
These extend the API to the optional product modules so you can build integrations. Each one also requires its product module to be enabled for your organization (on top of the API module); when the module is off the endpoint returns 403 module_disabled. They carry their own scopes.
| Resource | Endpoints | Scope | Access | Module |
|---|---|---|---|---|
| Cameras | GET /cameras/, GET /cameras/{id}/ |
cameras:read |
manager | Cameras |
| Camera events | GET /camera-events/, GET /camera-events/{id}/ |
cameras:read |
manager | Cameras |
| Conversations | GET /conversations/, GET /conversations/{id}/ |
ccaas:read |
can_use_ccaas / manager |
Contact Center |
| Messages | GET, POST /conversations/{id}/messages/ |
ccaas:read / ccaas:write |
can_use_ccaas / manager |
Contact Center |
| AI chat | POST /assistant/chat/ |
assistant:chat |
manager | AI Copilot |
| AI insights | GET /assistant/insights/ |
assistant:read |
manager | AI Copilot |
- Cameras are read-only. The detection-event feed (
/camera-events/) filters on?type=,?camera=,?site=,?date_from=,?date_to=and the?after=id cursor for incremental polling. RTSP URLs/credentials are never returned. - Contact Center: list/read conversations and their messages, and
POST /conversations/{id}/messages/to send a reply (this switches the conversation to human mode so the AI agent stops auto-replying). Provider session ids and secrets are never returned. -
AI Copilot:
POST /assistant/chat/with{message, thread_id?}returns{thread_id, reply, ui_actions}. It is read-only over the API — the copilot can answer and analyse but cannot create or modify records (do writes through the explicit endpoints above). It is manager-only because the copilot reads domain-wide data. -
Analytics (
analytics:read, manager, module Analytics):GET /analytics/summary/returns aggregate KPIs (revenue, bookings, expenses, net profit, inventory value, top services) for a period (?date_from=,?date_to=,?days=,?site=). Safe aggregates only — no PII. - Loyalty (
loyalty:read/loyalty:write,can_use_loyalty/manager, module Loyalty):GET /loyalty/accounts/(+{id}/with tier + ledger),GET+POST /loyalty/accounts/{id}/transactions/. The POST awards/redeems points ({txn_type: earn_manual|adjust|redeem, points, reason}) through the canonical funnel — redeem/adjust can never drive the balance negative. - Discounts (
discounts:read,can_manage_discounts/manager, module Discounts):GET /discount-codes/(+{id}/), andPOST /discount-codes/validate/({code, subtotal, customer_id?}) to check a code without redeeming it — ideal for an external checkout. - HR (read-only,
hr:read):GET /departments/,GET /leave-types/,GET /leave/. Leave is scoped: approvers/managers see the whole organization, everyone else sees only their own.
| Resource | Endpoints | Scope | Access | Module |
|---|---|---|---|---|
| Analytics | GET /analytics/summary/ |
analytics:read |
manager | Analytics |
| Loyalty | GET /loyalty/accounts/ (+{id}/, +{id}/transactions/), POST .../transactions/ |
loyalty:read / loyalty:write |
can_use_loyalty/manager |
Loyalty |
| Discounts | GET /discount-codes/ (+{id}/), POST /discount-codes/validate/ |
discounts:read |
can_manage_discounts/manager |
Discounts |
| HR | GET /departments/, /leave-types/, /leave/ |
hr:read |
manager / can_view_employee / vacation perms |
— |
The enum vocabularies for these (camera_event_type, conversation_status, conversation_mode, message_direction, message_status, loyalty_txn_type, discount_type, leave_status) are listed by GET /api/v1/meta/.
Responses, pagination & errors
List endpoints are paginated. Control them with ?page=N and ?page_size=N (default 50, max 200):
{
"count": 128,
"page": 1,
"num_pages": 3,
"page_size": 50,
"next": 2,
"previous": null,
"results": [ /* objects */ ]
}
List endpoints also accept ordering (comma-separated whitelisted keys; prefix - for descending), fields / exclude (sparse fieldsets — id is always kept), and the time windows created_since / created_until / updated_since (ISO date or datetime). See each endpoint's parameters in /api/docs/.
Detail, create, and update return the single object as JSON; creates return HTTP 201. Errors use a consistent envelope (documentation_url points back to these docs):
{ "error": "insufficient_scope", "detail": "This token is not authorized for the \"customers:write\" scope.", "documentation_url": "/api/docs/" }
| Status | error |
Meaning |
|---|---|---|
| 400 | validation |
Bad or missing fields, or unparseable JSON body. |
| 401 | unauthorized |
Missing or invalid/revoked token. |
| 403 | forbidden |
Authenticated, but the owner lacks the required privilege. |
| 403 | insufficient_scope |
The token is not authorized for the operation's scope. |
| 403 | api_disabled |
The API module is not enabled for your organization. |
| 403 | domain_suspended |
The organization's account is suspended — every API call returns {"error": "domain_suspended"} until it is reinstated. Have your integration back off and contact support; retrying will not help. |
| 404 | not_found |
No such object in your organization. |
| 405 | method_not_allowed |
HTTP verb not supported on this endpoint. |
| 429 | rate_limited |
Too many requests — see Retry-After and the X-RateLimit-* headers. |
| 500 | server_error |
Unexpected error. |
Validation detail strings
The detail text on a 400 validation response is stable, so you can branch on it. The ones you are most likely to hit:
detail |
When |
|---|---|
first_name is required. |
Creating a customer without a first_name. |
A customer with this email already exists. |
The email collides with an existing customer — email is unique per organization (returned on both create and update). |
amount must be a number. |
Creating a sale with a non-numeric amount (and no line items). |
Request body must be valid JSON. |
The request body is not parseable JSON. |
Booking create/update returns validation with the message from the calendar's own assignment, conflict, and on-leave guards (the API runs the same checks as the UI).
Notes & limits
- Read-replica safe.
GETendpoints never write, so they work against a read-only failover node. OPTIONSpreflight (CORS). AnOPTIONSrequest to any endpoint is answered without a token and returns anAllowheader listing the supported verbs (always includingOPTIONS). This is what browsers send before a cross-origin call — it never exposes data, so it does not require authentication.- Rate limiting is enforced per token. Every response carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(Unix epoch); exceeding the limit returns429 rate_limitedwith aRetry-Afterheader. Back off whenRemainingreaches0. - Caching. Read responses send
Cache-Control: private(they embed your identity — never cache them in a shared proxy); identity and write responses sendno-store.GET /api/v1/meta/is cacheable for a few minutes. - Idempotent creates.
POSTcreates accept an optionalIdempotency-Keyheader. A retry with the same key replays the original response instead of creating a duplicate — use a fresh UUID per logical create. - Versioning. This is
v1. Breaking changes will ship under a new version prefix; additive fields may appear withinv1.
Developer utility endpoints
| Endpoint | Purpose |
|---|---|
GET /api/v1/status/ |
Health/smoke test — confirms the token authenticates and the module is on; reports the rate-limit policy and server time. |
GET /api/v1/meta/ |
Reference vocabularies for building integrations: enum values, currencies, languages, available scopes, your organization settings and accessible sites. |
GET /api/v1/activity/ |
Recent activity feed — the most recently created/updated bookings and sales you can see (limit, since params). Respects your scopes. |
Tips
Start with GET /api/v1/me/ to confirm your token, see your is_manager flag, and read your visible site ids before building queries.
- Use
/api/docs/(Swagger UI) to explore and test endpoints interactively before writing code. - Page through large lists with
page_size=200and follow thenextfield until it isnull. - Ready to build? See API examples for copy-paste curl and Python, and Integrations & webhooks for connecting external systems.