Skip to content
Jaber GroupSmart Pager · Jaber Groupv1.0.0

POS integration API

Smart Pager replaces buzzer pagers at 99Grill and Chicken Dip with printed QR cards. This API lets your point-of-sale system do the cashier's part automatically: link an invoice to a pager card, mark the order ready so the customer's phone rings, ring again, and close the order when it is collected so the card can be reused safely.

بالعربية

يستبدل Smart Pager أجهزة البيجر التقليدية في 99 جريل وتشيكن ديب ببطاقات QR مطبوعة. تتيح هذه الواجهة لنظام الكاشير (POS) أن يقوم بعمل الكاشير تلقائيًا: ربط الفاتورة ببطاقة البيجر، وتحديد الطلب كجاهز ليرنّ هاتف العميل، وإعادة التنبيه عند الحاجة، ثم إغلاق الطلب عند الاستلام لتُستخدم البطاقة من جديد بأمان دون تنبيه العميل السابق. باقي التوثيق باللغة الإنجليزية لأنه موجّه للمطوّرين.

Base URLhttps://smart-pager-production.up.railway.app/api/v1
Contents

Overview

One order is one invoice on one printed pager card. The cashier hands the customer a numbered card; your POS links the invoice to that card number. The customer scans the card's QR code and follows the order on their phone. When the kitchen is done you mark the order ready: the open order page rings on every phone following it, and on branches with phone alerts the customer also gets a call (or an SMS) at the order's phone number. When the customer picks up the food and returns the card, you collect the order and the card is free for the next customer, who can never receive the previous customer's alerts.

  • JSON over HTTPS at https://smart-pager-production.up.railway.app/api/v1, authenticated with an API key per POS integration.
  • Two ways to address an order: by its id, or by branch + invoice number if your POS only knows the invoice.
  • Safe retries with Idempotency-Key, optimistic concurrency with ETag / If-Match.
  • Signed webhooks for every change, plus an event feed to catch up on anything missed.

5 minutes

Quickstart

  1. 1. Create an API key

    In the Smart Pager admin console, create an integration for your POS, scoped to a single branch (in-store POS) or a whole brand (central POS), and generate a key. Copy it right away: it is shown once and only its hash is stored. Keys look like pgr_ab12cd34ef_…. Check it:

    curl
    curl "https://smart-pager-production.up.railway.app/api/v1/me" \
      -H "Authorization: Bearer $SMARTPAGER_API_KEY"
  2. 2. Link the invoice to the pager card

    When the cashier hands over card 7, create the order. Generate the Idempotency-Key once, store it with your POS order and reuse it on every retry.

    curl
    curl -X POST "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle/orders" \
      -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
      -H "Idempotency-Key: 0b5e3a0c-6f1d-4d8e-9a77-2c1f3e4d5b6a" \
      -H "Content-Type: application/json" \
      -d '{"pager_number":7,"invoice_number":"A-1042","external_id":"pos-88231"}'
    201 created
    {
      "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
      "status": "preparing",
      "version": 1,
      "pager_number": 7,
      "invoice_number": "A-1042",
      "…": "…"
    }
  3. 3. Mark it ready

    The customer's phone rings. Use the order id, or the invoice route if that is all your POS has:

    curl
    curl -X POST "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10/ready" \
      -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
      -H "Idempotency-Key: 3c9e1f7a-52d4-4b8e-a1f0-6d2b7c8e9a41"
    curl
    curl -X POST "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle/invoices/A-1042/ready" \
      -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
      -H "Idempotency-Key: 3c9e1f7a-52d4-4b8e-a1f0-6d2b7c8e9a41"
  4. 4. Collect it

    When the food is handed over and the card is back, close the order. The card is free again.

    curl
    curl -X POST "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10/collect" \
      -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
      -H "Idempotency-Key: 5f7b9d1c-3e5a-4c2b-8d6f-1a3c5e7f9b25" \
      -H "Content-Type: application/json" \
      -d '{"direct":false}'

Order lifecycle

Order lifecycleA new order is preparing. Mark ready moves it to ready, where it can be rung again. Collect closes a ready order (or a preparing one with direct set to true). Cancel closes any active order. Active orders expire automatically after their timeout.ACTIVEpreparingreadycollectedcancelledexpiredreadycollectringcanceltimeout6 h / 2 hcollect · direct
Order statuses
StatusMeaning
preparingLinked to a pager card. The customer sees “being prepared”. Expires after 6 h.
readyThe customer was alerted. Automatic reminders follow after 1.5 min and 4 min unless the customer taps the optional OK on the ready alarm (order.acknowledged). Ring again manually at most every 30 s, up to 6 alerts in total. Expires after 2 h.
collectedHanded over. The pager card is free. Final.
cancelledClosed without handover. The card is free and no further alert is sent. Final.
expiredClosed by the cleanup timer. The card is free. Final.

Guarantees. A pager card has at most one active order, and an invoice has at most one active order per branch. Closing an order (collect, cancel, expire) cancels every pending alert, so a reused card never rings the previous customer. Marking ready is idempotent and never alerts twice: use ring to alert again.

Authentication

Send the key as a bearer token on every request except the index and the OpenAPI document:

http
Authorization: Bearer pgr_ab12cd34ef_<secret>
  • A branch key sees one branch; a brand key sees every branch of one brand. Branches and orders outside the key's reach answer 404 not_found, exactly like missing ones.
  • Missing, unknown or revoked keys answer 401 unauthorized; a key without the endpoint's scope answers 403 forbidden.
  • Keep keys on your server. The API does not allow browser calls (no CORS), because a key in a web page is a leaked key.
  • To rotate, create a new key for the same integration, deploy it, then revoke the old one. Idempotency keys and webhooks belong to the integration, so nothing else changes.
