Skip to main content

Webhooks (Outbound)

Webhooks let SalesOS call your system when something happens (a lead is offered, a ranking is published, a message is dispatched). You give us an HTTPS URL and a secret; we POST a signed JSON payload to it whenever a subscribed event fires.
This page covers outbound webhooks (SalesOS → your endpoint). For sending data into SalesOS, see Activities and API Keys.

Configure a webhook in the Dashboard

Go to Integrations → Webhooks → New Webhook and fill in the form.
1

Basic

  • Name (required) — e.g. Notify CRM.
  • Key — a stable identifier (e.g. notify_crm).
  • Description — optional.
2

Events (triggers)

Pick a Category, then select the events that fire this webhook — this is your subscription (see the catalog). Leave it empty for a webhook you only trigger manually (the Test button) or from a workflow.
3

Destination

  • MethodPOST (default), PUT or PATCH.
  • Timeout (seconds) — how long we wait for your 2xx (default 30).
  • URL (required) — your endpoint. Must be https:// and public (internal/loopback hosts are blocked).
  • Authentication — choose HMAC to sign every delivery, then in Auth Config (JSON) set your secret:
    { "secret": "whsec_your_secret" }
    
    (Other schemes are available: Bearer, API Key, Basic, OAuth2.)
4

Advanced

  • Custom Headers (JSON) — extra headers sent with every delivery.
  • Payload Template (JSON with {{ }} variables) — shapes the envelope data from the event context, e.g. { "id": "{{event.id}}", "type": "{{event.type}}", "to": "{{event.to}}" }.
Template values are interpolated into JSON. Use flat, scalar fields. Injecting a nested object via "{{event.data}}" may not render cleanly today — always check the Preview / send a Test first.
5

Preview, test & save

The form shows a live Preview (headers + body). Click Save webhook, then use Test Webhook to send a sample delivery and confirm your endpoint receives — and verifies — it.

The envelope

On the event path, every delivery is a single JSON object:
{
  "id": "evt_2KWPBgLlAfxdpx2AI54pPJ85f4W",
  "type": "ranking.weekly.published",
  "version": "1",
  "ts": "2026-06-03T12:00:00.000Z",
  "tenant": "9b1f0c2e-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
  "data": { /* event-specific — see catalog */ }
}
FieldDescription
idUnique event id, stable across retries → your idempotency key.
typeEvent type (see catalog).
versionSchema version of data. Evolves additively.
tsWhen the event occurred (ISO 8601, UTC).
tenantOrigin tenant (uuid).
dataEvent-specific payload.

Headers

HeaderDescription
X-SalesOS-EventEvent type — route without parsing the body.
X-SalesOS-Event-IdEquals id; stable across retries (idempotency).
X-SalesOS-Delivery-IdPer-attempt id (changes on retry; for debugging).
X-SalesOS-TimestampUnix seconds — part of the signature (anti-replay).
X-SalesOS-TenantOrigin tenant id.
X-SalesOS-Signaturesha256=<hmac> — see Verify.

Verify the signature

Always verify the signature before trusting a delivery. Without it, anyone who learns your URL could forge events.
On the event path, the signature covers {id}.{timestamp}.{rawBody} (Standard-Webhooks style), so it authenticates the payload and the timestamp (anti-replay). Recompute the HMAC with your secret, compare in constant time, and reject if the timestamp is more than 300s off.
import crypto from "node:crypto";

// raw = the exact raw request body (do NOT JSON.parse first)
export function verify(req, secret) {
  const id  = req.headers["x-salesos-event-id"];
  const ts  = req.headers["x-salesos-timestamp"];
  const sig = req.headers["x-salesos-signature"]; // "sha256=<hex>"
  const raw = req.rawBody;

  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay window

  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(`${id}.${ts}.${raw}`).digest("hex");

  const a = Buffer.from(sig), b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Test deliveries (the “Test Webhook” button) and legacy workflow triggers currently use a simpler scheme: the body is the raw rendered template (not the envelope) and the signature header is X-Signature: <prefix><hmac(body)>without the id.timestamp. prefix, so it has no anti-replay. Prefer the event path above for production.

Event catalog

typeWhen it firesdata
lead.offer_createdA lead is offered to a brokerlead/opportunity payload
lead.acceptedA broker accepts a leadlead/opportunity payload
lead.offer_expiredAn offer expires before acceptancelead/opportunity payload
mission.completedA mission is completedmission payload
ranking.weekly.publishedWeekly ranking is published (scheduled)leaderboard (rows by email)
ranking.consolidated.publishedConsolidated ranking is publishedleaderboard (rows by email)
message.dispatchA message is dispatched to a user/group{ to, m }
notification.push.dispatchA push is dispatched to a user/group{ to, n }
Example data for ranking.weekly.published:
{
  "p": { "type": "weekly", "label": "Semana 23/2026", "start": "2026-05-25", "end": "2026-05-31" },
  "org_unit": null,
  "totals": { "users": 843, "points": 1820400, "missions": 5120 },
  "rows": [
    { "rank": 1, "delta": 2, "email": "maria@loja.com", "points": 4820, "missions": 37, "org_unit": "ou_12" }
  ]
}
Recipients are keyed by email — names and ids are not sent; enrich on your side if needed.

Reliability

Respond fast. Return a 2xx status quickly (before heavy processing) to acknowledge receipt.
Your responseWhat SalesOS does
2xxMarks the delivery completed. No retry.
4xx (400, 401, 422…)Treated as a permanent rejection. No retry (except 429, which respects backoff).
5xx / timeout / connection errorRetried with exponential backoff (2^n minutes, up to 5 attempts), then dead-lettered.
Idempotency. The same event may be delivered more than once (retries). Deduplicate on X-SalesOS-Event-Id — it stays the same across retries.
Failed deliveries land in the Deliveries panel (Dashboard → Integrations → Webhooks) where you can inspect status, attempts and the last error, and resend from the dead-letter queue.