التوثيق

المطورون

الويب هوك

Receive signed HTTPS notifications from Sela for order events and automation actions, with payloads, retry rules and verification code.

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

Outgoing webhooks let Sela push events to your server as they happen. Each delivery is an HTTPS POST with a JSON body and an HMAC signature you can verify. This page describes what is actually sent, how it is signed and retried, and how to test your receiver.

Access and setup#

  • Plan: outbound webhooks require the Scale or Enterprise plan, and webhooks must be enabled for your workspace (contact support if they are not). See Plans and billing. If the plan lapses, in-flight retries stop with the error "paused: webhook delivery is not enabled for this workspace".
  • Who can manage them: admins, in the dashboard under Integrations, Webhooks. Webhook endpoints are managed only in the dashboard; there is no HTTP management API.

To add an endpoint:

  1. Open Webhooks and choose Add Endpoint.
  2. Enter the receiving URL, pick the events to subscribe to, and optionally add a description.
  3. Choose the environment (production or test).
  4. Copy the signing secret. It looks like whsec_ followed by 32 hex characters and is shown once. Sela cannot show it again, so create a new endpoint if you lose it.

URL rules#

The URL must use https://. Plain http:// is accepted only for localhost, 127.0.0.1, 192.168.* and 10.* addresses, or when the endpoint's environment is test. Your receiver must answer within 15 seconds.

Subscription patterns#

An endpoint's event list is matched like this:

PatternMatches
*Every event
order.*Any event starting with order.
order.createdThat exact event

Events that are actually sent#

Sela emits webhooks from two sources.

1. Order events#

EventSent when
order.createdA new order is recorded, once per order: confirmed in a chat, placed on your storefront, taken by your team (including phone orders) or made from an invoice. Restaurants and stores alike.
order.status_changedAny order changes status. See Order status changes.
order.approvedA restaurant order handled through the food-order tool is approved or accepted, from either orders board or by the REST API
order.rejectedSuch an order is rejected
order.arrivedSuch an order is marked as arrived (completed)

The payload data is the full order object described in REST API. The object also carries fulfillmentStatus and orderReference when the order is on the Store orders board, which covers chat and storefront orders placed since that board was released. Older orders do not have these two fields. The end-to-end flow for restaurants is in Restaurant order integration.

order.created for an order on the Store orders board adds source (where it came from: storefront, ai_chat, dashboard, phone or api) and order_v1, the order in Sela's export format. Its id is the Operations inbox request id when the order has one (the id POST /api/v1/orders/action takes), and the order's own id otherwise; order_v1.id is always the order's own id. When your order tool accepts orders automatically, that first acceptance sends no order.status_changed: order.created already shows the accepted status. Deliveries are not ordered, so still compare order_v1.updated_at before overwriting newer data.

The order_v1 field#

order.created and order.status_changed carry order_v1: the order as a sela.order.v1 object, the same structure as the dashboard's order export and GET /api/v1/store-orders. It is written after the change, so its status, timeline and updated_at describe the order at that moment, and phone numbers are full. Every field is described in Export orders. If you map Sela orders into a POS or ERP, read order_v1 and ignore the older fields around it.

Order status changes#

order.status_changed is sent for every status change of any order in a workspace with webhook access: storefront and chat orders, restaurants and stores, whichever board, the REST API or the system made the change. Endpoints subscribed to order.* or * receive it too. For restaurant orders it arrives alongside the matching order.approved, order.rejected or order.arrived.

It is not sent when an order is placed (the first status is submitted); order.created announces new orders in every workspace. A new ready time on the same status sends nothing.

Its data is the order object plus these fields:

