Receive webhooks

Three rules make a reliable receiver: answer fast, verify the signature, process async. Here is the shape of it.

1. A receiver that answers fast

app.js (Express)
app.post('/hooks/camelmailer', express.raw({ type: 'application/json' }), (req, res) => {
  // 1. verify the signature against the raw body (below)
  // 2. enqueue for processing — do NOT do the work here
  queue.push(JSON.parse(req.body));
  res.status(200).end();
});

Failed deliveries are retried with backoff, so a handler that takes seconds or 500s under load creates duplicate work for itself. Accept, enqueue, return.

2. Verify the signature

Payloads are RSA-signed with the installation key. Fetch the public key once from the dashboard (server settings), then:

const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(rawBody);
const ok = verifier.verify(publicKeyPem, signatureFromHeader, 'base64');
if (!ok) return res.status(400).end();
  • Verify against the raw request body, because re-encoding the parsed JSON breaks signatures.
  • Reject unsigned or badly signed requests loudly (400 + alert).

3. Process idempotently

Retries mean at-least-once delivery. Key your processing on the message id + event type so a replay is a no-op.

Debugging

  • The dashboard's webhook request log shows every attempt with request and response. Start there before your own logs.
  • Local development: expose your receiver with a tunnel (ngrok, cloudflared) and point a dev webhook at it; disable it afterwards (POST …/webhooks/{id}/disable).
Event catalogue and creation API: Webhooks and the management reference.