المطورون
واجهة 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#
https://jovial-bison-730.convex.siteAll examples below use it through an environment variable:
export SELA_API_BASE="https://jovial-bison-730.convex.site"
export SELA_API_KEY="sk_live_..."Create an API key#
- Open API Keys in the dashboard and create a key.
- Give it a name and choose the permissions it needs.
- Optionally set a rate limit (requests per minute) and an expiry date.
- 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#
| Permission | Allows |
|---|---|
conversations:read | GET /api/v1/conversations |
messages:read | GET /api/v1/messages |
orders:read | GET /api/v1/orders, GET /api/v1/store-orders |
orders:write | POST /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:
curl "$SELA_API_BASE/api/v1/conversations" \
-H "Authorization: Bearer $SELA_API_KEY"Requests and responses#
- Responses are
application/jsonwithCache-Control: no-store. - Every response carries
X-Sela-Request-Id. If you send anX-Request-Idheader, 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:
| Parameter | Default | Range | Meaning |
|---|---|---|---|
limit | 50 | 1 to 100 | Page size. |
cursor | None | Opaque | The 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:
{
"error": {
"code": "forbidden",
"message": "Missing orders:write permission.",
"request_id": "0b2f..."
}
}| HTTP | code | When |
|---|---|---|
| 400 | invalid_request | Bad or missing parameters, malformed id, invalid status or action, or a body that is not a JSON object. |
| 401 | unauthorized | Missing bearer token, or the key is invalid, revoked, expired, or the plan is no longer eligible. |
| 403 | forbidden | The key lacks the required permission. |
| 404 | not_found | The order does not exist or is not an order of your workspace. |
| 409 | invalid_state | The order cannot make that transition. |
| 429 | rate_limited | The 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:
{
"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 parameter | Values | Notes |
|---|---|---|
status | open, closed, escalated | Any other value is ignored (no filter). |
limit, cursor | See pagination |
curl "$SELA_API_BASE/api/v1/conversations?status=open&limit=25" \
-H "Authorization: Bearer $SELA_API_KEY"{
"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 parameter | Required | Notes |
|---|---|---|
conversation_id | Yes | The id from the conversations list. Missing or malformed returns 400 invalid_request. |
limit, cursor | No | See pagination. |
curl "$SELA_API_BASE/api/v1/messages?conversation_id=j57abc...&limit=50" \
-H "Authorization: Bearer $SELA_API_KEY"{
"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 parameter | Values | Notes |
|---|---|---|
status | pending, approved, arrived, rejected, failed, cancelled, all | Default all. Any other value returns 400 with the allowed list. |
limit, cursor | See pagination |
curl "$SELA_API_BASE/api/v1/orders?status=pending" \
-H "Authorization: Bearer $SELA_API_KEY"{
"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:
statusvalues:pending(waiting for approval),approved(approved or in progress),arrived(completed),rejected,failed,cancelled.conversationId,customer.*,fulfillment.*,specialInstructionsandexternalOrderIdcan benull.fulfillment.typeisdelivery,pickupornull.detailsis the raw order input captured by the assistant, or by the storefront for storefront orders.fulfillmentStatusis the order's step on the Store orders board:submitted,accepted,preparing,ready,out_for_delivery,completed,rejectedorcancelled.statusstaysapprovedthroughaccepted,preparing,readyandout_for_delivery, so readfulfillmentStatusfor the kitchen and delivery steps.orderReferenceis the customer-facing code, such asORD-7K3M.fulfillmentStatusandorderReferenceare 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 parameter | Values | Notes |
|---|---|---|
from | YYYY-MM-DD, an ISO 8601 timestamp, or Unix milliseconds | Orders placed at or after it. A date means the start of that business day in your workspace's time zone. |
to | Same | A date is inclusive (through the end of that day). A timestamp is exclusive. |
updated_since | An ISO 8601 timestamp or Unix milliseconds | Orders changed at or after this moment. Cannot be combined with from or to. Send back meta.next_updated_since, see Incremental sync. |
status | Comma-separated submitted, accepted, preparing, ready, out_for_delivery, completed, rejected, cancelled, or all | Default all. |
limit, cursor | See pagination | limit 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,toorupdated_sinceyou 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.
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"{
"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#
- Backfill once. Request with
fromset to your start date and follownext_cursoruntilis_doneistrue. Savemeta.next_updated_sincefrom the first page of the backfill: changes made while you were paging are picked up from there. - Poll for changes. Request with
updated_sinceset to the saved value and follownext_cursoruntilis_doneistrue. - Save the new watermark. Take
meta.next_updated_sincefrom the last page (whereis_doneistrue) 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 field | Required | Notes |
|---|---|---|
order_id | Yes | The order id. |
action | Yes | approve, reject or mark_arrived. |
external_order_id | No | Your own order reference. Trimmed and cut to 256 characters, and stored on the order. |
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"}'{
"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 status | approve | reject | mark_arrived |
|---|---|---|---|
pending | Changes, sends order.approved | Changes, sends order.rejected | 409 |
approved | No change (changed: false) | 409 | Changes, sends order.arrived |
rejected | 409 | No change | 409 |
arrived | 409 | 409 | No change |
failed, cancelled | 409 | 409 | 409 |
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:
approveandrejectsend 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:
approvemakes itaccepted,rejectmakes itrejected, andmark_arrivedmakes itcompleted. Each change also sendsorder.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#
| Method | Path | Permission |
|---|---|---|
| GET | /api/v1/conversations | conversations:read |
| GET | /api/v1/messages | messages:read |
| GET | /api/v1/orders | orders:read |
| GET | /api/v1/store-orders | orders:read |
| POST | /api/v1/orders/action | orders: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-Idwith every failure. - Do not poll aggressively. For order changes, use webhooks and reconcile with
GET /api/v1/store-orders?updated_since=...(orGET /api/v1/orders?status=pendingfor the restaurant order tool).
لم تجد ما تبحث عنه؟ تواصل مع الدعم.