API key scopes
ScopeAllowsDefault
branches:readList and read the branches this key can access.Yes
pagers:readList pager cards of a branch with their live status.Yes
orders:readRead single orders and list a branch's orders.Yes
orders:createLink an invoice to a pager card (create an order).Yes
orders:readyMark an order ready. Alerts the customer's phone (and calls it when a number is on file).Yes
orders:ringAlert the customer again while the order is ready.Yes
orders:collectMark an order collected. Frees the pager card.Yes
orders:cancelCancel an active order. Frees the pager card and stops alerts.Yes
contacts:writeAttach the customer's phone number (with consent) when creating an order. Not granted by default.No
events:readRead the order event feed (webhook reconciliation).Yes

Requests & responses

  • Request and response bodies are UTF-8 JSON with snake_case fields. Bodies are limited to 16 KB.
  • Timestamps are RFC 3339 in UTC (2026-10-17T18:11:42.020Z); ids are UUIDs; event ids look like evt_1289.
  • Invoice numbers are normalized: Arabic-Indic digits become 0-9, spaces are removed and letters are upper-cased, so a-1042 and A-١٠٤٢ are the same invoice.
  • Lists return { "data": [...], "next_cursor": "…", "has_more": true }. Pass next_cursor back unchanged as cursor.
  • Errors are application/problem+json (RFC 9457). Branch on code, never on title. retryable tells you whether the same request can succeed later.
  • Every response carries X-Request-Id; quote it when you report a problem. New fields, endpoints, event types and error codes may be added at any time, so ignore what you do not know.
  • A customer phone number is never returned by the API or sent in webhooks.
409 problem+json
{
  "type": "about:blank#pager_busy",
  "title": "This pager is already assigned to an active order",
  "status": 409,
  "code": "pager_busy",
  "retryable": false,
  "details": {
    "pager_number": 7,
    "invoice_number": "A-1039"
  },
  "request_id": "3f0f4b8e-5a43-4c1e-9d0b-2b8c7e6a1f25"
}

Idempotency

Networks fail after the server already acted. Send an Idempotency-Key header on every POST so a retry cannot create a second order or close the wrong one. It is required to create orders and optional (but recommended) on commands.

  • 1–128 visible ASCII characters. A UUID v4 generated once per operation and stored with your POS record is ideal. Do not derive it from the invoice alone: the same invoice can legitimately get a new order later.
  • Keys are scoped to your integration and remembered for 7 days. The request fingerprint is the method, the path and the JSON body (key order and whitespace do not matter).
Idempotency behaviour
You send the same key with…Result
the same request, first one succeededThe stored status and body are replayed with Idempotency-Replayed: true. Nothing runs twice.
the same request, first one still running409 idempotency_in_progress (retryable, Retry-After). A request abandoned for more than 60 s can be retried.
a different method, path or body after the first one succeeded409 idempotency_conflict
any request after the first one failed (4xx or 5xx)Nothing was stored and a failed command changes nothing, so the key is free again: retry with it, also with a corrected request (for example another pager card after pager_busy).

Concurrency & versioning

Every order has an integer version that grows on each change (ready, ring, reminder, acknowledgement, collect, cancel, expiry). Single-order responses carry it as ETag: "3". Cashiers can act on the same order from the tablet, so send If-Match with the version you last saw when you want a command to apply only to that state:

curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10/cancel" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H 'If-Match: "2"' \
  -H "Content-Type: application/json" \
  -d '{"reason":"invoice_voided"}'

On a mismatch you get 412 version_conflict with details.current_version; re-read the order and decide again. Without If-Match the command applies to the current state. Repeating a command whose result is already in place (ready on a ready order, collect on a collected one, cancel on a cancelled one) returns 200 with the unchanged order.

API versioning

The major version is in the path (/api/v1). Within v1 changes are additive only; the current revision (1.0.0) is in the X-SmartPager-Api-Version header and the OpenAPI document. Webhook bodies carry schema_version: 1. A breaking change would ship as a new major path, announced in advance.

Rate limits

Each integration may make 600 requests per minute (all of its keys together, fixed one-minute windows). Above that you get 429 rate_limited with Retry-After.

Rate limit headers
HeaderMeaning
RateLimit-LimitRequests allowed per window.
RateLimit-RemainingRequests left in the current window.
RateLimit-ResetSeconds until the window resets.

Ringing has its own guard: rings at least 30 s apart (429 ring_too_soon with details.retry_after) and at most 6 alerts per order (409 ring_limit). Prefer webhooks to polling; if you poll the event feed, every 5–10 seconds is plenty.

Endpoints

Paths are relative to https://smart-pager-production.up.railway.app. Every example uses branch jo-7th-circle, invoice A-1042 and pager card 7. Invoice numbers in paths must be URL-encoded (/ → %2F, # → %23).

Meta

Discovery, the OpenAPI document and the calling key.

GET/api/v1

API index

Entry point with links to the documentation, the OpenAPI document and the main collections. No authentication.

No authentication
curl
curl "https://smart-pager-production.up.railway.app/api/v1"
Example response200 Links
json
{
  "name": "Smart Pager POS API",
  "version": "1.0.0",
  "documentation": "https://smart-pager-production.up.railway.app/docs/api",
  "openapi": "https://smart-pager-production.up.railway.app/api/v1/openapi.json",
  "authentication": "Authorization: Bearer pgr_<prefix>_<secret>",
  "links": {
    "me": "https://smart-pager-production.up.railway.app/api/v1/me",
    "branches": "https://smart-pager-production.up.railway.app/api/v1/branches",
    "events": "https://smart-pager-production.up.railway.app/api/v1/events"
  }
}
GET/api/v1/openapi.json

