Developers
Webhooks
Receive signed HTTPS notifications from Sela for order events and automation actions, with payloads, retry rules and verification code.
Last updated: 2026-09-29
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:
- Open Webhooks and choose Add Endpoint.
- Enter the receiving URL, pick the events to subscribe to, and optionally add a description.
- Choose the environment (
productionortest). - 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:
| Pattern | Matches |
|---|---|
* | Every event |
order.* | Any event starting with order. |
order.created | That exact event |
Events that are actually sent#
Sela emits webhooks from two sources.
1. Order events#
For restaurants using the food-order tool, Sela always sends these to matching endpoints:
| Event | Sent when |
|---|---|
order.created | A customer confirms an order and it enters pending approval |
order.approved | The order is approved, from the dashboard or by the REST API |
order.rejected | The order is rejected |
order.arrived | The order is marked as arrived |
The payload data is the full order object described in REST API. The end-to-end flow is in Restaurant order integration.
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#
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| Header | Meaning |
|---|---|
X-Sela-Event | The event name. |
X-Sela-Delivery | Unique delivery ID. It stays the same across retries, so use it to deduplicate. |
X-Sela-Timestamp | Unix time in milliseconds, regenerated on every attempt. |
X-Sela-Signature-Version | Always v1. |
X-Sela-Signature | Lowercase hex HMAC-SHA256 signature. |
The body has this envelope, and its timestamp equals the header:
{
"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:
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#
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:
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#
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#
<?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
2xxresponse 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-Timestampand a fresh signature over the new body, whileX-Sela-Deliverystays the same. - After the last failure the delivery is marked
failed. Delivery statuses arepending,delivered,retryingandfailed. 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 assumeorder.approvedarrives afterorder.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:
- 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 alocalhostaddress only works when the receiver is on a network Sela can reach. - Subscribe to
order.*(or*if you use automations). - Trigger an event. For orders, place an order through your assistant and approve or reject it in the dashboard, or call
POST /api/v1/orders/action. For automation events, create an automation with an execute webhook action and fire its trigger. - Confirm the delivery shows
deliveredin the endpoint's log. - 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
401and 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.
Can't find what you need? Contact support.