Webhooks & EventsReference

Verifying Webhook Signatures

Authenticate deliveries with the raw request body, HMAC-SHA256, and constant-time comparison.

9 min readUpdated 25 Sept 20264 guides in this section

Treat every webhook as untrusted input. Verify the signature before parsing or acting on the payload, reject stale timestamps, and use the raw request bytes for verification.

Delivery headers

HeaderPurpose
X-OP26X-SignatureHex-encoded HMAC-SHA256 digest of the signed payload
X-OP26X-TimestampDelivery timestamp used for replay protection
X-OP26X-DeliveryUnique delivery identifier for deduplication and tracing

Express verification example

typescript
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.OP26X_WEBHOOK_SECRET;

app.post('/webhooks/op26x', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.get('x-op26x-signature');
  const timestamp = req.get('x-op26x-timestamp');
  const deliveryId = req.get('x-op26x-delivery');

  if (!signature || !timestamp || !deliveryId || !secret) {
    return res.sendStatus(400);
  }

  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > 300) {
    return res.sendStatus(401);
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');

  const expectedBuffer = Buffer.from(expected, 'utf8');
  const receivedBuffer = Buffer.from(signature, 'utf8');

  if (
    expectedBuffer.length !== receivedBuffer.length ||
    !crypto.timingSafeEqual(expectedBuffer, receivedBuffer)
  ) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  enqueueForProcessing(deliveryId, event);
  return res.sendStatus(202);
});

Response behavior

  • Return a 2xx response only after the delivery has been accepted durably.
  • Prefer queueing the work and acknowledging quickly rather than blocking the request.
  • Return a 400 response for malformed requests and a 401 response for failed authentication.
  • Deduplicate by delivery ID because webhook delivery can be at-least-once.
  • Never log signing secrets, authorization headers, or full sensitive payloads.

Secret rotation

  1. 01

    Create the replacement secret in the workspace

    Store the new value in the integration secret manager rather than source code.

  2. 02

    Accept both values during rollout

    Select the secret by the configured key identifier or deployment configuration.

  3. 03

    Verify the new secret

    Send a signed test delivery and confirm the new path succeeds.

  4. 04

    Revoke the old secret

    Remove the previous credential after all senders use the replacement.

Need implementation help?

Search the help center or talk with the OP26X telephony team.