OpenAPI document

This API described as OpenAPI 3.1, including the webhooks. No authentication; CORS enabled.

No authentication
curl
curl "https://smart-pager-production.up.railway.app/api/v1/openapi.json"
Example response200 OpenAPI 3.1 document
json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Smart Pager POS API",
    "version": "1.0.0"
  }
}
GET/api/v1/me

Calling key

Returns the integration behind the API key, what it can access and its scopes. Use it to verify a new key.

Scope: Any valid key
curl
curl "https://smart-pager-production.up.railway.app/api/v1/me" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY"
Example response200 The calling integration
json
{
  "integration": {
    "id": "c4a1e7d2-8b3f-4a6e-9c5d-0e1f2a3b4c5d",
    "name": "Foodics bridge (7th Circle)"
  },
  "scope": {
    "type": "branch",
    "brand_id": "5d2e9f0a-7c1b-4e3d-8a6f-1b2c3d4e5f60",
    "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50"
  },
  "scopes": [
    "branches:read",
    "pagers:read",
    "orders:read",
    "orders:create",
    "orders:ready",
    "orders:ring",
    "orders:collect",
    "orders:cancel",
    "events:read"
  ],
  "rate_limit": {
    "limit": 600,
    "window_seconds": 60
  }
}

Branches

Branches this key can access.

GET/api/v1/branches

List branches

Branches this key can access: one for a branch key, every branch of the brand for a brand key.

Scope: branches:read
curl
curl "https://smart-pager-production.up.railway.app/api/v1/branches" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY"
Example response200 Accessible branches, ordered by code
json
{
  "data": [
    {
      "id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
      "code": "jo-7th-circle",
      "brand": {
        "slug": "99grill",
        "name_en": "99Grill",
        "name_ar": "99 جريل"
      },
      "name_en": "7th Circle",
      "name_ar": "الدوار السابع",
      "country_code": "JO",
      "timezone": "Asia/Amman",
      "enabled": true
    }
  ]
}
GET/api/v1/branches/{branch}

Get a branch

One branch by code or UUID.

Scope: branches:read

Parameters

Get a branch: Parameters
NameDescription
branch
path · string · required
Branch code (for example jo-7th-circle) or branch UUID.
curl
curl "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY"
Example response200 The branch
json
{
  "id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "code": "jo-7th-circle",
  "brand": {
    "slug": "99grill",
    "name_en": "99Grill",
    "name_ar": "99 جريل"
  },
  "name_en": "7th Circle",
  "name_ar": "الدوار السابع",
  "country_code": "JO",
  "timezone": "Asia/Amman",
  "enabled": true
}

Pagers

Printed QR pager cards and their live status.

GET/api/v1/branches/{branch}/pagers

List pager cards

Every pager card of the branch with its live status: free, preparing, ready or disabled. Use it to offer only free cards in your POS. QR tokens are never exposed.

Scope: pagers:read

Parameters

List pager cards: Parameters
NameDescription
branch
path · string · required
Branch code (for example jo-7th-circle) or branch UUID.
curl
curl "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle/pagers" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY"
Example response200 Pager cards ordered by number
json
{
  "branch": {
    "id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
    "code": "jo-7th-circle"
  },
  "data": [
    {
      "number": 1,
      "label": null,
      "enabled": true,
      "status": "free",
      "order_id": null
    },
    {
      "number": 7,
      "label": null,
      "enabled": true,
      "status": "preparing",
      "order_id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
    },
    {
      "number": 8,
      "label": "Terrace",
      "enabled": false,
      "status": "disabled",
      "order_id": null
    }
  ]
}

Orders

Create, read and list orders (one order = one invoice on one pager card).

POST/api/v1/branches/{branch}/orders

Link an invoice to a pager

Creates an order: links the invoice to the pager card the cashier hands to the customer. The customer who scans that card follows this order. Requires an Idempotency-Key. Attaching customer.phone requires the contacts:write scope and the customer's consent; the number is encrypted at rest and never returned. pickup_code is only present when the branch requires customers to enter a code; print it on the receipt.

Scope: orders:createIdempotency-Key required

Parameters

Link an invoice to a pager: Parameters
NameDescription
branch
path · string · required
Branch code (for example jo-7th-circle) or branch UUID.
Idempotency-Key
header · string · required
Unique key for this order creation (a UUID is ideal). Generate it once, store it with the POS order and reuse it on every retry. Only a successful creation is stored, so after an error (for example pager_busy) you may retry with the same key and a corrected body. Keys live 7 days per integration.

Request body

Link an invoice to a pager: Request body
NameDescription
pager_number
integer · required
Number printed on the pager card handed to the customer.
invoice_number
string · required
Letters, digits and - _ / . # (numbers are accepted too). Normalized before storage.
external_id
string | null · optional
Your own order reference. Must be unique among your active orders.
customer
CustomerContact | null · optional
customer.phone
string · required
International (+962…) or national format; national numbers use country or the branch's country.
customer.country
string · optional
ISO 3166-1 alpha-2 country for national numbers.
customer.consent
boolean · required
The customer agreed to be called (or texted) about this order when it is ready.
curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle/orders" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H "Idempotency-Key: 0b5e3a0c-6f1d-4d8e-9a77-2c1f3e4d5b6a" \
  -H "Content-Type: application/json" \
  -d '{"pager_number":7,"invoice_number":"A-1042","external_id":"pos-88231"}'
