Skip to content

Webhook payloads and signatures

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: invalid_grant",
"data": {
"connectionId": 7,
"crm": "HUBSPOT",
"sourceId": 10,
"sourcePlatform": "smartlead",
"reason": "invalid_grant",
"remediation": "Your HubSpot connection appears disconnected or its access was revoked. Reconnect HubSpot in the OutboundSync dashboard: https://app.outboundsync.com/admin/dashboard/hubspot"
}
}

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

HeaderMeaning
OutboundSync-Event-IdEvent id (osevt_…)
OutboundSync-Delivery-IdDelivery attempt id (oswhd_…)
OutboundSync-Signaturet=<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 Dashboard → Webhooks.