Skip to main content
◆ Zayos API · v1

Build on top of
Zayos.

REST + webhooks. Per-tenant API keys. HMAC-signed events with replay defense. Built for the engineers on your team, not just point-and-click integrations.

<your-app-domain>/api/v1v1TLS only

Auth

Per-tenant API keys. Bearer token in the Authorization header.

Operators generate a per-tenant key in /app/api-keys and present it as Authorization: Bearer zk_live_.... Each key carries a set of scopes (menu:read, customers:read, orders:read, orders:write, webhooks:manage) and a per-minute rate limit; an endpoint returns 403 forbidden_scope if the key lacks the scope it needs. A key can be bound to a single brand or left org-wide. All requests are TLS-only; HTTP is redirected. (A partner OAuth client_credentials flow is on the roadmap but not available yet — use API keys today.)

REST

Live v1 endpoints. JSON in, JSON out. Errors are flat: {"error": "code", "message"?}.

GET/api/v1/menu+

Fetch the active menu (categories + items) for the brand bound to the key. Org-scoped keys must bind to a brand if the org runs more than one brand.

Required scope: menu:read

Capped at 2000 items. meta.truncated is true if the cap was hit.

Response
{ "data": { "categories": [...], "items": [...] }, "meta": { "brand_scope": "brand", "count": { "categories": 6, "items": 48 }, "truncated": false } }
GET/api/v1/customers+

List the organization's customer book. Offset paginated. Brand-bound keys still return the org-wide book (customers are org-level).

Required scope: customers:read

Query params: ?page (>=1) and ?page_size (1-200). Default page 1.

Response
{ "data": [...], "meta": { "brand_scope": "organization", "page": 1, "page_size": 50, "total": 1280, "has_more": true } }
GET/api/v1/orders/{order_number}+

Fetch a single order by its order number, with status, money breakdown, and timestamps. Brand-bound keys can only read their brand's orders.

Required scope: orders:read

Response
{ "data": { "order_number": "Z-1024", "status": "ready", "total_cents": 4180, ... }, "meta": { "brand_scoped": true } }
GET/api/v1/webhooks+

List this org's active outbound webhook subscriptions. The signing secret is never returned here.

Required scope: webhooks:manage

Response
{ "data": [{ "id": "...", "url": "...", "events": [...] }], "meta": { "count": 2 } }
POST/api/v1/webhooks+

Create an outbound webhook subscription. The signing secret is returned exactly once in this response — store it now.

Required scope: webhooks:manage

201 on success. url must be HTTPS and not a zayrev.com / zayos.com host. 1-50 events, all from the catalog below.

Request body
{ "url": "https://example.com/hooks/zayos", "events": ["order.created", "order.ready"] }
Response
{ "data": { "id": "...", "url": "...", "events": [...], "created_at": "...", "signing_secret": "whsec_..." } }
DELETE/api/v1/webhooks/{id}+

Deactivate an outbound webhook subscription. Soft delete — the delivery history is preserved.

Required scope: webhooks:manage

Response
{ "data": { "id": "...", "deactivated": true } }
◆ Coming soon — not yet available

These are part of the planned v1 surface but are not live yet. POST /api/v1/orders returns 501 today (its request shape is stable, so you can integrate ahead of the writes going live); the others are not built. Do not depend on these returning data.

POST/api/v1/orderssoon+

Create an order via the headless API. Re-prices server-side, runs the same inventory / capacity / promo checks as the hosted checkout.

Required scope: orders:write

Returns 501 today. customer.name, customer.email, customer.phone, location_id, and at least one item are required. Writes go live in a later v1 release; the request shape is stable so you can integrate now.

Request body
{ "customer": { "name": "Maya R.", "phone": "+13055550101", "email": "maya@example.com" }, "fulfillment": "pickup", "location_id": "...", "items": [{ "menu_item_id": "...", "quantity": 2 }] }
Response (today)
{ "error": "not_implemented_v1", "message": "...", "received": { "items_count": 1, "fulfillment": "pickup", "location_id": "..." } }
POST/api/v1/refundssoon+

