CueDMDocs
Receiving webhooks

Webhooks

Receiving webhooks

Add an endpoint, verify the signature on every request, and understand retries and delivery.

How webhooks work

When something happens in your workspace, such as a DM going out or a link being clicked, CueDM sends a POST request with a JSON body to every webhook subscribed to that event. Your endpoint answers with any 2xx status to confirm it received the event.

Webhooks are available on every plan. A workspace can have up to 10. Each one has its own URL, its own events and its own signing secret.

Add an endpoint

  1. Build or choose the endpoint

    Any public https:// URL works: your own server, a serverless function, or a Zapier or Make catch hook. CueDM won't send to plain http://, to URLs with a username or password in them, or to private and internal addresses.

  2. Add it in CueDM

    In Settings → Integrations, choose Add webhook. Paste the URL, add a label if you like (for example “Zapier → Google Sheets”), and tick the events it should receive.

  3. Save the signing secret

    After saving, CueDM shows the webhook's secret, which starts with whsec_. It is shown only once. Store it where your endpoint can read it, such as CUEDM_WEBHOOK_SECRET. If you lose it, delete the webhook and add it again.

  4. Send a test

    Send test delivers a ping event to that webhook, whatever events it subscribes to. Check that it arrives and that your signature check accepts it. Then, under Deliveries, you'll see the status your endpoint returned.

Webhooks can also be added and removed with the API. That's how Zapier and Make connect on their own.

The request

Every delivery is a POST with these headers:

Content-Typeheader
application/json
User-Agentheader
CueDM-Webhooks/1.0
X-CueDM-Eventheader
The event name, such as dm.sent.
X-CueDM-Deliveryheader
The delivery ID. It is the same as id in the body and stays the same on every retry.
X-CueDM-Timestampheader
When this attempt was sent, in Unix seconds.
X-CueDM-Signatureheader
sha256= followed by the hex HMAC. See Verify the signature.

The body has the same shape for every event:

idstring
The delivery ID. Use it to ignore duplicates.
eventstring
The event name. ping for a test.
workspaceIdstring
The workspace the event happened in.
occurredAtstring
When it happened, ISO 8601 in UTC.
dataobject
The event's details. Each event's fields are in the event reference.

Verify the signature

Anyone who finds your URL can send it a request, so check that each one came from CueDM. The signature is an HMAC-SHA256, keyed with the webhook's secret, of the timestamp, a full stop and the raw request body:

Signed content
X-CueDM-Signature = "sha256=" + hex(HMAC_SHA256(secret, timestamp + "." + body))

To verify a request:

  1. Read the raw body as bytes, before any JSON parser touches it.
  2. Compute the HMAC as above and compare it with the header in constant time.
  3. Reject timestamps more than five minutes from now, so an old request can't be replayed. Each retry is signed again with a new timestamp.
import crypto from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.CUEDM_WEBHOOK_SECRET; // whsec_…

// express.raw: the signature is over the exact bytes sent, so read them
// before anything parses the JSON.
app.post("/webhooks/cuedm", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-CueDM-Timestamp") ?? "";
  const signature = req.get("X-CueDM-Signature") ?? "";
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(`${timestamp}.${req.body}`).digest("hex");

  const valid =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  if (!valid || !fresh) return res.status(400).send("Invalid signature");

  const event = JSON.parse(req.body);
  // Answer first, work after: CueDM waits 10 seconds at most.
  res.sendStatus(200);
  handle(event);
});

The most common mistake

Checking the signature against re-serialised JSON. Frameworks that parse the body first change spacing and key order, and the check then fails on every request. Always sign the raw bytes.

Respond quickly

Return a 2xx within 10 seconds. Anything else counts as a failure: another status, a timeout, a connection error or a redirect. CueDM doesn't follow redirects, so use the final URL.

If the work takes longer, such as calling another API or writing to a slow database, save the event, answer 200, and do the work in the background.

Retries and duplicates

A failed delivery is tried up to five times in total. The retries come 30 seconds, 1 minute, 2 minutes and 4 minutes after each failure. After the fifth failure CueDM stops trying that delivery. You can see it under Deliveries in Settings.

Because of retries, the same event can reach you more than once, for example if your endpoint did the work but timed out before answering. Every attempt has the same id. Keep the IDs you've handled and skip any you see again.

Events are sent as they happen, and retries can land after newer events. Order by occurredAt, not by when the request arrived.

When an endpoint keeps failing

After 20 failed attempts in a row, CueDM turns the webhook off so it stops sending to a URL that's gone. In Settings it shows as Switched off after failures. Fix the endpoint, turn the webhook back on, and use Send test to check it. Events from while it was off aren't sent again.

Before that happens, the webhook shows how many attempts are failing. The Deliveries list shows the last 20 deliveries, with the status code or error for each one.

Security checklist

  • Verify the signature and the timestamp on every request.
  • Keep the secret on the server, never in client-side code or a repository.
  • Treat data as untrusted. It includes what people type in comments and DMs.
  • Emails in contact.email_captured and bio.subscribed are personal data. Only send marketing to people who agreed to it (marketingConsent), and honour bio.unsubscribed.

Stuck, or something here is wrong? Email support@cuedm.com and a person will answer.