Webhooks

Sailor sends a signed, durable outcome webhook to the public HTTPS URL configured on an action outcome. Use the payload to update your CRM, trigger follow-up, audit list movement, or start downstream automation.

Before configuring an action webhook, an owner or admin creates a signing secret in Settings → Integrations → Webhook signing. Sailor shows whsec_… secrets once. Store the secret only in the receiver’s server-side secret manager.

For an outcome that existed before signing setup, Sailor atomically creates an encrypted bootstrap secret on the next delivery and signs that delivery immediately—there is no unsigned fallback. Rotate the bootstrap secret in Settings to obtain the one-time value before changing your receiver to reject invalid signatures.

When Webhooks Are Sent

1

You create an action outcome

The outcome has outcome_type: "action" and a webhook_url.

2

An agent applies the outcome

The action is selected during a live call.

3

Sailor runs the destination action

Sailor records the outcome and attempts any configured Smart List movement.

4

Sailor posts the webhook

Your endpoint receives the outcome, call, scope, and destination details.

Return any 2xx status to acknowledge receipt. Do slow or unreliable downstream work after you have accepted the webhook.

Verify Every Delivery

Sailor sends these headers on every attempt:

HeaderMeaning
X-Sailor-Delivery-IdStable UUID for this logical delivery. It does not change across retries.
Idempotency-KeySame value as X-Sailor-Delivery-Id.
X-Sailor-Signature-VersionInteger signing-secret version.
X-Sailor-Signaturet=<unix-seconds>,v1=<hex-hmac> over the exact raw body.

Build the signed message as:

<timestamp>.<delivery-id>.<exact-raw-request-body>

Compute HMAC-SHA256 with the matching whsec_… secret and compare the lowercase hex digest in constant time. Reject a timestamp more than five minutes from your server clock. Verify the signature before parsing JSON or performing any side effect.

During rotation, new deliveries use the new version immediately and the prior version remains listed for a seven-day verification overlap. Keep both secrets until the overlap ends; a delivery always identifies its version.

Example Payload

1{
2 "error_message": "",
3 "timestamp": 1781935984000,
4 "scope": {
5 "subaccount_id": "00000000-0000-4000-8000-000000000001",
6 "organization_id": "00000000-0000-4000-8000-000000000002"
7 },
8 "outcome": {
9 "outcome_id": "00000000-0000-4000-8000-000000000003",
10 "outcome_name": "Interested",
11 "outcome_type": "action"
12 },
13 "call": {
14 "call_id": "00000000-0000-4000-8000-000000000004",
15 "phone_number": "+15555550100",
16 "contact_id": "00000000-0000-4000-8000-000000000005"
17 },
18 "destination": {
19 "destination_type": "smart_list",
20 "destination_id": "00000000-0000-4000-8000-000000000006"
21 }
22}

Payload Fields

FieldMeaning
timestampUnix epoch timestamp in milliseconds for the webhook event.
scope.subaccount_idWorkspace scope that owns the call and outcome.
scope.organization_idOrganization that owns the workspace.
outcomeOutcome ID, name, and type applied by the agent.
callCall ID, contacted phone number, and contact ID.
destinationSmart List destination action Sailor attempted, when configured.
error_messageError text if Sailor attempted the destination action and encountered a failure.

Destination Shapes

For an add-to-list outcome:

1{
2 "destination_type": "smart_list",
3 "destination_id": "00000000-0000-4000-8000-000000000006"
4}

For a remove-from-lists outcome:

1{
2 "destination_type": "remove_from_smart_list",
3 "selection": "selected",
4 "destination_ids": [
5 "00000000-0000-4000-8000-000000000010"
6 ]
7}

Receiver Checklist

  • Accept POST requests with Content-Type: application/json.
  • Return a 2xx status after you store or enqueue the event.
  • Verify X-Sailor-Signature against the exact raw body before parsing JSON.
  • Make your receiver idempotent by storing the unique X-Sailor-Delivery-Id before doing work.
  • Keep endpoint credentials and forwarding secrets out of browser code.
  • Log delivery ID, signature version, result, and timestamp while testing. Do not log the signing secret.

Minimal Receiver

server.js
1import express from "express";
2import crypto from "node:crypto";
3
4const app = express();
5const signingSecrets = new Map([
6 ["1", process.env.SAILOR_WEBHOOK_SECRET_V1],
7]);
8
9async function saveOutcomeEvent(event) {
10 console.log("Accepted Sailor outcome event", event);
11}
12
13app.post("/sailor/outcomes", express.raw({ type: "application/json" }), async (req, res) => {
14 const deliveryId = req.get("x-sailor-delivery-id") ?? "";
15 const version = req.get("x-sailor-signature-version") ?? "";
16 const signatureHeader = req.get("x-sailor-signature") ?? "";
17 const secret = signingSecrets.get(version);
18 const fields = Object.fromEntries(signatureHeader.split(",").map((part) => part.trim().split("=")));
19 const timestamp = Number(fields.t);
20 const received = fields.v1 ?? "";
21 const body = req.body.toString("utf8");
22
23 if (!secret || !deliveryId || !Number.isSafeInteger(timestamp)
24 || Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300
25 || !/^[a-f0-9]{64}$/.test(received)) {
26 return res.sendStatus(401);
27 }
28 const expected = crypto
29 .createHmac("sha256", secret)
30 .update(`${timestamp}.${deliveryId}.${body}`, "utf8")
31 .digest("hex");
32 if (!crypto.timingSafeEqual(Buffer.from(received, "hex"), Buffer.from(expected, "hex"))) {
33 return res.sendStatus(401);
34 }
35
36 const { outcome, call, destination } = JSON.parse(body);
37
38 await saveOutcomeEvent({
39 deliveryId,
40 callId: call.call_id,
41 outcomeId: outcome.outcome_id,
42 outcomeName: outcome.outcome_name,
43 destinationType: destination?.destination_type ?? "none",
44 });
45
46 res.sendStatus(204);
47});

Delivery And Retry Behavior

Sailor persists the URL, exact body, delivery ID, and signing-secret version before the first network attempt. Network errors and HTTP 408, 409, 425, 429, and 5xx responses retry with bounded exponential backoff and deterministic jitter. A valid Retry-After header can extend the next delay. There are at most eight total attempts.

Delivery is at least once: a receiver can process a request even when Sailor never receives its response. Deduplicate on X-Sailor-Delivery-Id; do not assume exactly-once delivery or global ordering.

Every attempt has an append-only audit row. The original delivery keeps one stable ID and body across retries and operator replay.

Endpoint Safety

Webhook URLs must use HTTPS on port 443. Sailor resolves both A and AAAA records before every attempt, rejects the whole hostname if any address is private or special-use, pins the validated address for the TLS connection, preserves hostname verification, and does not follow redirects. Localhost, link-local, private-network, credential-bearing, fragment-bearing, and non-HTTPS URLs are rejected.

Testing Locally

Use a tunnel during development so Sailor can reach your local server.

$ngrok http 3000

Set the outcome webhook_url to the public HTTPS tunnel URL while testing, then switch it to your production HTTPS URL before using the outcome with live agents.

See the Webhooks section in the API Reference for the complete schema.