Skip to main content
Version: 2026-05

Webhooks

&Open sends webhook events to notify your application when something happens in your account. Each delivery is signed, so you can verify it came from &Open and was not tampered with in transit.

Headers

Every webhook request includes these headers:

HeaderDescription
AndOpen-Webhook-SignatureHMAC-SHA256 signature of the request, hex-encoded
AndOpen-Webhook-Dispatch-TimestampUnix time, in seconds, when the request was dispatched
AndOpen-Webhook-Event-IdUUID of the event — a stable handle for deduplication and log correlation
AndOpen-Webhook-Event-TypeThe type of event being delivered
AndOpen-Webhook-API-VersionThe API version the payload is serialised in
Content-Typeapplication/json

The payload itself is a flat JSON object in the same shape the API returns for that resource (see the API Reference).

Verifying signatures

Verify every delivery before you act on it:

  1. Read the timestamp from AndOpen-Webhook-Dispatch-Timestamp.
  2. Concatenate the timestamp and the raw request body with a dot: "{timestamp}.{body}". Use the bytes you received — re-serialising the parsed JSON can change the body and break the signature.
  3. Compute the HMAC-SHA256 of that string with your webhook secret.
  4. Compare the result to AndOpen-Webhook-Signature with a constant-time comparison.

Replay protection

The dispatch timestamp gives you stateless replay protection: it stops an attacker who captures a delivery from re-sending it to your endpoint later. After the signature checks out, confirm the timestamp is within an acceptable window of the current time — five minutes is a reasonable default — and reject anything older. Because a valid request stops validating once the window passes, a captured one is useless to a replay attacker.

The timestamp is set fresh on each delivery attempt, so legitimate retries are not rejected as stale.

For stronger guarantees, deduplicate on AndOpen-Webhook-Event-Id: record the IDs you have processed and ignore any you have already seen. The same ID is sent on every retry of a given event.

Code examples

These examples verify the signature and reject stale requests. Send back a 403 when either check fails.

const crypto = require('crypto');
const express = require('express');

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

// Mount the raw body parser so `req.body` is the exact bytes &Open sent.
// Verifying against a re-serialised JSON parse would break the signature.
app.post('/webhooks', express.raw({ type: '*/*' }), (req, res) => {
const timestamp = req.headers['andopen-webhook-dispatch-timestamp'];
const signature = req.headers['andopen-webhook-signature'];
const body = req.body; // Buffer of the raw request body

// Reject stale requests (replay protection)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(403).send('Timestamp too old');
}

// Verify signature
const signedContent = `${timestamp}.${body}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('hex');

const expectedBuf = Buffer.from(expected);
const signatureBuf = Buffer.from(signature);
if (expectedBuf.length !== signatureBuf.length ||
!crypto.timingSafeEqual(expectedBuf, signatureBuf)) {
return res.status(403).send('Invalid signature');
}

// Signature is valid — handle the event.
res.status(200).send();
});