FieldMeaning
fulfillmentStatusThe new status: submitted, accepted, preparing, ready, out_for_delivery, completed, rejected or cancelled.
previousFulfillmentStatusThe status before this change, same values.
orderReferenceThe customer-facing code, such as ORD-7K3M.
sourceWhere the order came from: storefront, ai_chat, dashboard (placed by your team), phone (taken by your team during a call) or api.
statusChangedAtUnix milliseconds of the change.
actorTypeWho made it: staff, customer, system or ai.
cancellationReasonOnly on rejected or cancelled orders with a reason: customer_request, out_of_stock, closed, out_of_zone, customer_unreachable, fake_order, no_driver or other.
estimatedReadyAtUnix milliseconds, only when a ready time was set.
statusNoteOnly when staff wrote a message to the customer with this change.
order_v1The order after the change, in Sela's export format. See The order_v1 field.

Two fields depend on whether the order has a request in the Operations inbox, which is the case in workspaces that use the AI order tool:

With an Operations inbox requestWithout one
idThe request id, the one POST /api/v1/orders/action takes for restaurant ordersThe order's own id. The REST API cannot act on it.
detailsThe raw request inputorderReference, source, lines (name, quantity, unit and line totals, options), subtotal, deliveryFee, total and currency

status is always derived from the new fulfillmentStatus, with or without an Operations inbox request: submitted is pending; accepted, preparing, ready and out_for_delivery are approved; completed is arrived; rejected and cancelled keep their names. Use fulfillmentStatus to follow the kitchen and delivery steps, and status only for the legacy approval states.

json
{
  "event": "order.status_changed",
  "timestamp": 1712345900000,
  "data": {
    "id": "jh72...",
    "status": "approved",
    "createdAt": 1712345678901,
    "updatedAt": 1712345900000,
    "conversationId": null,
    "customer": { "id": "...", "name": "Ali", "phone": "9647701234567", "email": null },
    "fulfillment": { "type": "delivery", "address": "Karrada, street 12, near the bank" },
    "orderSummary": "2 × Chicken shawarma, Fries",
    "specialInstructions": null,
    "details": { "orderReference": "ORD-7K3M", "source": "storefront", "lines": [], "subtotal": 9000, "deliveryFee": 2000, "total": 11000, "currency": "IQD" },
    "externalOrderId": null,
    "fulfillmentStatus": "preparing",
    "orderReference": "ORD-7K3M",
    "previousFulfillmentStatus": "accepted",
    "source": "storefront",
    "statusChangedAt": 1712345900000,
    "actorType": "staff",
    "order_v1": { "schema": "sela.order.v1", "id": "jh72...", "number": "ORD-7K3M", "status": "preparing", "updated_at": "2026-10-05T14:05:00.000+03:00" }
  }
}

lines is shortened to [] and order_v1 to a few fields in this example.

2. Automation "execute webhook" actions#

An automation rule can have an execute webhook action that sends the automation's trigger payload to one chosen endpoint. The event field is the event that fired the automation. Automation events include:

conversation.created, message.received, conversation.escalated, conversation.closed, conversation.lifecycle_changed, work_item.created, work_item.stage_changed, task.created, appointment.created, appointment.updated, approval.requested, approval.decided, payment.confirmed, file.ready.

For automation deliveries, the endpoint must also be subscribed to that event through its pattern list. If it is not, the delivery is closed as failed with "endpoint no longer subscribed to this event". The data shape follows the automation's context and is not a fixed schema, so read fields defensively. See Automations and proactive messages.

Delivery format#

http
POST /sela/webhook HTTP/1.1
Content-Type: application/json
X-Sela-Event: order.created
X-Sela-Delivery: jd7...
X-Sela-Timestamp: 1712345678901
X-Sela-Signature-Version: v1
X-Sela-Signature: 9f2c...e1
HeaderMeaning
X-Sela-EventThe event name.
X-Sela-DeliveryUnique delivery ID. It stays the same across retries, so use it to deduplicate.
X-Sela-TimestampUnix time in milliseconds, regenerated on every attempt.
X-Sela-Signature-VersionAlways v1.
X-Sela-SignatureLowercase hex HMAC-SHA256 signature.

