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
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 plainhttp://, to URLs with a username or password in them, or to private and internal addresses.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.
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 asCUEDM_WEBHOOK_SECRET. If you lose it, delete the webhook and add it again.Send a test
Send test delivers a
pingevent 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-Typeheaderapplication/jsonUser-AgentheaderCueDM-Webhooks/1.0X-CueDM-Eventheader- The event name, such as
dm.sent. X-CueDM-Deliveryheader- The delivery ID. It is the same as
idin the body and stays the same on every retry. X-CueDM-Timestampheader- When this attempt was sent, in Unix seconds.
X-CueDM-Signatureheadersha256=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.
pingfor 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:
X-CueDM-Signature = "sha256=" + hex(HMAC_SHA256(secret, timestamp + "." + body))To verify a request:
- Read the raw body as bytes, before any JSON parser touches it.
- Compute the HMAC as above and compare it with the header in constant time.
- 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
dataas untrusted. It includes what people type in comments and DMs. - Emails in
contact.email_capturedandbio.subscribedare personal data. Only send marketing to people who agreed to it (marketingConsent), and honourbio.unsubscribed.
Stuck, or something here is wrong? Email support@cuedm.com and a person will answer.