Receive webhooks

Point CamelMailer at your URL from the dashboard, then run a receiver that answers fast, verifies the signature, and does its work asynchronously.

1. Add the endpoint in the dashboard

Open your mail server and go to Server → Webhooks, then choose New webhook. Give it a name, paste the URL that receives the callback, and pick the events it should get. Leaving every event unchecked subscribes to all of them. No API call is involved.

  • Events. CamelMailer fires MessageSent, MessageDelayed, MessageDeliveryFailed, and MessageHeld. The Webhooks reference explains what each one means and shows the payload.
  • Signing is on by default, so every delivery arrives with an RSA signature you verify below. Keep it on.
  • Custom headers ride on every request, which is where a shared Authorization bearer token goes if your receiver expects one.

After saving, use Send test on the webhook row to deliver one sample payload straight away, with your headers and signature, marked test: true. The Enabled toggle pauses and resumes delivery while keeping the configuration.

2. 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, and 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.

3. Verify the signature

Deliveries are RSA-signed with the installation signing key, the same key CamelMailer uses for DKIM. Export its public half once and hand it to your receiver:

terminal
openssl rsa -in /path/to/signing.key -pubout -out camelmailer-webhooks.pub

Then check every request against the exact bytes you received, before you parse them:

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 changes the bytes and breaks the signature.
  • The signature arrives in X-CamelMailer-Signature as standard base64. Reject unsigned or badly signed requests loudly (400 plus an alert).

4. Process idempotently

Retries mean at-least-once delivery. The per-delivery uuid is stable across retries of one event, so key your processing on it and a replay becomes a no-op.

Debugging

  • The dashboard's webhook request log (Server → Webhooks → Request log) shows every attempt with its request and response. Start there before your own logs.
  • Local development: expose your receiver with a tunnel (ngrok, cloudflared) and point a webhook at it, then flip its Enabled toggle off afterwards.

Same endpoint over the API

To script it instead, create the same webhook through the management API. The dashboard and the API write the identical resource.

terminal
curl -X POST \
  https://mail-admin.example.com/api/v2/admin/organizations/acme/servers/production/webhooks \
  -H "X-Admin-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "delivery-events",
        "url": "https://app.example.com/hooks/camelmailer",
        "events": ["MessageSent", "MessageDeliveryFailed"],
        "sign": true
      }'
The full event catalogue, payload shape, signature algorithm, and retry behaviour live in the Webhooks reference.