Example response201 Order created
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "preparing",
  "version": 1,
  "ring_count": 0,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": null,
  "acknowledged_at": null,
  "last_ring_at": null,
  "closed_at": null,
  "close_reason": null,
  "expires_at": "2026-10-18T00:04:11.512Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 0,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}
GET/api/v1/branches/{branch}/orders

List orders

Orders of a branch, newest first, with cursor pagination. status=active (default) returns orders on a pager right now; closed returns collected, cancelled and expired ones.

Scope: orders:read

Parameters

List orders: Parameters
NameDescription
branch
path · string · required
Branch code (for example jo-7th-circle) or branch UUID.
status
query · string · optional
active = preparing or ready; closed = collected, cancelled or expired; or a single status.
invoice_number
query · string · optional
Only orders of this invoice (normalized like the invoice field).
limit
query · integer · optional
cursor
query · string · optional
Opaque next_cursor of the previous page.
curl
curl "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle/orders?status=active&limit=20" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY"
Example response200 A page of orders
json
{
  "data": [
    {
      "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
      "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
      "branch_code": "jo-7th-circle",
      "pager_number": 7,
      "invoice_number": "A-1042",
      "business_date": "2026-10-17",
      "status": "preparing",
      "version": 1,
      "ring_count": 0,
      "external_id": "pos-88231",
      "source": "api",
      "created_at": "2026-10-17T18:04:11.512Z",
      "ready_at": null,
      "acknowledged_at": null,
      "last_ring_at": null,
      "closed_at": null,
      "close_reason": null,
      "expires_at": "2026-10-18T00:04:11.512Z",
      "has_phone": false,
      "customer": {
        "linked_devices": 0,
        "notifications_enabled_devices": 0
      },
      "links": {
        "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
      }
    }
  ],
  "next_cursor": null,
  "has_more": false
}
GET/api/v1/orders/{id}

Get an order

One order by id. The ETag header carries its version for If-Match.

Scope: orders:read

Parameters

Get an order: Parameters
NameDescription
id
path · string · required
Order id returned when the order was created.
curl
curl "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY"
Example response200 The order
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "ready",
  "version": 2,
  "ring_count": 1,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": "2026-10-17T18:11:42.020Z",
  "acknowledged_at": null,
  "last_ring_at": "2026-10-17T18:11:42.020Z",
  "closed_at": null,
  "close_reason": null,
  "expires_at": "2026-10-17T20:11:42.020Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 1,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}

Order commands

Lifecycle commands addressed by order id.

POST/api/v1/orders/{id}/ready

Mark ready

Marks the order ready and alerts every device following it (the open order page rings). When the order has a phone number (customer.phone) and the branch uses phone alerts, the customer also gets an automated voice call right away (or one SMS on SMS branches), even with the page closed; no tap is needed. Idempotent: calling it on an order that is already ready returns 200 with the unchanged order and never alerts again. Use ring for that.

Scope: orders:readyIdempotency-Key supportedIf-Match supported

Parameters

Mark ready: Parameters
NameDescription
id
path · string · required
Order id returned when the order was created.
Idempotency-Key
header · string · optional
Unique key for this operation (a UUID is ideal). Retrying with the same key and the same request replays the first successful response instead of acting twice. Failed requests are not stored. Keys live 7 days per integration.
If-Match
header · string · optional
Apply the command only if the order version still equals this entity tag (the ETag you received, e.g. "3"). Otherwise 412 version_conflict.
curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10/ready" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H "Idempotency-Key: 3c9e1f7a-52d4-4b8e-a1f0-6d2b7c8e9a41"
Example response200 The order after the command
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "ready",
  "version": 2,
  "ring_count": 1,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": "2026-10-17T18:11:42.020Z",
  "acknowledged_at": null,
  "last_ring_at": "2026-10-17T18:11:42.020Z",
  "closed_at": null,
  "close_reason": null,
  "expires_at": "2026-10-17T20:11:42.020Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 1,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}
POST/api/v1/orders/{id}/ring

Ring again

Alerts the customer again while the order is ready. Rings must be at least 30 s apart (429 ring_too_soon with retry_after) and an order alerts at most 6 times in total, automatic reminders included (409 ring_limit). On branches that call, each ring also places one more voice call to the order's phone number (not while the previous call may still be ringing or talking), at most 3 calls per order including the automatic one.

Scope: orders:ringIdempotency-Key supportedIf-Match supported

Parameters

Ring again: Parameters
NameDescription
id
path · string · required
Order id returned when the order was created.
Idempotency-Key
header · string · optional
Unique key for this operation (a UUID is ideal). Retrying with the same key and the same request replays the first successful response instead of acting twice. Failed requests are not stored. Keys live 7 days per integration.
If-Match
header · string · optional
Apply the command only if the order version still equals this entity tag (the ETag you received, e.g. "3"). Otherwise 412 version_conflict.
curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10/ring" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H "Idempotency-Key: a8d4c2e6-1b3f-4f7a-9c5e-0d2e4f6a8b13"
Example response200 The order after the command
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "ready",
  "version": 2,
  "ring_count": 1,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": "2026-10-17T18:11:42.020Z",
  "acknowledged_at": null,
  "last_ring_at": "2026-10-17T18:11:42.020Z",
  "closed_at": null,
  "close_reason": null,
  "expires_at": "2026-10-17T20:11:42.020Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 1,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}
POST/api/v1/orders/{id}/collect

Collect

Closes a ready order as handed over and frees the pager card; pending alerts stop (a call that may still be ringing is hung up), so the next customer of this card is never confused with this one. Send {"direct": true} to close an order that is still preparing (handed over at the counter without paging). Collecting an already collected order returns 200.

