Documentation

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:

  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#

For restaurants using the food-order tool, Sela always sends these to matching endpoints:

EventSent when
order.createdA customer confirms an order and it enters pending approval
order.approvedThe order is approved, from the dashboard or by the REST API
order.rejectedThe order is rejected
order.arrivedThe 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#

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 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.
  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.
Webhooks | Sela docs | Sela