> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useorbitcrm.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Security

> Verify webhook signatures to ensure requests are genuinely from Orbit and haven't been tampered with, and guard against replays and duplicates.

## Signature header

Every delivery carries an HMAC-SHA256 of the raw request body, keyed with the webhook's secret:

```
X-Orbit-Signature: sha256=abc123def456...
```

The other headers on a delivery are listed in the [overview](https://docs.useorbitcrm.ai/developers/webhooks/overview.md#headers). Two matter for security: `X-Orbit-Timestamp` (when the event happened) and `X-Orbit-Event-ID` (stable across retries of the same event).

## Verification example

Here's how to verify the signature in Node.js:

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
  
  const expected = `sha256=${expectedSignature}`;
  if (!signature || signature.length !== expected.length) return false;
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Usage in Express — the signature is computed over the raw request body,
// so mount express.raw() for this route (express.json() would re-serialize
// the body and break verification)
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-orbit-signature'];
  const isValid = verifyWebhookSignature(
    req.body.toString('utf8'),
    signature,
    process.env.WEBHOOK_SECRET
  );
  
  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Replays: refuse deliveries whose event is older than you are willing to accept
  const occurredAt = Date.parse(req.headers['x-orbit-timestamp']);
  if (Number.isNaN(occurredAt) || Date.now() - occurredAt > 10 * 60 * 1000) {
    return res.status(401).send('Stale delivery');
  }

  // Duplicates: a retry carries the same X-Orbit-Event-ID
  const eventId = req.headers['x-orbit-event-id'];
  if (alreadyProcessed(eventId)) {
    return res.status(200).send('OK');
  }
  
  // Process webhook...
  res.status(200).send('OK');
});
```

> [!WARNING]
>
> **Important**
>
> Always verify the signature before processing webhook data. Never trust webhook payloads without verification.

Deliveries that failed are retried for up to about 20 minutes, so a replay window of 10 minutes rejects a retry the sender legitimately delayed. Use 30 minutes if your endpoint is often down for a while, or skip the timestamp check and rely on de-duplicating `X-Orbit-Event-ID`.

## Webhook secret

Your webhook secret is shown once, when the webhook is created, in the dashboard under **Integrations → Webhooks** or in the response of `POST /api/v1/webhooks`.

To rotate it without missing a delivery, call `POST /api/v1/webhooks/:id/rotate-secret`. The response carries the new secret (once) and `previous_secret_expires_at`, 24 hours later. Until then every delivery carries two signatures: `X-Orbit-Signature` with the new secret and `X-Orbit-Signature-Previous` with the old one. Verify against whichever secret you hold, store the new one, and the previous header stops after the grace period. If you suspect the secret has been compromised, rotate it and switch immediately: once you hold the new secret, treat a delivery that only the old one verifies as unverified.

* Store the secret in environment variables
* Never commit secrets to version control
* Rotate secrets periodically for security

## What is never sent

A delivery never carries a submitter's IP address, a call's Twilio recording URL, or a call transcript's text. Fetch recordings and transcripts with the [Dialer API](https://docs.useorbitcrm.ai/developers/api/dialer.md) using a credential with `dialer:read`.

[Next: Embed Script →](https://docs.useorbitcrm.ai/developers/embed.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.