Issue a refund against an order. Not yet available.

Not built yet. Issue refunds from the operator dashboard for now.

POST/api/v1/menu/itemssoon+

Create a menu item under a brand. Not yet available.

Not built yet. Menu writes happen in the operator dashboard for now; the API is read-only on menu.

PATCH/api/v1/menu/items/{id}/availabilitysoon+

86 / un-86 an item, optionally per location. Not yet available.

Not built yet. Toggle availability in the operator dashboard for now.

Webhooks

HMAC-SHA256-signed event push to a URL you configure. At-least-once delivery with retries.

Configure a delivery URL in /app/integrations. We POST every relevant event with header `x-zayos-signature`, whose value is the hex-encoded HMAC-SHA256 of the exact raw request body using your shared secret. Verify by recomputing HMAC-SHA256(rawBody, secret) and constant-time-comparing the hex. We retry up to 5 times with backoff on non-2xx responses. (No timestamp header is sent, so there is no timestamp-based replay window — rely on the event id for idempotency.)

◆ Event catalog
EventWhen it fires
order.createdA new paid order entered the kitchen feed.
order.acceptedOperator accepted the order.
order.readyOrder is ready for handoff.
order.deliveredCustomer received the order.
order.cancelledOrder was cancelled (reason in payload).
order.refundedFull or partial refund issued (refund_type in payload).
menu.updatedThe brand's menu changed (item added, edited, or 86'd).

Rate limits

Per API key. Each key has its own per-minute budget, set in /app/api-keys (default 60/min).

The public API requires a key on every request, so limits are keyed per API key (not per IP). Read endpoints (GET menu / customers / orders / webhooks) get the key's full per-minute budget; write endpoints (POST orders, POST/DELETE webhooks) get half that budget from a separate bucket. Exceeding it returns 429 {"error": "rate_limited", "retry_after_seconds": N} with a Retry-After header. Every response also carries X-RateLimit-Limit and X-RateLimit-Remaining.

Errors

Flat shape, stable error codes. Switch on the code; the message is human-readable and may change.

Every error returns a flat JSON body: {"error": "snake_case_code", "message": "Human-readable explanation"}. The message field is optional and present on most (not all) errors. There is no nested error object and no request_id field today. Auth: 401 "unauthorized" (missing/invalid key), 403 "forbidden_scope" (key lacks the required scope). Validation (400): "invalid_json", "invalid_pagination", "customer_required", "location_id_required", "items_required", "too_many_items", "invalid_url", "blocked_url", "invalid_events", "unknown_events", "invalid_id", "brand_required_for_multi_brand_org". Not found (404): "not_found", "order_not_found". Conflict (409): "webhook_cap_reached". Method (405): "method_not_allowed". Rate limit (429): {"error": "rate_limited", "retry_after_seconds": N} plus a Retry-After header. Not built yet (501): "not_implemented_v1". Transient/backend (503): "service_unavailable" and the *_query_failed / *_create_failed / *_delete_failed family.

Quickstart

Hello world (curl)

Replace <your-app-domain> with your Zayos app origin (e.g. your dashboard host). Fetch the live menu — a read-only call that works today:

curl https://<your-app-domain>/api/v1/menu \
  -H "Authorization: Bearer ${ZAYOS_API_KEY}"

Order creation is documented but not live yet — this request returns 501 today. The shape is stable, so you can build against it now:

curl https://<your-app-domain>/api/v1/orders \
  -H "Authorization: Bearer ${ZAYOS_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": { "name": "Maya R.", "phone": "+13055550101", "email": "maya@example.com" },
    "fulfillment": "pickup",
    "location_id": "...",
    "items": [{ "menu_item_id": "...", "quantity": 2 }]
  }'