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

TypeWhendata
lead.createda lead was stored (website form, x3.lead(), server API, manual contact, Instant Form)lead
lead.updatedname, company, e-mail or phone of a lead changedlead, changed (field names)
lead.status_changedthe pipeline status changedlead, from, to, reasonCode, changedBy
conversion.createda conversion was recordedconversion (definition, source, status, value, lead id)
experiment.decidedan 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, manual or import. tags: the lead's tags (an Instant Form can put one on every lead).
  • contact is null unless the endpoint was created with contact data (includeContact). That needs leads:read – the same personal-data right as reading leads – besides project: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: true marks test leads, test sessions and the test button. Never act towards real people on a test event (e.g. mail only your own address).
  • version changes 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):

FilterMatches
formslead.form
originslead.origin, e.g. meta_instant_form
tagsone of lead.tags, e.g. leadmagnet:guide
goalsconversion.definition
includeTest: falsedrops 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 after whsec_.
verify.ts
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 2xx within 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 Gone stops 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, MCP webhooks_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 https URLs 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 with awaitingConfirmation: true until 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

RoutePurpose
GET /v1/projects/{project}/webhookslist endpoints
POST /v1/projects/{project}/webhookscreate (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}/secretnew secret
POST /v1/projects/{project}/webhooks/{id}/testtest event
GET /v1/projects/{project}/webhook-deliveriesdelivery log (endpoint, status, event, cursor)
POST /v1/projects/{project}/webhook-deliveries/{id}/redeliversend 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.