Developers
REST API
Read conversations, messages and orders and act on restaurant orders with Sela's server-to-server REST API, including auth, errors and rate limits.
Last updated: 2026-09-29
The Sela REST API is a small, read-mostly, server-to-server API. It lets your backend list conversations and messages, 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 |
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 created by the assistant's food-order tool. If your workspace has none, the result is an empty page with is_done: true. 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
}
],
"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.
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). 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.
Endpoint summary#
| Method | Path | Permission |
|---|---|---|
| GET | /api/v1/conversations | conversations:read |
| GET | /api/v1/messages | messages:read |
| GET | /api/v1/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/orders?status=pending.
Can't find what you need? Contact support.