Scope: orders:collectIdempotency-Key supportedIf-Match supported

Parameters

Collect: Parameters
NameDescription
id
path · string · required
Order id returned when the order was created.
Idempotency-Key
header · string · optional
Unique key for this operation (a UUID is ideal). Retrying with the same key and the same request replays the first successful response instead of acting twice. Failed requests are not stored. Keys live 7 days per integration.
If-Match
header · string · optional
Apply the command only if the order version still equals this entity tag (the ETag you received, e.g. "3"). Otherwise 412 version_conflict.

Request body (optional)

Collect: Request body
NameDescription
direct
boolean · optional
Allow collecting an order that is still preparing (handed over directly without paging).
curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10/collect" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H "Idempotency-Key: 5f7b9d1c-3e5a-4c2b-8d6f-1a3c5e7f9b25" \
  -H "Content-Type: application/json" \
  -d '{"direct":false}'
Example response200 The order after the command
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "collected",
  "version": 3,
  "ring_count": 1,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": "2026-10-17T18:11:42.020Z",
  "acknowledged_at": null,
  "last_ring_at": "2026-10-17T18:11:42.020Z",
  "closed_at": "2026-10-17T18:13:05.877Z",
  "close_reason": "collected",
  "expires_at": "2026-10-17T20:11:42.020Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 1,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}
POST/api/v1/orders/{id}/cancel

Cancel

Closes an active order without handover (voided invoice, customer left). Frees the pager card, stops pending alerts and hangs up a call to the customer that may still be ringing. Cancelling an already cancelled order returns 200.

Scope: orders:cancelIdempotency-Key supportedIf-Match supported

Parameters

Cancel: Parameters
NameDescription
id
path · string · required
Order id returned when the order was created.
Idempotency-Key
header · string · optional
Unique key for this operation (a UUID is ideal). Retrying with the same key and the same request replays the first successful response instead of acting twice. Failed requests are not stored. Keys live 7 days per integration.
If-Match
header · string · optional
Apply the command only if the order version still equals this entity tag (the ETag you received, e.g. "3"). Otherwise 412 version_conflict.

Request body (optional)

Cancel: Request body
NameDescription
reason
string · optional
Short machine-friendly reason, e.g. invoice_voided.
curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10/cancel" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H "Idempotency-Key: e2a4c6e8-7f1b-4d3a-b5c7-9e1f3a5c7d37" \
  -H "Content-Type: application/json" \
  -d '{"reason":"customer_left"}'
Example response200 The order after the command
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "cancelled",
  "version": 2,
  "ring_count": 0,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": null,
  "acknowledged_at": null,
  "last_ring_at": null,
  "closed_at": "2026-10-17T18:06:40.004Z",
  "close_reason": "customer_left",
  "expires_at": "2026-10-18T00:04:11.512Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 1,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}

Invoice commands

The same commands addressed by branch + invoice number, for POS systems that only know the invoice.

POST/api/v1/branches/{branch}/invoices/{invoice}/ready

Mark ready by invoice

Marks the order ready and alerts every device following it (the open order page rings). When the order has a phone number (customer.phone) and the branch uses phone alerts, the customer also gets an automated voice call right away (or one SMS on SMS branches), even with the page closed; no tap is needed. Idempotent: calling it on an order that is already ready returns 200 with the unchanged order and never alerts again. Use ring for that. The order is the active order of the invoice in this branch; when there is none you get 404 no_active_order with the latest order's status in details.last_status.

Scope: orders:readyIdempotency-Key supportedIf-Match supported

Parameters

