Skip to content

Webhooks (Beta)

Webhook payloads and signatures

Verify OutboundSync webhook deliveries — JSON envelope, OutboundSync-Signature HMAC, the OutboundSync-Event-Id and -Delivery-Id headers, and secret rotation.

When to use this guide: You are implementing the HTTPS handler that receives OutboundSync Webhooks and need the payload shape plus signature verification. For inbound Sources → CRM delivery, see Sources and delivery.

Each delivery is a POST of this JSON body:

{
"id": "osevt_1a2b3c...",
"type": "sync.failed",
"created": "2026-07-09T12:00:00.000Z",
"summary": "Sync smartlead → HubSpot is failing: The CRM connection is missing a valid refresh token. Reconnect the CRM to resume syncing.",
"data": {
"connectionId": 7,
"crm": "HUBSPOT",
"sourceId": 10,
"sourcePlatform": "smartlead",
"consecutiveFailures": 1,
"reason": "The CRM connection is missing a valid refresh token. Reconnect the CRM to resume syncing.",
"remediation": "Your HubSpot connection appears disconnected or its access was revoked. Reconnect HubSpot in the OutboundSync dashboard: https://app.outboundsync.com/admin/dashboard/hubspot — https://outboundsync.com/docs/api/v1#remediation-reconnect"
}
}

Every sync.failed payload pairs reason (what went wrong) with remediation (the exact fix, with a dashboard or docs deep link).

  • summary uses the CRM’s display name (HubSpot, Salesforce); data.crm stays the enum value (HUBSPOT).
  • reason is one readable line. Email addresses are redacted to [email], it’s capped at 240 characters, and it’s never raw JSON.
  • remediation ends with a link to its category in Common sync.failed reasons.
  • consecutiveFailures is the failure count for the source×connection when the alert fired.
Header Meaning
OutboundSync-Event-Id Event id (osevt_…)
OutboundSync-Delivery-Id Delivery attempt id (oswhd_…)
OutboundSync-Signature t=<unix>,v1=<hex> HMAC signature

How to verify OutboundSync webhook signatures

Section titled “How to verify OutboundSync webhook signatures”

The v1 signature is HMAC-SHA256(secret, "<t>.<raw request body>"), hex-encoded. Compare with a constant-time check, and reject timestamps outside a tolerance window (for example 5 minutes).

  1. Read the raw request body bytes (do not re-serialize JSON before verifying).
  2. Parse t and v1 from OutboundSync-Signature.
  3. Compute HMAC-SHA256 over "${t}.${rawBody}" with your oswhsec_… secret.
  4. Reject if the timestamp is stale or the hex digests differ.
const crypto = require('crypto');
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)) <= toleranceSec;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

POST /api/v1/webhooks/:id/rotate-secret (write scope) returns a new secret shown once. Update your verifier before discarding the old secret. Same flow is available under Platform access → Webhooks.