التوثيق

المطورون

واجهة REST

Read conversations, messages and orders, sync every order into your own system, and act on restaurant orders with Sela's server-to-server REST API.

آخر تحديث: 2026-10-05

The Sela REST API is a small, read-mostly, server-to-server API. It lets your backend list conversations and messages, sync every order into your POS, ERP or accounting system, list restaurant orders, and approve, reject or complete an order. It is versioned under /api/v1.

Access and plans#

  • Plan: API keys are available on the Scale and Enterprise plans, and API access must be enabled for your workspace (contact support if it is not). The AI Assist add-on is not required. See Plans and billing.
  • If the plan lapses: every existing key is treated as invalid and calls return 401 unauthorized.
  • Who can create keys: workspace admins, in the dashboard under API Keys.

Base URL#

text
https://jovial-bison-730.convex.site

All examples below use it through an environment variable:

bash
export SELA_API_BASE="https://jovial-bison-730.convex.site"
export SELA_API_KEY="sk_live_..."

Create an API key#

  1. Open API Keys in the dashboard and create a key.
  2. Give it a name and choose the permissions it needs.
  3. Optionally set a rate limit (requests per minute) and an expiry date.
  4. Copy the key immediately. It is shown once. Sela stores only a SHA-256 hash and cannot show it again.

Keys always look like sk_live_ followed by 64 hex characters, even when the environment label is test; the label is only a tag. Revoking a key disables it at once; deleting it removes it.

Permissions#

PermissionAllows
conversations:readGET /api/v1/conversations
messages:readGET /api/v1/messages
orders:readGET /api/v1/orders, GET /api/v1/store-orders
orders:writePOST /api/v1/orders/action

Grant the smallest set each integration needs. A missing permission returns 403 forbidden.

Authentication#

Send the key as a bearer token on every request:

bash
curl "$SELA_API_BASE/api/v1/conversations" \
  -H "Authorization: Bearer $SELA_API_KEY"

Requests and responses#

  • Responses are application/json with Cache-Control: no-store.
  • Every response carries X-Sela-Request-Id. If you send an X-Request-Id header, Sela echoes it; otherwise it generates a UUID. Include this ID when you contact support.
  • Success bodies use { "data": ..., "meta": { "request_id": "...", ... } }.
  • Timestamps are Unix milliseconds.

Pagination#

List endpoints accept:

ParameterDefaultRangeMeaning
limit501 to 100Page size.
cursorNoneOpaqueThe meta.next_cursor value from the previous page.

meta includes next_cursor (string or null), is_done (boolean) and limit. Keep requesting with the returned cursor until is_done is true. Do not build cursors yourself.

Errors#

Errors use one envelope:

json
{
  "error": {
    "code": "forbidden",
    "message": "Missing orders:write permission.",
    "request_id": "0b2f..."
  }
}
HTTPcodeWhen
400invalid_requestBad or missing parameters, malformed id, invalid status or action, or a body that is not a JSON object.
401unauthorizedMissing bearer token, or the key is invalid, revoked, expired, or the plan is no longer eligible.
403forbiddenThe key lacks the required permission.
404not_foundThe order does not exist or is not an order of your workspace.
409invalid_stateThe order cannot make that transition.
429rate_limitedThe key exceeded its per-minute limit.

Rate limits#

Rate limits are set per key when you create it, as a positive number of requests per minute. If you leave it empty the key is not throttled, so set one for every key you hand to a third party. The window is a fixed 60-second window.

A 429 response includes extra fields inside error:

json
{
  "error": {
    "code": "rate_limited",
    "message": "API key rate limit exceeded.",
    "request_id": "0b2f...",
    "retry_after_seconds": 12,
    "limit": 60
  }
}

There is no Retry-After header. Read error.retry_after_seconds and back off.

GET /api/v1/conversations#

Requires conversations:read. Returns conversations, most recently updated first.

Query parameterValuesNotes
statusopen, closed, escalatedAny other value is ignored (no filter).
limit, cursorSee pagination
bash
curl "$SELA_API_BASE/api/v1/conversations?status=open&limit=25" \
  -H "Authorization: Bearer $SELA_API_KEY"
json
{
  "data": [
    {
      "id": "j57abc...",
      "status": "open",
      "channel": "whatsapp",
      "subject": "Order help",
      "isSolved": false,
      "createdAt": 1712345600000,
      "updatedAt": 1712345678901,
      "lastMessageAt": 1712345678901
    }
  ],
  "meta": {
    "request_id": "...",
    "next_cursor": "...",
    "is_done": false,
    "limit": 25,
    "status": "open"
  }
}

subject is optional. channel is one of web, whatsapp, messenger, instagram, telegram, email, sms, voice. meta.status is the applied filter, or null.

GET /api/v1/messages#

Requires messages:read. Returns the messages of one conversation, newest first.