The body has this envelope, and its timestamp equals the header:

json
{
  "event": "order.created",
  "timestamp": 1712345678901,
  "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
  }
}

Verify the signature#

The signature is:

text
hex( HMAC_SHA256( secret, X-Sela-Timestamp + "." + rawBody ) )

rawBody is the exact bytes you received. Verify before parsing, so read the raw body and do not re-serialize the JSON. Also reject old timestamps: five minutes is a good window.

Node.js#

verify.mjs
import crypto from "node:crypto";

export function verify(rawBody, headers, secret) {
  const ts = headers["x-sela-timestamp"];
  if (!ts || Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");
  const got = headers["x-sela-signature"] ?? "";
  return (
    got.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))
  );
}

An Express receiver needs the raw body:

server.mjs
import express from "express";
import { verify } from "./verify.mjs";

const app = express();
app.post("/sela/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.headers, process.env.SELA_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(raw);
  // Deduplicate on req.headers["x-sela-delivery"], then process asynchronously
  res.sendStatus(200);
});
app.listen(8080);

Python#

verify.py
import hashlib
import hmac
import time


def verify(raw_body: bytes, ts: str, sig: str, secret: str) -> bool:
    if abs(time.time() * 1000 - int(ts)) > 300_000:
        return False
    expected = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

PHP#

verify.php
<?php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_SELA_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_SELA_SIGNATURE'] ?? '';
$fresh = abs(microtime(true) * 1000 - (float) $ts) <= 300000;
$ok = $fresh && hash_equals(hash_hmac('sha256', "$ts.$raw", $secret), $sig);
http_response_code($ok ? 200 : 401);

Retries and failure handling#

  • A delivery succeeds on any 2xx response within 15 seconds. Anything else (non-2xx, timeout, network error) is a failure. Redirects are followed by the HTTP client.
  • Sela makes at most 5 attempts in total (the first plus four retries), waiting 10 s, 30 s, 90 s and 270 s after attempts 1 to 4, roughly 400 seconds overall.
  • Each retry has a fresh X-Sela-Timestamp and a fresh signature over the new body, while X-Sela-Delivery stays the same.
  • After the last failure the delivery is marked failed. Delivery statuses are pending, delivered, retrying and failed. Error text is truncated to 500 characters.
  • After 10 consecutive exhausted deliveries the endpoint is disabled automatically. Re-enable it in the dashboard, which resets its failure counter.
  • Delivery is at least once and unordered. Deduplicate on X-Sela-Delivery, and do not assume order.approved arrives after order.created.
  • Deliveries dropped because the endpoint was disabled, deleted or unsubscribed do not count toward auto-disable.

Respond quickly with 200 and process the event in a background job. Slow handlers cause retries and duplicates.

Inspect deliveries#

Each endpoint card in Webhooks lists its recent deliveries with the event, attempt number, status, HTTP status and last error. Use it to see why a delivery failed.

Test your receiver#

There is no "send test event" button, so test with real events:

  1. Create an endpoint with the environment set to test, pointing at a public HTTPS tunnel to your local receiver. Sela's servers must be able to reach the URL, so a localhost address only works when the receiver is on a network Sela can reach.
  2. Subscribe to order.* (or * if you use automations).
  3. Trigger an event. For orders, place an order through your assistant or your storefront and move it on Store, Orders (each step sends order.status_changed), approve or reject it in the Operations inbox, or call POST /api/v1/orders/action. For automation events, create an automation with an execute webhook action and fire its trigger.
  4. Confirm the delivery shows delivered in the endpoint's log.
  5. Check your signature code against a tampered body, which must return 401.

Security notes#

  • Keep the signing secret server-side and store it in a secret manager.
  • Respond 401 and do nothing for invalid signatures or stale timestamps.
  • Use HTTPS in production, and allow only what your receiver needs.
  • Webhook payloads contain customer contact details for orders. Treat them as personal data.