DEVELOPERS · API v1
Build an app for DigiMenu
Apply for API access and explain your use case. After DigiMenu approval, connect with the restaurant’s permission. This guide covers the application process, setup and technical details.
How apps work
API access requires an application and DigiMenu approval for each app. Explain its purpose, why it needs the API and which data it will use. Installation, tokens and webhooks are blocked before approval, including TEST mode. A subscription or developer portal access does not replace approval.
Your app runs on your own server. A restaurant administrator grants access in DigiMenu; your app calls the API with that installation’s OAuth token and receives signed webhooks. You do not upload code or ZIP files to DigiMenu servers.
API base: https://api.digimenu.us/api/v1. TEST and LIVE apps use the same host. TEST only restricts installation to the developer organization; it is not a separate or simulated database. Use a dedicated test restaurant.
- Read menus and tables, list orders and build delivery integrations.
- This API has no menu/product writes, order creation, inventory/CRM management, payment collection or refund endpoint. First-party DigiMenu apps have capabilities that are not exposed to third parties.
- The starter runs on Node.js 22.18+ without extra packages: OAuth connection, menu reads, token refresh and signed webhook receipt.
Independent developer account
Apply at developers.digimenu.us without a restaurant account or subscription. Verify your email and describe yourself or your company, the app purpose and required data. After developer approval you can create an app application. Each app is reviewed separately; account approval alone does not grant API access.
Create an isolated test environment from an approved app’s settings. It never uses customer tables and supports sample menu, table and order reads plus order delivery-status changes. It does not simulate OAuth, courier or webhook flows. The sandbox_ key lasts 24 hours and cannot be used at the live /api/v1 address. Reset restores fixtures and revokes the old key.
GET https://api.digimenu.us/api/admin/developer-sandbox/v1/menu
Authorization: Bearer SANDBOX_TOKENFor production, switch your approved app to LIVE. A restaurant administrator must separately grant access through the OAuth flow below. An independent developer account does not provide restaurant dashboard access. Suspending the developer account blocks its existing apps and sandbox keys too.
App registration and testing
Open developers.digimenu.us with your approved independent account. Business-account developers can continue using panel.digimenu.us/gelistirici, which requires Developer & Platform enabled in their organization. The app settings below apply to both portals.
- Create an API access application draft. Explain the purpose and data needs in 40–2000 characters. The resulting clientId does not grant access by itself.
- Register an exact HTTPS redirect URL, e.g. https://app.example/callback. Wildcards, credentials and fragments are rejected. Trailing slashes must match.
- For the starter choose menu:read, orders:read and webhooks:manage. Check the granted scopes in each token response.
- Save the purpose, redirect URLs and scopes, then submit for review. After DigiMenu approval you may create an optional client_secret. Once configured it is required for code exchange and refresh, and is displayed only once.
- After your application is approved, authorize a test restaurant in your own organization in TEST mode. Use your own HTTPS domain or tunnel for callback and webhook URLs; HTTP localhost redirects cannot be registered.
APP_ORIGIN=https://eklenti.example
CLIENT_ID=YOUR_CLIENT_ID
SCOPES="menu:read orders:read webhooks:manage"
# Only if a secret was created:
# CLIENT_SECRET=YOUR_CLIENT_SECRET
# Set after registering the webhook receiver:
# WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET
PORT=8787
INBOX_DIR=./webhook-inboxnode --env-file=starter.env digimenu-starter.mjs
# HTTPS proxy/tunnel → http://127.0.0.1:8787
# Open https://eklenti.example/connectThe starter is for development: sessions and tokens live in memory and require reauthorization after restart; webhook events are written to local disk. Before production add user authentication, encrypted persistent token storage, organization/installation mapping, a durable work queue and a retention policy. Do not commit starter.env.
OAuth and PKCE
PKCE S256 is mandatory for all clients. Generate a new code_verifier and state for each connection attempt and bind them to the user’s server session. The verifier must be 43–128 characters. Validate state before exchanging the code. Authorization codes are single-use and valid for 60 seconds.
import { randomBytes, createHash } from 'node:crypto';
const verifier = randomBytes(32).toString('base64url');
const state = randomBytes(32).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');
// Store verifier + state in this user's server session before redirecting.
const url = new URL('https://panel.digimenu.us/yetkilendir');
url.search = new URLSearchParams({
client_id: process.env.CLIENT_ID,
redirect_uri: 'https://eklenti.example/callback',
scope: 'menu:read orders:read webhooks:manage',
code_challenge: challenge,
code_challenge_method: 'S256',
state,
}).toString();After consent the callback receives code and state; denial returns error=access_denied and state. Do not collect a dashboard login token. Obtain app tokens through the exchange below. The client_credentials grant is not supported.
POST https://api.digimenu.us/api/v1/oauth/token
Content-Type: application/json
{
"grant_type": "authorization_code",
"client_id": "YOUR_CLIENT_ID",
"code": "CODE_FROM_CALLBACK",
"redirect_uri": "https://eklenti.example/callback",
"code_verifier": "ORIGINAL_VERIFIER"
}{
"access_token": "ACCESS_TOKEN",
"refresh_token": "REFRESH_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "menu:read orders:read webhooks:manage"
}Refresh and uninstall
Access tokens last 1 hour; refresh tokens last 60 days. Use expires_in. Each refresh returns a new access/refresh pair; store both atomically. Serialize refreshes per installation. Reusing an old refresh token revokes the installation’s refresh-token family.
POST https://api.digimenu.us/api/v1/oauth/token
Content-Type: application/json
{
"grant_type": "refresh_token",
"client_id": "YOUR_CLIENT_ID",
"refresh_token": "LATEST_REFRESH_TOKEN"
}Include client_secret if configured. If a refresh response is lost, do not blindly retry the old token; reconnect through OAuth. Do not loop on invalid_grant.
Uninstalling an app in the dashboard closes API access. Stop syncing on APP_REVOKED or APP_NOT_INSTALLED and ask the restaurant to authorize again. SUBSCRIPTION_REQUIRED must be resolved through the restaurant’s billing panel, not token refresh.
Scopes and data access
Each access token belongs to one restaurant and installation. Store separate grants/tokens for each restaurant, keyed by installationId and restaurantId. Scopes do not imply one another: orders:write does not include orders:read.
- menu:read — Active categories and products, without menu writes.
- orders:read — Order list/detail; excludes line prices, payment records and the customer table.
- orders:write — Delivery-leg status transitions only, without payment, kitchen or cancellation operations.
- tables:read — Active table ID, name and number; no QR secrets or occupancy.
- deliveries:read / deliveries:write — Read/operate assigned courier runs. Granting write provisions a courier for the installation.
- customers:read — Does not expose a /customers endpoint. Detailed order addresses require this scope AND assignment to your app’s courier.
- webhooks:manage — Manage app receivers and delivery logs. Receiving events also requires orders:read or deliveries:read.
Requests, pagination and retries
curl 'https://api.digimenu.us/api/v1/orders?channel=DELIVERY&limit=25' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Accept: application/json'Order and webhook-delivery lists default to limit=25, range 1–100. Pass nextCursor as cursor for the next request; stop at null. /tables and /webhooks return bare arrays. /menu returns restaurant/categories, not a data envelope.
Dates use ISO 8601. Menu price is in major units (149.90); deliveryFeeMinor is in minor units (14990). These responses do not include currency; establish the restaurant’s currency separately. null can indicate unavailable or inapplicable data.
Idempotency-Key applies only to /orders/{id}/transitions: a per-installation, five-minute, in-process response cache. Use a different key for every order/operation/body. It is not durable across restarts and is not a guarantee for all POSTs. Reconcile state before retrying other writes after ambiguous network failures.
Webhook setup and verification
Receivers belong to the app, not a restaurant; do not register a new receiver for every installation. Use a public HTTPS URL on port 443. Private/local hosts and redirects are rejected. Verify the signature over the raw request bytes before parsing JSON.
POST https://api.digimenu.us/api/v1/webhooks
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"url": "https://eklenti.example/webhooks",
"events": ["order.created", "order.paid"]
}The 201 response exposes secret only once. Store it securely. Receiver creation is not idempotent: after a lost response check GET /webhooks before retrying. If the secret is lost, disable the old receiver and create a new one.
X-Digimenu-Signature: t=1790467200,v1=<64-character-hex-hmac>
X-Digimenu-Event-Id: <event-uuid>
X-Digimenu-Event-Type: order.created
HMAC-SHA256(secret, timestamp + "." + rawBody){
"id": "00000000-0000-4000-8000-000000000001",
"type": "order.created",
"createdAt": "2026-09-27T00:00:00.000Z",
"restaurantId": "restaurant_example",
"installationId": "installation_example",
"data": { "orderId": "order_example", "channel": "TAKEAWAY" }
}Compare signatures in constant time and reject timestamps outside ±300 seconds. The signed body id must match X-Digimenu-Event-Id. Persist deduplication by that ID and match restaurantId/installationId against your own installation records.
Return 2xx within 10 seconds, after storing the event durably. Failed delivery has up to 6 attempts, with delays of 30, 120, 600, 1,800 and 7,200 seconds before retries; worker timing may delay them. A total of 20 FAILED delivery records auto-disables the receiver.
Ordering and exactly-once delivery are not guaranteed. Fetch current order state using GET /orders/{id}. Inspect GET /webhooks/{id}/deliveries for attempts. There is no public replay or re-enable endpoint.
Event catalog
- order.created — Takeaway/delivery order created. data: orderId, channel. Requires orders:read; not guaranteed for every table-order path.
- order.paid — Channel-order payment settled. data: orderId. Requires orders:read.
- delivery.ready — Assignment to an external courier network. data: orderId, runId and delivery details. Requires deliveries:read. Verify your assigned run through the API; avoid logging sensitive delivery data.
- delivery.picked_up / delivery.delivered / delivery.failed — data: orderId, runId, status. Requires deliveries:read.
- delivery.assigned — A reserved name accepted during registration; no producer exists in the current release. Do not make your workflow depend on it.
Courier integration
Install with deliveries:read, deliveries:write and webhooks:manage; add orders:read for order detail and customers:read for assigned-order contact details. After the restaurant assigns a delivery, read the runId from delivery.ready.
GET /api/v1/deliveries/{runId}
POST /api/v1/deliveries/{runId}/depart
POST /api/v1/deliveries/{runId}/pings
{ "lat": 41.01, "lng": 28.97 }
POST /api/v1/deliveries/orders/{orderId}/delivered
{ "code": "CUSTOMER_DELIVERY_CODE" }
# Alternative when delivery fails:
POST /api/v1/deliveries/orders/{orderId}/failed
{ "reason": "CUSTOMER_ABSENT" }Delivery confirmation checks fresh location, proximity and the restaurant’s proof mode. CODE requires the customer’s code. PHOTO/SIGNATURE require a DigiMenu-issued upload, but public v1 has no proof-upload endpoint. These modes cannot be treated as complete v1-only workflows.
Courier apps should use /deliveries. Generic /orders/{id}/transitions does not perform GPS or delivery-proof checks. The pings route resolves the active run from the authenticated courier; runId cannot select another run.
Embedded screens
The developer portal exposes embeddedUrl and embeddedOrigin. The screen runs in a sandboxed iframe: do not rely on cookies, session access or the parent DOM. Tokens are never sent through iframe messages. Start with a standalone HTTPS app.
The embedded bridge is not yet a stable integration contract: the current iframe sandbox and origin-targeted messaging require end-to-end validation together. Do not assume restaurantId/locale/theme or auto-resize messages will arrive. Validate this flow with support before production.
Errors and limits
Limits are 30 requests per 10 seconds and 5,000 per UTC hour window per installation, plus a global 600/minute per-IP limit. The token endpoint is limited to 30/minute per IP. Back off with jitter after 429; do not assume Retry-After is always present.
{ "code": "insufficient_scope", "requiredScope": "orders:read", "message": "..." }{ "error": "invalid_grant", "error_description": "..." }- 400 — Check types, allowed statuses and required fields. Unknown body fields are rejected. Validation errors may also use statusCode/message/error.
- 401 — Refresh an expired token on APP_UNAUTHENTICATED. APP_REVOKED / APP_NOT_INSTALLED require reauthorization. For OAuth invalid_client check client_id and configured client_secret.
- 403 — insufficient_scope, SUBSCRIPTION_REQUIRED or courier/proof restriction. Repeated requests cannot fix missing permission.
- 404 — Resource missing or outside the token’s restaurant/courier access.
- 409 — INVALID_TRANSITION: read the current order state before choosing the next transition.
- 429 — rate_limited / quota_exceeded or global throttling. Slow the queue; hourly quota requires the next UTC hour window.
- 5xx or network failure — Retry reads a bounded number of times. Reconcile writes before retrying.
Review and release
Apps start as DRAFT and TEST. Redirect URLs, scopes and descriptions are editable while DRAFT or REJECTED. Register at least one redirect and scope, then submit to IN_REVIEW. Approval changes it to APPROVED; use Go live to enter LIVE mode. Contact support for later configuration changes to an approved app.
- Test approval, denial, incorrect state and an expired authorization code.
- Verify tokens and records remain isolated between two test restaurants.
- Exercise refresh, a lost response and uninstalling from the dashboard.
- Test tampered, expired, duplicate and out-of-order webhooks.
- Implement bounded retries for 429/transient errors and duplicate handling for writes.
- Provide support with app purpose, scopes, test steps, HTTPS URLs and contact details, without secrets or user tokens.
Reference checked against the current v1 implementation on 27 September 2026. Check the current OpenAPI before using new endpoints or fields. Tolerate unknown response fields and record unknown event types without crashing.
Endpoint reference
Expand an operation for parameters, request body, response fields and HTTP statuses. Schema descriptions use English, shared with OpenAPI. Response examples show structure, not real restaurant data.
17 / 17 operations
GET/healthService health
Scope: Public
Success response · 200
{
"status": "ok",
"service": "api-v1",
"ts": "2026-09-27T00:00:00.000Z"
}All response fields and types
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"ok"
]
},
"service": {
"type": "string",
"enum": [
"api-v1"
]
},
"ts": {
"type": "string",
"format": "date-time"
}
},
"required": [
"status",
"service",
"ts"
]
}200 Success
POST/oauth/tokenExchange an authorization code or rotate tokens
Scope: Public
JSON only in these examples. No client_credentials grant. PKCE S256 is mandatory, even for confidential clients. Serialize refresh operations per installation; old refresh-token reuse revokes the refresh family.
Request body · application/json
{
"type": "object",
"properties": {
"grant_type": {
"type": "string",
"enum": [
"authorization_code",
"refresh_token"
]
},
"client_id": {
"type": "string",
"maxLength": 200
},
"client_secret": {
"type": "string",
"description": "Required if a client secret has been configured.",
"maxLength": 200
},
"code": {
"type": "string",
"description": "Required for authorization_code; single use, valid for 60 seconds.",
"maxLength": 200
},
"redirect_uri": {
"type": "string",
"description": "Required for authorization_code. Exact registered URL.",
"maxLength": 500,
"format": "uri"
},
"code_verifier": {
"type": "string",
"description": "Required for authorization_code. Original PKCE verifier.",
"minLength": 43,
"maxLength": 128
},
"refresh_token": {
"type": "string",
"description": "Required for refresh_token grant.",
"maxLength": 200
}
},
"required": [
"grant_type",
"client_id"
]
}Success response · 200
{
"access_token": "example",
"refresh_token": "example",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "example"
}All response fields and types
{
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"refresh_token": {
"type": "string"
},
"token_type": {
"type": "string",
"enum": [
"Bearer"
]
},
"expires_in": {
"type": "integer",
"example": 3600
},
"scope": {
"type": "string",
"description": "Space-separated granted permissions."
}
},
"required": [
"access_token",
"refresh_token",
"token_type",
"expires_in",
"scope"
]
}200 Success400 invalid_request / invalid_grant401 invalid_client403 access_denied: app application requires DigiMenu approval, including TEST mode429 30 requests per minute per IP
GET/meRead the token’s installation context
Scope: Valid app token
Success response · 200
{
"installationId": "example",
"appId": "example",
"restaurantId": "example",
"scopes": [
"menu:read"
],
"mode": "TEST",
"restaurant": {
"slug": "example",
"name": "example"
}
}All response fields and types
{
"type": "object",
"properties": {
"installationId": {
"type": "string"
},
"appId": {
"type": "string"
},
"restaurantId": {
"type": "string"
},
"scopes": {
"type": "array",
"items": {
"type": "string",
"enum": [
"menu:read",
"orders:read",
"orders:write",
"tables:read",
"deliveries:read",
"deliveries:write",
"customers:read",
"webhooks:manage"
]
}
},
"mode": {
"type": "string",
"enum": [
"TEST",
"LIVE"
]
},
"restaurant": {
"type": "object",
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"slug",
"name"
],
"nullable": true
}
},
"required": [
"installationId",
"appId",
"restaurantId",
"scopes",
"mode",
"restaurant"
]
}200 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
GET/menuRead active categories and products
Scope: menu:read
No pagination. Does not expose recipe cost, inventory, variants or internal preparation data.
Success response · 200
{
"restaurant": {
"slug": "example",
"name": "example"
},
"categories": [
{
"id": "example",
"name": "example",
"order": 0,
"products": [
{
"id": "example",
"name": "example",
"description": null,
"price": 0,
"isActive": true,
"order": 0,
"allergens": null
}
]
}
]
}All response fields and types
{
"type": "object",
"properties": {
"restaurant": {
"type": "object",
"properties": {
"slug": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"slug",
"name"
]
},
"categories": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"order": {
"type": "integer"
},
"products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"type": "string",
"nullable": true
},
"price": {
"type": "number",
"description": "Major currency units, not minor units. Currency is not included in this response."
},
"isActive": {
"type": "boolean"
},
"order": {
"type": "integer"
},
"allergens": {
"type": "string",
"nullable": true
}
},
"required": [
"id",
"name",
"description",
"price",
"isActive",
"order",
"allergens"
]
}
}
},
"required": [
"id",
"name",
"order",
"products"
]
}
}
},
"required": [
"restaurant",
"categories"
]
}200 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
GET/tablesRead active tables
Scope: tables:read
Returns a bare array. Does not expose QR secrets, sessions or occupancy.
Success response · 200
[
{
"id": "example",
"name": "example",
"tableNumber": 0
}
]All response fields and types
{
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"tableNumber": {
"type": "integer"
}
},
"required": [
"id",
"name",
"tableNumber"
]
}
}200 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
GET/ordersList orders, newest first
Scope: orders:read
| Parameter | Location / type | Description |
|---|---|---|
limit | query / integer | Page size. {"type":"integer","minimum":1,"maximum":100,"default":25} |
cursor | query / string | Pass the previous nextCursor unchanged. Stop when null. {"type":"string","maxLength":64} |
channel | query / string | Order channel. {"type":"string","enum":["TABLE","TAKEAWAY","DELIVERY"]} |
status | query / string | Use a known order status, e.g. OUT_FOR_DELIVERY; omit for all statuses. {"type":"string","maxLength":40} |
Success response · 200
{
"data": [
{
"id": "example",
"shortId": "example",
"channel": "TABLE",
"status": "example",
"createdAt": "2026-09-27T00:00:00.000Z",
"scheduledFor": null,
"deliveryAddress": {},
"deliveryFeeMinor": null,
"items": [
{
"name": "example",
"quantity": 0,
"note": null
}
],
"courierRef": null
}
],
"nextCursor": null
}All response fields and types
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"shortId": {
"type": "string"
},
"channel": {
"type": "string",
"enum": [
"TABLE",
"TAKEAWAY",
"DELIVERY"
]
},
"status": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"scheduledFor": {
"type": "string",
"format": "date-time",
"nullable": true
},
"deliveryAddress": {
"type": "object",
"nullable": true,
"additionalProperties": true,
"description": "Coarse locality with contactRedacted=true by default. Exact address/contact is returned only with customers:read AND assignment to this app’s courier."
},
"deliveryFeeMinor": {
"type": "integer",
"description": "Minor currency units; currency not included.",
"nullable": true
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"quantity": {
"type": "integer"
},
"note": {
"type": "string",
"nullable": true
}
},
"required": [
"name",
"quantity",
"note"
]
}
},
"courierRef": {
"type": "string",
"nullable": true
}
},
"required": [
"id",
"shortId",
"channel",
"status",
"createdAt",
"scheduledFor",
"deliveryAddress",
"deliveryFeeMinor",
"items",
"courierRef"
]
}
},
"nextCursor": {
"type": "string",
"nullable": true
}
},
"required": [
"data",
"nextCursor"
]
}200 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
GET/orders/{id}Read an order
Scope: orders:read
| Parameter | Location / type | Description |
|---|---|---|
id * | path / string | Resource identifier. {"type":"string"} |
Success response · 200
{
"id": "example",
"shortId": "example",
"channel": "TABLE",
"status": "example",
"createdAt": "2026-09-27T00:00:00.000Z",
"scheduledFor": null,
"deliveryAddress": {},
"deliveryFeeMinor": null,
"items": [
{
"name": "example",
"quantity": 0,
"note": null
}
],
"courierRef": null
}All response fields and types
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"shortId": {
"type": "string"
},
"channel": {
"type": "string",
"enum": [
"TABLE",
"TAKEAWAY",
"DELIVERY"
]
},
"status": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"scheduledFor": {
"type": "string",
"format": "date-time",
"nullable": true
},
"deliveryAddress": {
"type": "object",
"nullable": true,
"additionalProperties": true,
"description": "Coarse locality with contactRedacted=true by default. Exact address/contact is returned only with customers:read AND assignment to this app’s courier."
},
"deliveryFeeMinor": {
"type": "integer",
"description": "Minor currency units; currency not included.",
"nullable": true
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"quantity": {
"type": "integer"
},
"note": {
"type": "string",
"nullable": true
}
},
"required": [
"name",
"quantity",
"note"
]
}
},
"courierRef": {
"type": "string",
"nullable": true
}
},
"required": [
"id",
"shortId",
"channel",
"status",
"createdAt",
"scheduledFor",
"deliveryAddress",
"deliveryFeeMinor",
"items",
"courierRef"
]
}200 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
POST/orders/{id}/transitionsUpdate a delivery-leg order status
Scope: orders:write
ASSIGNED_TO_COURIER → OUT_FOR_DELIVERY → DELIVERED or DELIVERY_FAILED. Same status is a no-op. This generic route does not run courier GPS/proof checks: courier apps should use /deliveries. No payment, kitchen, cancel, create-order or refund operation is exposed.
| Parameter | Location / type | Description |
|---|---|---|
id * | path / string | Resource identifier. {"type":"string"} |
Idempotency-Key | header / string | Optional. Unique per installation AND operation. A response is cached for 5 minutes in one process; not durable across restarts. Never reuse for another order or body. {"type":"string"} |
Request body · application/json
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"OUT_FOR_DELIVERY",
"DELIVERED",
"DELIVERY_FAILED"
]
}
},
"required": [
"status"
]
}Success response · 201
{
"id": "example",
"shortId": "example",
"channel": "TABLE",
"status": "example",
"createdAt": "2026-09-27T00:00:00.000Z",
"scheduledFor": null,
"deliveryAddress": {},
"deliveryFeeMinor": null,
"items": [
{
"name": "example",
"quantity": 0,
"note": null
}
],
"courierRef": null
}All response fields and types
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"shortId": {
"type": "string"
},
"channel": {
"type": "string",
"enum": [
"TABLE",
"TAKEAWAY",
"DELIVERY"
]
},
"status": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"scheduledFor": {
"type": "string",
"format": "date-time",
"nullable": true
},
"deliveryAddress": {
"type": "object",
"nullable": true,
"additionalProperties": true,
"description": "Coarse locality with contactRedacted=true by default. Exact address/contact is returned only with customers:read AND assignment to this app’s courier."
},
"deliveryFeeMinor": {
"type": "integer",
"description": "Minor currency units; currency not included.",
"nullable": true
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"quantity": {
"type": "integer"
},
"note": {
"type": "string",
"nullable": true
}
},
"required": [
"name",
"quantity",
"note"
]
}
},
"courierRef": {
"type": "string",
"nullable": true
}
},
"required": [
"id",
"shortId",
"channel",
"status",
"createdAt",
"scheduledFor",
"deliveryAddress",
"deliveryFeeMinor",
"items",
"courierRef"
]
}201 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation409 INVALID_TRANSITION429 Rate or hourly quota exceeded
GET/webhooksList this app’s webhook endpoints
Scope: webhooks:manage
App-wide, not restaurant-specific. Register once per app, not once per installation.
Success response · 200
[
{
"id": "example",
"url": "https://eklenti.example/webhooks",
"events": [
"order.created"
],
"active": true,
"disabledAt": null,
"createdAt": "2026-09-27T00:00:00.000Z"
}
]All response fields and types
{
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
},
"events": {
"type": "array",
"items": {
"type": "string",
"enum": [
"order.created",
"order.paid",
"delivery.ready",
"delivery.assigned",
"delivery.picked_up",
"delivery.delivered",
"delivery.failed"
]
}
},
"active": {
"type": "boolean"
},
"disabledAt": {
"type": "string",
"format": "date-time",
"nullable": true
},
"createdAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"url",
"events",
"active",
"disabledAt",
"createdAt"
]
}
}200 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
POST/webhooksRegister a webhook receiver
Scope: webhooks:manage
No idempotency-key support. Check GET /webhooks before retrying a create after a network timeout; a duplicate request creates another endpoint. Secret is returned once.
Request body · application/json
{
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "Public HTTPS URL on port 443; credentials, redirects and private hosts are rejected.",
"format": "uri",
"maxLength": 2048
},
"events": {
"type": "array",
"items": {
"type": "string",
"enum": [
"order.created",
"order.paid",
"delivery.ready",
"delivery.assigned",
"delivery.picked_up",
"delivery.delivered",
"delivery.failed"
],
"maxLength": 64
},
"minItems": 1
}
},
"required": [
"url",
"events"
]
}Success response · 201
{
"id": "example",
"url": "https://eklenti.example/webhooks",
"events": [
"order.created"
],
"active": true,
"disabledAt": null,
"createdAt": "2026-09-27T00:00:00.000Z",
"secret": "example"
}All response fields and types
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
},
"events": {
"type": "array",
"items": {
"type": "string",
"enum": [
"order.created",
"order.paid",
"delivery.ready",
"delivery.assigned",
"delivery.picked_up",
"delivery.delivered",
"delivery.failed"
]
}
},
"active": {
"type": "boolean"
},
"disabledAt": {
"type": "string",
"format": "date-time",
"nullable": true
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"secret": {
"type": "string",
"description": "Signing secret; returned once at creation."
}
},
"required": [
"id",
"url",
"events",
"active",
"disabledAt",
"createdAt",
"secret"
]
}201 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
DELETE/webhooks/{id}Disable a webhook endpoint
Scope: webhooks:manage
Soft disable; repeating for your already-disabled endpoint succeeds. No public re-enable endpoint: register a new endpoint and store its new secret.
| Parameter | Location / type | Description |
|---|---|---|
id * | path / string | Resource identifier. {"type":"string"} |
Success response · 200
{
"id": "example",
"active": false
}All response fields and types
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"active": {
"type": "boolean",
"enum": [
false
]
}
},
"required": [
"id",
"active"
]
}200 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
GET/webhooks/{id}/deliveriesRead delivery attempts for this app’s endpoint
Scope: webhooks:manage
| Parameter | Location / type | Description |
|---|---|---|
id * | path / string | Resource identifier. {"type":"string"} |
limit | query / integer | Page size. {"type":"integer","minimum":1,"maximum":100,"default":25} |
cursor | query / string | Pass the previous nextCursor unchanged. Stop when null. {"type":"string","maxLength":64} |
Success response · 200
{
"data": [
{
"id": "example",
"eventType": "example",
"status": "PENDING",
"attempts": 0,
"responseCode": null,
"lastError": null,
"createdAt": "2026-09-27T00:00:00.000Z"
}
],
"nextCursor": null
}All response fields and types
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"eventType": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"PENDING",
"DELIVERED",
"FAILED"
]
},
"attempts": {
"type": "integer"
},
"responseCode": {
"type": "integer",
"nullable": true
},
"lastError": {
"type": "string",
"nullable": true
},
"createdAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"eventType",
"status",
"attempts",
"responseCode",
"lastError",
"createdAt"
]
}
},
"nextCursor": {
"type": "string",
"nullable": true
}
},
"required": [
"data",
"nextCursor"
]
}200 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
GET/deliveries/{runId}Read a run assigned to this app
Scope: deliveries:read
Requires a courier installation created by granting deliveries:write. Reads only that installation’s courier runs.
| Parameter | Location / type | Description |
|---|---|---|
runId * | path / string | Resource identifier. {"type":"string"} |
Success response · 200
{
"id": "example",
"status": "example",
"startedAt": null,
"completedAt": null,
"routeDistanceMeters": null,
"routeDurationSeconds": null,
"orders": [
{
"id": "example",
"status": "example"
}
],
"location": {
"lat": 0,
"lng": 0,
"at": "2026-09-27T00:00:00.000Z"
}
}All response fields and types
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string"
},
"startedAt": {
"type": "string",
"format": "date-time",
"nullable": true
},
"completedAt": {
"type": "string",
"format": "date-time",
"nullable": true
},
"routeDistanceMeters": {
"type": "integer",
"nullable": true
},
"routeDurationSeconds": {
"type": "integer",
"nullable": true
},
"orders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string"
}
},
"required": [
"id",
"status"
]
}
},
"location": {
"type": "object",
"properties": {
"lat": {
"type": "number"
},
"lng": {
"type": "number"
},
"at": {
"type": "string",
"format": "date-time"
}
},
"required": [
"lat",
"lng",
"at"
],
"nullable": true
}
},
"required": [
"id",
"status",
"startedAt",
"completedAt",
"routeDistanceMeters",
"routeDurationSeconds",
"orders",
"location"
]
}200 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
POST/deliveries/{runId}/departStart an assigned run
Scope: deliveries:write
ASSIGNED → ON_ROUTE. Moves assigned orders to OUT_FOR_DELIVERY. A second depart fails with INVALID_RUN_STATE; reconcile GET before retrying.
| Parameter | Location / type | Description |
|---|---|---|
runId * | path / string | Resource identifier. {"type":"string"} |
Success response · 201
{
"ok": true,
"orders": 0
}All response fields and types
{
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"orders": {
"type": "integer"
}
},
"required": [
"ok",
"orders"
]
}201 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
POST/deliveries/{runId}/pingsSend a courier location
Scope: deliveries:write
The implementation resolves the active run from the authenticated courier and does not use runId to select a run. Send your assigned active run ID. Pings may be dropped with stored=false / THROTTLED; do not immediately retry.
| Parameter | Location / type | Description |
|---|---|---|
runId * | path / string | Resource identifier. {"type":"string"} |
Request body · application/json
{
"type": "object",
"properties": {
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"accuracyM": {
"type": "number"
},
"headingDeg": {
"type": "number"
},
"speedMps": {
"type": "number"
},
"recordedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"lat",
"lng"
]
}Success response · 201
{
"stored": true,
"requeried": true
}All response fields and types
{
"oneOf": [
{
"type": "object",
"properties": {
"stored": {
"type": "boolean",
"enum": [
true
]
},
"requeried": {
"type": "boolean"
}
},
"required": [
"stored",
"requeried"
]
},
{
"type": "object",
"properties": {
"stored": {
"type": "boolean",
"enum": [
false
]
},
"reason": {
"type": "string",
"enum": [
"THROTTLED"
]
}
},
"required": [
"stored",
"reason"
]
}
]
}201 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
POST/deliveries/orders/{orderId}/deliveredConfirm delivery with location and proof
Scope: deliveries:write
Requires assigned order OUT_FOR_DELIVERY, fresh GPS near drop-off, and restaurant-selected proof. CODE requires code; PHOTO/SIGNATURE require a platform-issued upload unavailable through public v1. Do not treat these modes as a complete external-only integration.
| Parameter | Location / type | Description |
|---|---|---|
orderId * | path / string | Resource identifier. {"type":"string"} |
Request body · application/json
{
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Required when the restaurant uses CODE proof mode.",
"maxLength": 16
},
"proofRef": {
"type": "string",
"description": "PHOTO/SIGNATURE require a platform-issued upload. The public v1 API has no proof-upload endpoint; do not submit an arbitrary URL.",
"maxLength": 500
}
},
"required": []
}Success response · 201
{
"ok": true
}All response fields and types
{
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
},
"required": [
"ok"
]
}201 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
POST/deliveries/orders/{orderId}/failedReport a failed delivery
Scope: deliveries:write
Requires an assigned OUT_FOR_DELIVERY order. No general idempotency header support; reconcile order state after an ambiguous response.
| Parameter | Location / type | Description |
|---|---|---|
orderId * | path / string | Resource identifier. {"type":"string"} |
Request body · application/json
{
"type": "object",
"properties": {
"reason": {
"type": "string",
"enum": [
"CUSTOMER_ABSENT",
"WRONG_ADDRESS",
"CUSTOMER_REJECTED",
"OTHER"
]
},
"note": {
"type": "string",
"description": "Accepted by validation; currently not persisted by the delivery service.",
"maxLength": 200
}
},
"required": [
"reason"
]
}Success response · 201
{
"ok": true
}All response fields and types
{
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
},
"required": [
"ok"
]
}201 Success400 Invalid request or resource state401 Missing/expired token, revoked installation, or app removed403 APP_APPROVAL_REQUIRED (including TEST), missing scope, inactive subscription or delivery restriction404 Resource not found within the installation429 Rate or hourly quota exceeded
Integration support
Send your app ID, endpoint and error code. Do not send tokens or secrets.
support@digimenu.us