Query parameterRequiredNotes
conversation_idYesThe id from the conversations list. Missing or malformed returns 400 invalid_request.
limit, cursorNoSee pagination.
bash
curl "$SELA_API_BASE/api/v1/messages?conversation_id=j57abc...&limit=50" \
  -H "Authorization: Bearer $SELA_API_KEY"
json
{
  "data": [
    { "id": "k17def...", "direction": "contact", "text": "Hello", "createdAt": 1712345678901 }
  ],
  "meta": {
    "request_id": "...",
    "next_cursor": null,
    "is_done": true,
    "limit": 50,
    "conversation_id": "j57abc..."
  }
}

direction is contact (the customer), agent (the assistant or a team member) or system. Only text and timestamps are exposed: no attachments, sender ids, or an AI-versus-human flag. A conversation id that belongs to another workspace returns an empty page with is_done: true, not an error.

GET /api/v1/orders#

Requires orders:read. Returns restaurant orders handled through the assistant's food-order tool: orders confirmed in a chat, and storefront orders, which get a request in the Operations inbox too. If your workspace has none, the result is an empty page with is_done: true. Store-workspace orders are not listed. See Restaurant order integration for the full flow.

Query parameterValuesNotes
statuspending, approved, arrived, rejected, failed, cancelled, allDefault all. Any other value returns 400 with the allowed list.
limit, cursorSee pagination
bash
curl "$SELA_API_BASE/api/v1/orders?status=pending" \
  -H "Authorization: Bearer $SELA_API_KEY"
json
{
  "data": [
    {
      "id": "kx7...",
      "status": "pending",
      "createdAt": 1712345678901,
      "updatedAt": 1712345678901,
      "conversationId": "j57abc...",
      "customer": { "id": "...", "name": "Ali", "phone": "+9647700000000", "email": null },
      "fulfillment": { "type": "delivery", "address": "Al Mansour" },
      "orderSummary": "2 burgers, fries",
      "specialInstructions": null,
      "details": {},
      "externalOrderId": null,
      "fulfillmentStatus": "submitted",
      "orderReference": "ORD-7K3M"
    }
  ],
  "meta": { "request_id": "...", "next_cursor": null, "is_done": true, "limit": 50, "status": "pending" }
}

Field notes:

  • status values: pending (waiting for approval), approved (approved or in progress), arrived (completed), rejected, failed, cancelled.
  • conversationId, customer.*, fulfillment.*, specialInstructions and externalOrderId can be null.
  • fulfillment.type is delivery, pickup or null.
  • details is the raw order input captured by the assistant, or by the storefront for storefront orders.
  • fulfillmentStatus is the order's step on the Store orders board: submitted, accepted, preparing, ready, out_for_delivery, completed, rejected or cancelled. status stays approved through accepted, preparing, ready and out_for_delivery, so read fulfillmentStatus for the kitchen and delivery steps.
  • orderReference is the customer-facing code, such as ORD-7K3M.
  • fulfillmentStatus and orderReference are additive and appear only for orders that are on the Store orders board. Older orders do not have them, so treat both as optional.

GET /api/v1/store-orders#

Requires orders:read. Returns every order in your workspace: storefront, AI chat, phone and dashboard orders, orders made from invoices, restaurants and stores alike. Each one is a sela.order.v1 object, the same structure as the dashboard's order export and the order_v1 webhook field. The fields are documented in Export orders. Use this endpoint to keep another system in sync; GET /api/v1/orders above keeps its own shape and only covers the restaurant order tool.

Query parameterValuesNotes
fromYYYY-MM-DD, an ISO 8601 timestamp, or Unix millisecondsOrders placed at or after it. A date means the start of that business day in your workspace's time zone.
toSameA date is inclusive (through the end of that day). A timestamp is exclusive.
updated_sinceAn ISO 8601 timestamp or Unix millisecondsOrders changed at or after this moment. Cannot be combined with from or to. Send back meta.next_updated_since, see Incremental sync.
statusComma-separated submitted, accepted, preparing, ready, out_for_delivery, completed, rejected, cancelled, or allDefault all.
limit, cursorSee paginationlimit is at most 100. A cursor only works with the same other parameters.

Order of results:

  • With updated_since: oldest change first.
  • Otherwise: oldest order first. Without from, to or updated_since you get every order.

ISO 8601 timestamps need a time and a zone: Z or a UTC offset, as in 2026-10-05T00:00:00+03:00 or 2026-10-04T21:00:00Z. A timestamp without one (2026-10-05T09:00:00) is refused rather than guessed. A bad value returns 400 invalid_request saying which parameter is wrong.

bash
curl "$SELA_API_BASE/api/v1/store-orders?from=2026-10-01&to=2026-10-05&status=completed&limit=100" \
  -H "Authorization: Bearer $SELA_API_KEY"
