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.
Delivery envelope
Section titled “Delivery envelope”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).
Headers
Section titled “Headers”| 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).
- Read the raw request body bytes (do not re-serialize JSON before verifying).
- Parse
tandv1fromOutboundSync-Signature. - Compute HMAC-SHA256 over
"${t}.${rawBody}"with youroswhsec_…secret. - 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));}Rotate the signing secret
Section titled “Rotate the signing secret”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.