How Webhooks work

This is more of a technical article but in short, Commslayer webhooks deliver real-time event notifications to your server over HTTPS.

Example use cases

  • Sync tickets to your CRM: Listen for conversation_created and conversation_updated events to automatically create and update records in Salesforce, HubSpot, or any CRM, keeping your sales team in the loop without manual data entry.
  • Trigger order actions from customer replies: Use message_created events to detect when a customer confirms a cancellation or address change, then call your fulfillment API to process it instantly.
  • Feed support data into your analytics pipeline: Stream every event to a data warehouse like BigQuery or Snowflake to build custom dashboards, track resolution times, or run sentiment analysis across all customer interactions.

Every delivery is signed with HMAC-SHA256 using a shared secret, includes a timestamp for replay protection, and carries a stable delivery_id for deduplication. Failed deliveries are retried up to 20 times with exponential backoff, and a circuit breaker protects both sides during extended outages. You can test any event type on demand, inspect delivery logs, and redeliver missed events through the API.

1. Signature header name

X-Commslayer-Signature, value format sha256=<signature>. Every delivery also carries X-Commslayer-Timestamp, X-Commslayer-Delivery-ID, and X-Commslayer-Event. Read header names case-insensitively.

2. HMAC/hash algorithm

HMAC-SHA256. The key is your webhook's secret - a 64-character hex string returned once in the POST /api/integration/v1/webhooks create response, and again only from POST .../webhooks/:id/rotate_secret.

3. Exact bytes that are signed

"{timestamp}.{raw_request_body}" - the Unix-seconds timestamp string, a literal dot, then the request body exactly as received. Always verify against the raw bytes; never re-serialize the parsed JSON.

4. Signature encoding

Lowercase hex, prefixed with sha256=.

5. Timestamp header and replay window

X-Commslayer-Timestamp, Unix epoch seconds. We don't enforce a window server-side - reject anything more than 5 minutes from your clock. Every retry and redelivery is re-signed with a fresh timestamp, so the tight window never conflicts with our retry schedule. Replay protection = valid signature + fresh timestamp + dedupe on delivery_id from the payload (see 7); the payload copy is covered by the signature, so it can't be forged or swapped.

6. Retry schedule and timeout

10-second timeout per attempt; success is any 2xx - return it before doing heavy processing. On failure: up to 20 attempts with exponential backoff (5s, 10s, 20s, doubling, capped at 1 hour), spanning roughly 10.5 hours. Sustained failures trip a circuit breaker that pauses deliveries for up to 1 hour; anything dropped during a pause appears in GET .../webhooks/:id/delivery_logs with status skipped and can be resent via POST .../webhooks/:id/delivery_logs/:log_id/redeliver. Roughly a day of continuous failure disables the webhook and emails your account admins; recover with POST .../reset_circuit_breaker or by re-enabling the webhook, then redeliver what you missed.

7. Unique delivery ID

In two places with the same value: a top-level delivery_id field inside the payload (covered by the signature - use this as your dedupe key) and the X-Commslayer-Delivery-ID header (convenience copy). It's stable across all retries and manual redeliveries of the same delivery. The event type is in the payload's event field and the X-Commslayer-Event header. Duplicates and out-of-order arrival are both possible - dedupe on delivery_id and don't assume ordering.

8. Example payload and verification code

POST /api/integration/v1/webhooks/:id/test sends a realistic sample of any event type through the production delivery path - identical headers, signing, and payload structure to real events, delivered once with no retries. A message_created payload looks like:

{
  "event": "message_created",
  "id": 999001,
  "content": "Hello, I need help with my recent order #12345...",
  "content_type": "text",
  "message_type": "incoming",
  "private": false,
  "created_at": "2026-08-10T13:47:57Z",
  "source_id": null,
  "conversation": { "id": 1001, "status": "open", "...": "..." },
  "inbox": { "id": 1, "name": "Support Inbox" },
  "sender": { "...": "..." },
  "account": { "id": 1, "name": "..." },
  "attachments": [],
  "delivery_id": "7424357d-9166-4f47-98c7-9b39af82f0a1"
}

Verification (Node):

const crypto = require('crypto');

function verifyWebhook(rawBody, headers, secret) {
  const ts = headers['x-commslayer-timestamp'];
  const sig = headers['x-commslayer-signature']; // "sha256=<hex>"
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  if (expected.length !== sig.length) return false;
  const authentic = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
  return authentic && fresh;
}

// After verification, dedupe on JSON.parse(rawBody).delivery_id

Two integration notes: parse payloads permissively (we may add fields over time - don't reject unknown properties), and monitor your webhook's health via GET /api/integration/v1/webhooks/:id, which exposes healthy, active, and circuit-breaker state.