Mark ready by invoice: Parameters
NameDescription
branch
path · string · required
Branch code (for example jo-7th-circle) or branch UUID.
invoice
path · string · required
Invoice number as printed by the POS, URL-encoded (/ → %2F, # → %23). Matching ignores case and spaces and accepts Arabic-Indic digits.
Idempotency-Key
header · string · optional
Unique key for this operation (a UUID is ideal). Retrying with the same key and the same request replays the first successful response instead of acting twice. Failed requests are not stored. Keys live 7 days per integration.
If-Match
header · string · optional
Apply the command only if the order version still equals this entity tag (the ETag you received, e.g. "3"). Otherwise 412 version_conflict.
curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle/invoices/A-1042/ready" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H "Idempotency-Key: 3c9e1f7a-52d4-4b8e-a1f0-6d2b7c8e9a41"
Example response200 The order after the command
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "ready",
  "version": 2,
  "ring_count": 1,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": "2026-10-17T18:11:42.020Z",
  "acknowledged_at": null,
  "last_ring_at": "2026-10-17T18:11:42.020Z",
  "closed_at": null,
  "close_reason": null,
  "expires_at": "2026-10-17T20:11:42.020Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 1,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}
POST/api/v1/branches/{branch}/invoices/{invoice}/ring

Ring again by invoice

Alerts the customer again while the order is ready. Rings must be at least 30 s apart (429 ring_too_soon with retry_after) and an order alerts at most 6 times in total, automatic reminders included (409 ring_limit). On branches that call, each ring also places one more voice call to the order's phone number (not while the previous call may still be ringing or talking), at most 3 calls per order including the automatic one. The order is the active order of the invoice in this branch; when there is none you get 404 no_active_order with the latest order's status in details.last_status.

Scope: orders:ringIdempotency-Key supportedIf-Match supported

Parameters

Ring again by invoice: Parameters
NameDescription
branch
path · string · required
Branch code (for example jo-7th-circle) or branch UUID.
invoice
path · string · required
Invoice number as printed by the POS, URL-encoded (/ → %2F, # → %23). Matching ignores case and spaces and accepts Arabic-Indic digits.
Idempotency-Key
header · string · optional
Unique key for this operation (a UUID is ideal). Retrying with the same key and the same request replays the first successful response instead of acting twice. Failed requests are not stored. Keys live 7 days per integration.
If-Match
header · string · optional
Apply the command only if the order version still equals this entity tag (the ETag you received, e.g. "3"). Otherwise 412 version_conflict.
curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle/invoices/A-1042/ring" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H "Idempotency-Key: a8d4c2e6-1b3f-4f7a-9c5e-0d2e4f6a8b13"
Example response200 The order after the command
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "ready",
  "version": 2,
  "ring_count": 1,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": "2026-10-17T18:11:42.020Z",
  "acknowledged_at": null,
  "last_ring_at": "2026-10-17T18:11:42.020Z",
  "closed_at": null,
  "close_reason": null,
  "expires_at": "2026-10-17T20:11:42.020Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 1,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}
POST/api/v1/branches/{branch}/invoices/{invoice}/collect

Collect by invoice

Closes a ready order as handed over and frees the pager card; pending alerts stop (a call that may still be ringing is hung up), so the next customer of this card is never confused with this one. Send {"direct": true} to close an order that is still preparing (handed over at the counter without paging). Collecting an already collected order returns 200. The order is the active order of the invoice in this branch; when there is none you get 404 no_active_order with the latest order's status in details.last_status.

Scope: orders:collectIdempotency-Key supportedIf-Match supported

Parameters

Collect by invoice: Parameters
NameDescription
branch
path · string · required
Branch code (for example jo-7th-circle) or branch UUID.
invoice
path · string · required
Invoice number as printed by the POS, URL-encoded (/ → %2F, # → %23). Matching ignores case and spaces and accepts Arabic-Indic digits.
Idempotency-Key
header · string · optional
Unique key for this operation (a UUID is ideal). Retrying with the same key and the same request replays the first successful response instead of acting twice. Failed requests are not stored. Keys live 7 days per integration.
If-Match
header · string · optional
Apply the command only if the order version still equals this entity tag (the ETag you received, e.g. "3"). Otherwise 412 version_conflict.

Request body (optional)

Collect by invoice: Request body
NameDescription
direct
boolean · optional
Allow collecting an order that is still preparing (handed over directly without paging).
curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle/invoices/A-1042/collect" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H "Idempotency-Key: 5f7b9d1c-3e5a-4c2b-8d6f-1a3c5e7f9b25" \
  -H "Content-Type: application/json" \
  -d '{"direct":false}'
Example response200 The order after the command
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "collected",
  "version": 3,
  "ring_count": 1,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": "2026-10-17T18:11:42.020Z",
  "acknowledged_at": null,
  "last_ring_at": "2026-10-17T18:11:42.020Z",
  "closed_at": "2026-10-17T18:13:05.877Z",
  "close_reason": "collected",
  "expires_at": "2026-10-17T20:11:42.020Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 1,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}
POST/api/v1/branches/{branch}/invoices/{invoice}/cancel

Cancel by invoice

Closes an active order without handover (voided invoice, customer left). Frees the pager card, stops pending alerts and hangs up a call to the customer that may still be ringing. Cancelling an already cancelled order returns 200. The order is the active order of the invoice in this branch; when there is none you get 404 no_active_order with the latest order's status in details.last_status.

Scope: orders:cancelIdempotency-Key supportedIf-Match supported

Parameters

Cancel by invoice: Parameters
NameDescription
branch
path · string · required
Branch code (for example jo-7th-circle) or branch UUID.
invoice
path · string · required
Invoice number as printed by the POS, URL-encoded (/ → %2F, # → %23). Matching ignores case and spaces and accepts Arabic-Indic digits.
Idempotency-Key
header · string · optional
Unique key for this operation (a UUID is ideal). Retrying with the same key and the same request replays the first successful response instead of acting twice. Failed requests are not stored. Keys live 7 days per integration.
If-Match
header · string · optional
Apply the command only if the order version still equals this entity tag (the ETag you received, e.g. "3"). Otherwise 412 version_conflict.

Request body (optional)

Cancel by invoice: Request body
NameDescription
reason
string · optional
Short machine-friendly reason, e.g. invoice_voided.
curl
curl -X POST "https://smart-pager-production.up.railway.app/api/v1/branches/jo-7th-circle/invoices/A-1042/cancel" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY" \
  -H "Idempotency-Key: e2a4c6e8-7f1b-4d3a-b5c7-9e1f3a5c7d37" \
  -H "Content-Type: application/json" \
  -d '{"reason":"customer_left"}'
Example response200 The order after the command
json
{
  "id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "branch_code": "jo-7th-circle",
  "pager_number": 7,
  "invoice_number": "A-1042",
  "business_date": "2026-10-17",
  "status": "cancelled",
  "version": 2,
  "ring_count": 0,
  "external_id": "pos-88231",
  "source": "api",
  "created_at": "2026-10-17T18:04:11.512Z",
  "ready_at": null,
  "acknowledged_at": null,
  "last_ring_at": null,
  "closed_at": "2026-10-17T18:06:40.004Z",
  "close_reason": "customer_left",
  "expires_at": "2026-10-18T00:04:11.512Z",
  "has_phone": false,
  "customer": {
    "linked_devices": 1,
    "notifications_enabled_devices": 0
  },
  "links": {
    "self": "https://smart-pager-production.up.railway.app/api/v1/orders/6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10"
  }
}

Events

Ordered, replayable feed of order events (webhook reconciliation).

GET/api/v1/events

Order event feed

Every order event of the branches this key can access, oldest first, by ascending id. Event ids are the same evt_ ids your webhooks carry, so you can dedupe across both. Poll with after=<next_cursor> to catch anything a webhook missed. A page never contains an event while an event with a lower id is still being saved, so following next_cursor never skips one; while such a save is unusually slow the page can come back empty with next_cursor unchanged. Ignore event types you do not know.

Scope: events:read

Parameters

Order event feed: Parameters
NameDescription
after
query · string · optional
Return events after this event id (evt_1289 or 1289). Omit to start from the oldest retained event.
limit
query · integer · optional
branch
query · string · optional
Only events of this branch (code or UUID). Brand keys see every branch of the brand by default.
curl
curl "https://smart-pager-production.up.railway.app/api/v1/events?after=evt_1287&limit=100" \
  -H "Authorization: Bearer $SMARTPAGER_API_KEY"
Example response200 A page of events
json
{
  "data": [
    {
      "id": "evt_1288",
      "type": "order.created",
      "occurred_at": "2026-10-17T18:04:11.512Z",
      "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
      "order_id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
      "data": {
        "status": "preparing",
        "pager_number": 7,
        "invoice_number": "A-1042",
        "external_id": "pos-88231",
        "source": "api"
      }
    },
    {
      "id": "evt_1289",
      "type": "order.ready",
      "occurred_at": "2026-10-17T18:11:42.020Z",
      "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
      "order_id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
      "data": {
        "status": "ready",
        "pager_number": 7,
        "invoice_number": "A-1042",
        "external_id": "pos-88231"
      }
    }
  ],
  "next_cursor": "evt_1289",
  "has_more": false
}

Webhooks

Register an HTTPS endpoint for your integration in the admin console and choose its events. Smart Pager then POSTs a JSON envelope for every matching order event in your branches. The signing secret is shown once when the endpoint is created.

json · POST to your endpoint
{
  "id": "evt_1289",
  "type": "order.ready",
  "schema_version": 1,
  "occurred_at": "2026-10-17T18:11:42.020Z",
  "branch_id": "0b8f3c1e-2a44-4d6b-9e1f-5c7a2d3e4f50",
  "data": {
    "order_id": "6f1c2a7e-9d1b-4c55-8a3e-2b7f0e4d9a10",
    "external_id": "pos-88231",
    "status": "ready",
    "version": 2,
    "pager_number": 7,
    "invoice_number": "A-1042",
    "business_date": "2026-10-17"
  }
}
Webhook headers
HeaderValue
X-SmartPager-Event-IdThe event id (same as the body's id). Deduplicate on it.
X-SmartPager-TimestampUnix seconds when this attempt was signed.
X-SmartPager-Signaturev1= + lowercase hex HMAC-SHA256 of <timestamp>.<raw body> keyed with your signing secret.

Verify every delivery

  1. Read the raw body bytes before any JSON parsing (re-serialized JSON will not match).
  2. Reject the delivery when the timestamp is more than 5 minutes from your clock (replay protection).
  3. Compute HMAC-SHA256(secret, timestamp + "." + raw_body) as lowercase hex and compare it in constant time with each v1= value of the signature header. The key is the whole secret exactly as shown, whsec_ prefix included, as UTF-8 text: do not strip or base64-decode it.
  4. Skip event ids you already processed, answer 2xx quickly, then do the work.
javascript
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.SMARTPAGER_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;

function verify(rawBody, timestamp, signatureHeader) {
  if (!/^\d+$/.test(timestamp ?? "")) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(timestamp + ".")
    .update(rawBody) // the exact bytes received
    .digest("hex");
  const want = Buffer.from(expected);
  return (signatureHeader ?? "").split(",").some((part) => {
    const [version, hex = ""] = part.trim().split("=");
    const got = Buffer.from(hex);
    return version === "v1" && got.length === want.length && crypto.timingSafeEqual(got, want);
  });
}

// express.raw keeps the body as a Buffer: never verify re-serialized JSON.
app.post("/webhooks/smartpager", express.raw({ type: "application/json" }), async (req, res) => {
  const ok = verify(
    req.body,
    req.get("X-SmartPager-Timestamp"),
    req.get("X-SmartPager-Signature"),
  );
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  if (await alreadyProcessed(event.id)) return res.sendStatus(200); // dedupe by event id

  res.sendStatus(200); // acknowledge fast, then do the work
  await handleEvent(event); // e.g. event.type === "order.ready"
});

Delivery semantics

  • Answer within 10 seconds. Anything but a 2xx answer (including redirects, which are not followed) or no answer in time is retried with exponential backoff for up to 24 hours.
  • Deliveries can arrive twice or out of order. Use data.version: ignore an event whose version is lower than what you stored. Versions can skip numbers (automatic reminders are not sent as webhooks). Events without data.version (order.customer_linked, and order.notifications_enabled, which is no longer sent) never change the order's status.
  • Missed something? GET /events replays the same events with the same evt_ ids.

Event types

Webhook event types
TypeWhendata
order.createdAn invoice was linked to a pager card, from this API or from the cashier screen.order_id, external_id, status, version, pager_number, invoice_number, business_date
order.readyThe order was marked ready. Every device following it is being alerted.order_id, external_id, status, version, pager_number, invoice_number, business_date
order.rangStaff or a POS rang the customer again. data.ring is the alert round number (1 = the ready alert).order_id, external_id, status, version, pager_number, invoice_number, business_date, ring
order.acknowledgedThe customer tapped OK on the ready alarm (optional; most customers never do). Automatic reminders stop. Its absence does not mean the customer missed the alert.order_id, external_id, status, version, pager_number, invoice_number, business_date
order.collectedThe order was handed over. The pager card is free again.order_id, external_id, status, version, pager_number, invoice_number, business_date
order.cancelledThe order was cancelled. The pager card is free and pending alerts were stopped.order_id, external_id, status, version, pager_number, invoice_number, business_date
order.expiredThe order was closed automatically: 6 h after creation while preparing, or 2 h after it became ready.order_id, external_id, status, version, pager_number, invoice_number, business_date
order.customer_linkedA customer device scanned the pager card (or entered the pickup code) and now follows the order.order_id, external_id, via
order.notifications_enabledNo longer sent: push notifications were removed (the open order page rings instead), so this event never fires. The type is still accepted in webhook subscriptions so existing endpoints keep validating; you can stop subscribing to it.order_id, external_id
pingSent when an admin presses “Send test” for your endpoint in the admin console. Its id starts with evt_test_, it has no branch_id, it never appears in GET /events and it is retried for 1 hour only. Answer with any 2xx.message

The event feed also contains order.reminded (an automatic reminder alerted the customer again (data.round, data.ring). Increments the order version) and order.phone_added (a phone number was attached to the order. The number itself is never exposed) and order.phone_alerted (twilio accepted the phone alert for the order: data.channel = call (placed the moment the order became ready, or by a ring) or sms (SMS branches, or the one SMS backup after an unanswered call). The number is never exposed) and order.phone_alert_failed (twilio rejected the phone alert (data.channel, data.code such as twilio_21211 for an invalid number), or gave no clear answer (data.code ambiguous_timeout / ambiguous_network / ambiguous_http_5xx: the alert may still have gone out, and it is never sent twice)) and order.phone_call_ended (a call to the customer ended (data.call_status = completed | busy | no-answer | failed | canceled; data.duration_s = seconds Twilio reported, 0 when nobody answered; only call_status = completed means answered). data.sms_backup is true when this queued the order's one SMS backup (busy / no-answer / failed / canceled while the order is still ready, on branches that enable it). It does not change the order's status) and order.followers_reset (staff unlinked every phone following the order (and rotated the pickup code when the branch uses one)), which are not sent as webhooks. A feed page never contains an event while one with a lower id is still being saved, so following next_cursor never skips an event.

Error codes

Error responses always have the problem shape shown in Requests & responses. Requests answered 405 (wrong method on a known path) have no body.

Error codes
CodeMeaning and what to do
invalid_json
HTTP 400 · not retryable
The body could not be parsed as JSON.
idempotency_key_required
HTTP 400 · not retryable
Creating an order requires an Idempotency-Key header.
invalid_idempotency_key
HTTP 400 · not retryable
The Idempotency-Key header is empty, too long or contains spaces or non-ASCII characters.
invalid_precondition
HTTP 400 · not retryable
If-Match is not a single entity tag. Send the ETag you received, e.g. "3".
invalid_cursor
HTTP 400 · not retryable
The pagination cursor was altered or truncated. Pass next_cursor back exactly as received.
unauthorized
HTTP 401 · not retryable
The API key is missing, malformed, unknown or revoked, or its integration is disabled.
forbidden
HTTP 403 · not retryable
The key does not have the scope this endpoint needs (sending customer also needs contacts:write), or the branch is disabled.
not_found
HTTP 404 · not retryable
The branch, pager, order or endpoint does not exist, or your key cannot access it. Inaccessible resources always look like missing ones.
no_active_order
HTTP 404 · not retryable
Invoice commands only: the invoice has no active order in this branch. details.last_status tells you what happened to its latest order (for example collected). If it is the state you wanted, treat it as success.
pager_busy
HTTP 409 · not retryable
The pager card already has an active order. Collect or cancel that order first, or use another card.
pager_disabled
HTTP 409 · not retryable
The pager card was disabled by an admin (lost or damaged). Use another card.
invoice_active
HTTP 409 · not retryable
The invoice (or your external_id) already has an active order in this branch. details.pager_number shows which card holds it.
invalid_transition
HTTP 409 · not retryable
The command does not apply to the order's current status, for example ringing an order that is still preparing or marking a collected order ready.
ring_limit
HTTP 409 · not retryable
An order can alert the customer at most 6 times, automatic reminders included.
idempotency_conflict
HTTP 409 · not retryable
The Idempotency-Key already belongs to a different request (method, path or body) that succeeded or is still running. Use a new key for a new operation.
idempotency_in_progress
HTTP 409 · retryable
The first request with this key has not finished yet. Retry after Retry-After seconds with the same key.
version_conflict
HTTP 412 · not retryable
If-Match did not match the order's current version. details.current_version holds the latest version; re-read the order and decide again.
payload_too_large
HTTP 413 · not retryable
Request bodies are limited to 16 KB.
validation_failed
HTTP 422 · not retryable
A field or query parameter is missing or invalid. details.issues lists each problem with its path.
ring_too_soon
HTTP 429 · retryable
Rings must be at least 30 seconds apart. Retry after details.retry_after seconds (also in Retry-After).
rate_limited
HTTP 429 · retryable
More than 600 requests in one minute for this integration. Retry after Retry-After seconds.
internal_error
HTTP 500 · retryable
Something failed on our side. Retry with backoff and the same Idempotency-Key; read the order first if you are unsure whether the command applied. Quote request_id when reporting it.

OpenAPI

The complete contract, including every webhook, is published as OpenAPI 3.1. Import it into Postman, Insomnia or Swagger Editor, or generate a client from it.

OpenAPI 3.1 JSON

url
https://smart-pager-production.up.railway.app/api/v1/openapi.json

Back to top