json
{
  "data": [
    {
      "schema": "sela.order.v1",
      "id": "jh72...",
      "number": "ORD-7K3M",
      "created_at": "2026-10-05T14:03:22.123+03:00",
      "updated_at": "2026-10-05T14:40:00.000+03:00",
      "business_date": "2026-10-05",
      "timezone": "Asia/Baghdad",
      "status": "completed",
      "source": "storefront",
      "customer": { "id": "k57...", "name": "Ali", "phone": "9647701234567" },
      "amount_only": false,
      "lines": [],
      "promotions": [],
      "totals": { "subtotal": 9000, "delivery_fee": 2000, "discount": 0, "tax": 0, "total": 11000, "currency": "IQD", "promotion_savings": 0 }
    }
  ],
  "meta": {
    "request_id": "...",
    "next_cursor": "...",
    "is_done": true,
    "limit": 100,
    "schema": "sela.order.v1",
    "timezone": "Asia/Baghdad",
    "next_updated_since": "2026-10-05T14:38:00.000+03:00"
  }
}

meta.next_updated_since is where your next incremental poll should start, in the same format as updated_at. See below.

The order is shortened here; every response carries all sela.order.v1 fields. Phone numbers are always full, as in GET /api/v1/orders. Orders with "amount_only": true have no lines: staff recorded an agreed amount, described in notes.

Incremental sync#

  1. Backfill once. Request with from set to your start date and follow next_cursor until is_done is true. Save meta.next_updated_since from the first page of the backfill: changes made while you were paging are picked up from there.
  2. Poll for changes. Request with updated_since set to the saved value and follow next_cursor until is_done is true.
  3. Save the new watermark. Take meta.next_updated_since from the last page (where is_done is true) and use it for the next poll. If you stop mid-way, the value from the last page you processed lets you resume.

Store each order by its id and overwrite it when it comes back (an upsert). The watermark deliberately overlaps: on a finished listing it is never closer than two minutes to the moment of the request, because an order's updated_at is when the change started and a change that was still being saved during your request can carry an earlier time. Orders from that overlap come back on the next poll, and the upsert makes that harmless. Do not build the watermark from updated_at yourself.

Every change to an order (status, payment, delivery, address, a linked invoice's status) moves its updated_at, so a changed order comes back with its new state. A changed order can also reappear later in the same run; the upsert absorbs that too.

POST /api/v1/orders/action#

Requires orders:write. Approves, rejects or completes one order.

Body fieldRequiredNotes
order_idYesThe order id.
actionYesapprove, reject or mark_arrived.
external_order_idNoYour own order reference. Trimmed and cut to 256 characters, and stored on the order.
bash
curl -X POST "$SELA_API_BASE/api/v1/orders/action" \
  -H "Authorization: Bearer $SELA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_id":"kx7...","action":"approve","external_order_id":"menu-123"}'
json
{
  "data": { "id": "kx7...", "status": "approved", "externalOrderId": "menu-123" },
  "meta": { "request_id": "...", "changed": true, "event": "order.approved" }
}

data is the full order object shown above (shortened here), including fulfillmentStatus and orderReference when present. meta.changed says whether the state moved, and meta.event is the webhook event that was sent, or null.

Allowed transitions#

Current statusapproverejectmark_arrived
pendingChanges, sends order.approvedChanges, sends order.rejected409
approvedNo change (changed: false)409Changes, sends order.arrived
rejected409No change409
arrived409409No change
failed, cancelled409409409

Repeating an action that already took effect returns 200 with changed: false and no event, so retries are safe. A supplied external_order_id is still applied. Illegal moves return 409 invalid_state, for example Cannot mark_arrived an order with status pending.

Side effects:

  • approve and reject send an assistant message to the customer's conversation. If that message cannot be delivered the failure is logged and the call still succeeds. You cannot supply a custom customer message through the API.
  • The matching outbound webhook event is sent to your subscribed endpoints, including your own. Ignore echoes of your own actions. See Webhooks.
  • The order on the Store orders board follows: approve makes it accepted, reject makes it rejected, and mark_arrived makes it completed. Each change also sends order.status_changed.
  • The API acts on the three legacy actions only. Preparing, ready and out-for-delivery steps are set on Store, Orders.

Endpoint summary#

MethodPathPermission
GET/api/v1/conversationsconversations:read
GET/api/v1/messagesmessages:read
GET/api/v1/ordersorders:read
GET/api/v1/store-ordersorders:read
POST/api/v1/orders/actionorders:write

Good practice#

  • Store the key in a server-side secret manager and rotate it by creating a new key, switching your service, then revoking the old one.
  • Set an expiry on keys for contractors and pilots.
  • Log X-Sela-Request-Id with every failure.
  • Do not poll aggressively. For order changes, use webhooks and reconcile with GET /api/v1/store-orders?updated_since=... (or GET /api/v1/orders?status=pending for the restaurant order tool).