Webhooks
X3 can call your server when something happens in a project – a new lead, a status change, a recorded conversion, a decided experiment. Typical uses: send a lead magnet by mail when a Meta Instant Form lead arrives, start a sequence in your mail tool, notify a chat channel.
Endpoints are set up per project in Einstellungen › Webhooks, over REST (/v1/projects/{project}/webhooks)
or with the MCP tools webhooks_*. Managing webhooks needs project:admin.
Events
| Type | When | data |
|---|---|---|
lead.created | a lead was stored (website form, x3.lead(), server API, manual contact, Instant Form) | lead |
lead.updated | name, company, e-mail or phone of a lead changed | lead, changed (field names) |
lead.status_changed | the pipeline status changed | lead, from, to, reasonCode, changedBy |
conversion.created | a conversion was recorded | conversion (definition, source, status, value, lead id) |
experiment.decided | an experiment was decided (rule or manual) | experiment, outcome, appliedArm, rolledOut |
Spam leads are never sent. Each event is stored once per subject: the same lead never produces two
lead.created events.
Payload
{
"type": "lead.created",
"id": "5b0c1e3a-8f0e-4a39-9c67-2a8c1c4f0d11",
"created": "2026-10-25T09:14:03.120Z",
"version": "2026-10-25",
"project": { "id": "…", "name": "Website" },
"test": false,
"data": {
"lead": {
"id": "…",
"createdAt": "2026-10-25T09:14:02.000Z",
"form": "meta_instant_form",
"origin": "meta_instant_form",
"tags": ["leadmagnet:guide"],
"channel": "meta",
"attribution": { "utm_source": "facebook", "utm_medium": "paid_social", "utm_campaign": "Guide" },
"isTest": false,
"meta": { "formId": "…", "formName": "Guide form", "adId": "…", "campaignId": "…" },
"contact": { "email": "ada@example.org", "phone": "+4917012345678", "firstName": "Ada", "lastName": "Lovelace", "company": null, "message": null, "answers": {} }
}
}
}origin:website,meta_instant_form,manualorimport.tags: the lead's tags (an Instant Form can put one on every lead).contactisnullunless the endpoint was created with contact data (includeContact). That needsleads:read– the same personal-data right as reading leads – besidesproject:admin, and every change is in the audit log. Contact fields come from the lead (projects that store contact data in plain text) or from the call that created it.test: truemarks test leads, test sessions and the test button. Never act towards real people on a test event (e.g. mail only your own address).versionchanges only for breaking changes; new fields can appear at any time.
Filters
An endpoint receives an event only when every filter it has matches (one value per list is enough):
| Filter | Matches |
|---|---|
forms | lead.form |
origins | lead.origin, e.g. meta_instant_form |
tags | one of lead.tags, e.g. leadmagnet:guide |
goals | conversion.definition |
includeTest: false | drops test events |
Verify the signature
Every request carries three headers in the Standard Webhooks format:
webhook-id– the event id, the same on every retry and redelivery: store it and ignore repeats.webhook-timestamp– unix seconds of this attempt: reject anything older than 5 minutes (replay protection).webhook-signature–v1,<base64 HMAC-SHA256>of<webhook-id>.<webhook-timestamp>.<raw body>; the key is the base64 part of your secret afterwhsec_.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyX3Webhook(secret: string, headers: Headers, rawBody: string): boolean {
const id = headers.get("webhook-id");
const timestamp = headers.get("webhook-timestamp");
const signature = headers.get("webhook-signature");
if (!id || !timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
return signature.split(" ").some((part) => {
const given = Buffer.from(part.split(",")[1] ?? "", "base64");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}Verify the raw body before parsing it. The secret is shown once when the endpoint is created (or after „Neues Secret“); keep it in your secret store.
Delivery and retries
- Answer with any
2xxwithin 10 seconds. Do slow work after answering. - Anything else is retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h and 13 h – 8 attempts over about 24
hours.
410 Gonestops at once. - After 20 deliveries in a row that used all attempts, X3 switches the endpoint off and shows why in the dashboard. Switching it on again resets the counter; waiting deliveries then go out.
- The delivery log (
GET /v1/projects/{project}/webhook-deliveries, MCPwebhooks_deliveries) shows status, response code, duration, next retry and the last attempts. „Erneut senden“ (POST …/webhook-deliveries/{id}/redeliver) sends the same event again now. - „Test senden“ (
POST /v1/projects/{project}/webhooks/{id}/test) sends a test event with a fictional contact, regardless of filters.
Security
- In production only
httpsURLs with a public host are accepted; X3 resolves the host before each request, refuses private, loopback and link-local addresses and never follows redirects. - Payloads are stored encrypted and cleared after 30 days; the log is kept 90 days. Erasing a lead removes its events.
- At most 10 endpoints per project.
- A webhook that an AI connection sets up (MCP
webhooks_create, or a key made through such a connection) stays switched off withawaitingConfirmation: trueuntil someone switches it on in Einstellungen › Webhooks („Freigeben und einschalten“). The same happens when an AI connection gives an endpoint a new address or switches contact data on. Text in lead and visit data comes from website visitors, so a chat must never be able to send future leads to a new place on its own. Test events (fictional data) work before the confirmation.
REST
| Route | Purpose |
|---|---|
GET /v1/projects/{project}/webhooks | list endpoints |
POST /v1/projects/{project}/webhooks | create (url, events, filters, includeContact, description) – returns secret once |
PATCH /v1/projects/{project}/webhooks/{id} | change, switch on/off |
DELETE /v1/projects/{project}/webhooks/{id} | delete |
POST /v1/projects/{project}/webhooks/{id}/secret | new secret |
POST /v1/projects/{project}/webhooks/{id}/test | test event |
GET /v1/projects/{project}/webhook-deliveries | delivery log (endpoint, status, event, cursor) |
POST /v1/projects/{project}/webhook-deliveries/{id}/redeliver | send again |
Sales Engine events
The Sales Engine is a product of its own: its events (contacts, tasks, deals, calls, bookings) go to endpoints of the
organization that you manage in the Sales Engine under Einstellungen › API & Webhooks or through its own API
(https://api.crm.x3growth.io). Signature and retries follow the same Standard Webhooks rules as above.