Campaign webhooks
Send a campaign's details and recipient list to your own server, or to a tool like n8n, Pipedream or IFTTT, when you launch the campaign. Zidi signs each request, so your server can check that it came from Zidi.
Available on: Starter and Pro
Each campaign has its own webhook. It sends the campaign once, at launch, and again whenever you click Send Webhook. It does not send views or clicks. For those, use Zapier or Make, which need Pro.
Set up a campaign webhook
You set the webhook while the campaign is still a draft. After launch, you cannot change it.
- In the sidebar, click Campaigns.
- Open a campaign that is still in Draft.
- Find the Custom Webhook card.
- In Webhook URL (HTTPS), enter your endpoint, for example
https://your-endpoint.example.com/webhook. - Turn on Fire webhook on launch.
- Decide whether to turn on Include recipient PII. See Include recipient PII below.
- Click Save Webhook. You see "Webhook settings saved".
- Give your endpoint your signing secret, so it can check each request. See Verify that a request came from Zidi.
- Launch the campaign. Zidi sends the webhook as part of the launch.
The URL must use HTTPS and must be a public address. Local and private network addresses are refused. If you save a URL but leave Fire webhook on launch off, nothing is sent at launch. You can still send it later with Send Webhook.
Send the webhook again
After launch, the campaign page shows a Send Webhook button if the campaign has a webhook URL. Click it to send the campaign and its current recipient list again. You see "Webhook sent".
That message means Zidi queued the request. It does not confirm that your server accepted it. Check your server's logs to confirm.
You cannot change the URL after launch. To use a different URL, set it on a new draft campaign.
What Zidi sends
Zidi sends a JSON POST request to your URL.
| Field | Contents |
|---|---|
event | campaign.launched at launch, campaign.manual_trigger from Send Webhook |
timestamp | When Zidi built the request. It stays the same on retries. |
campaign | id, name, status, launchedAt, videoId |
recipients | One entry per recipient |
totalRecipients | The number of recipients |
{
"event": "campaign.launched",
"timestamp": "2026-09-24T10:00:00.000Z",
"campaign": {
"id": "…",
"name": "Q3 outreach",
"status": "ACTIVE",
"launchedAt": "2026-09-24T10:00:00.000Z",
"videoId": "…"
},
"recipients": [
{ "recipientHash": "3f9a1c0b7d2e4f61" }
],
"totalRecipients": 1
}Headers
| Header | Contents |
|---|---|
Content-Type | application/json |
X-Zidi-Signature | The signature of the request body |
X-Zidi-Signature-V2 | A signature that also covers the timestamp |
X-Zidi-Timestamp | When this attempt was sent. New on every retry. |
X-Zidi-Delivery-Id | An ID for the delivery. The same on every retry. |
Zidi waits up to 30 seconds for your server to answer. If your server does not answer in time, or returns a 5xx error, 408 or 429, Zidi tries again. Zidi does not retry other error answers, and it does not follow redirects, so use your endpoint's final address.
Include recipient PII
This toggle decides whether the request identifies each recipient.
| Toggle | Each recipient entry contains |
|---|---|
| Off (default) | recipientHash: a 16-character ID, the same for that recipient every time |
| On | email, personalizedUrl (their personal campaign link) and tokens (the values from your recipient list, such as name and company) |
Turn it on only if your endpoint needs to contact recipients directly. Like the URL, you can only change it while the campaign is in Draft.
The same toggle also decides whether Zapier and Make receive the recipient's email, name and company, and whether Slack shows the recipient's email, for this campaign.
Verify that a request came from Zidi
Anyone who learns your webhook URL can send requests to it. Check the signature on each request before you act on it. You need your signing secret for this.
Find your signing secret
- In the sidebar, click Campaigns and open a campaign.
- On the Custom Webhook card, find Signing secret.
- Click Reveal. The button reads Revealing... while the secret loads.
- Click Copy. You see "Signing secret copied".
Click Hide to cover the secret again. You can also select the secret in its box and copy it yourself.
On a draft, the Signing secret row is below Save Webhook. On a launched campaign that has a webhook URL, a smaller Custom Webhook card shows only the signing secret. The Free plan has no Custom Webhook card.
You have one signing secret. It is the same for all your campaigns, and it also signs the requests Zidi sends to Zapier. Keep it private, like a password.
How the signatures are made
- Both signatures use HMAC-SHA256 and are written as 64 lowercase hexadecimal characters.
- The key is the 64-character signing secret exactly as shown, used as text. Do not decode it from hexadecimal first. That gives a different key, and no signature matches.
X-Zidi-Signatureis the signature of the raw request body.X-Zidi-Signature-V2has the formt=<timestamp>,v1=<signature>. The timestamp is the same value asX-Zidi-Timestamp. The signature covers the timestamp, a dot, and then the raw request body.X-Zidi-Timestampis a date and time in ISO 8601 format, in UTC with milliseconds, for example2026-09-25T10:00:03.123Z. It is not a number of seconds.
Check a request in Node.js
Use the request body exactly as it arrived. If your framework turns the JSON into an object first, read the raw body for this route instead. JSON that is parsed and written out again may not match the signature.
import { createHmac, timingSafeEqual } from "node:crypto";
// secret: your 64-character signing secret, used as text.
// rawBody: the request body exactly as it arrived, as a string.
function hmacHex(secret, text) {
return createHmac("sha256", secret).update(text).digest("hex");
}
function sameValue(expected, received) {
const a = Buffer.from(expected);
const b = Buffer.from(String(received ?? ""));
return a.length === b.length && timingSafeEqual(a, b);
}
// Checks the X-Zidi-Signature header.
function verifySignature(rawBody, signatureHeader, secret) {
return sameValue(hmacHex(secret, rawBody), signatureHeader);
}
// Checks the X-Zidi-Signature-V2 header, and rejects a request
// whose timestamp is more than 5 minutes from now.
function verifySignatureV2(rawBody, signatureV2Header, secret) {
const fields = Object.fromEntries(
String(signatureV2Header ?? "").split(",").map((part) => part.split("=")),
);
const sentAt = Date.parse(fields.t);
if (!fields.v1 || Number.isNaN(sentAt)) return false;
if (Math.abs(Date.now() - sentAt) > 5 * 60 * 1000) return false;
return sameValue(hmacHex(secret, fields.t + "." + rawBody), fields.v1);
}verifySignatureV2 is the stronger check, because it also rejects old requests that someone sends again. Five minutes is an example. Choose the window that suits you. If a check fails, answer with an error such as 401. Zidi does not retry that answer.
Retries and duplicate requests
- Every attempt, including a retry, carries a new
X-Zidi-Timestampand a newX-Zidi-Signature-V2. A late retry still passes a 5-minute check. X-Zidi-Delivery-Idstays the same on every attempt of one delivery. Zidi can send the same delivery more than once, so save the ID and skip a delivery you have already handled.- The
timestampfield in the body is when Zidi built the request. On a retry, it is earlier thanX-Zidi-Timestamp, so do not compare the two.
If someone else has your secret
If you think someone else has your signing secret, email support@zidi.ai.
Troubleshooting
| Message | What it means |
|---|---|
| Webhooks require a Starter or Pro plan | Free does not include webhooks. See Plans. |
| Webhook URL must use HTTPS | Use an https:// address. |
| Webhook URL cannot point to a private or internal address | Use a public address, not localhost or a private network. |
| Webhook URL is too long (max 2000 characters) | Shorten the URL. |
| Invalid webhook URL | Check the URL for typos. |
| Can only configure webhook URL/enabled for DRAFT campaigns | The campaign was launched. Set up the webhook on a new draft. |
| Campaign must be launched first | Send Webhook works only after launch. |
| No webhook URL configured | The campaign was launched without a webhook URL. |
| The signing secret could not be loaded. Please try again. | Click Reveal again. If it keeps failing, reload the page. |
| Could not copy. Select the secret and copy it manually. | Your browser blocked copying. Select the secret in its box and copy it yourself. |
If signatures never match, check three things: you use the secret as text, you sign the raw body, and you compare the result as lowercase hexadecimal.
If you cannot see the Custom Webhook card, check that your account is on Starter or Pro. On a launched campaign, the card appears only if the campaign has a webhook URL, and it shows only the signing secret.
Related