Lead webhook — push leads into your CRM

When a new lead is captured (or re-engaged after 30 days of silence), Wilow POSTs signed JSON to a URL you configure. Use this to drop leads straight into HubSpot, Pipedrive, a Google Sheet, your own CRM — anything that takes a POST.

When to enable it

Enable the webhook if a sales or support tool other than Wilow is the system of record for your leads. Without it you're copy-pasting rows from the admin Leads page into whatever tool runs the pipeline — tolerable for a handful, painful past a few a week.

Skip it if you're happy working directly from the admin Leads page. All lead data is visible and exportable there; the webhook is an integration convenience, not a prerequisite.

Configure

Admin → Inbox → Leads — the webhook lives in the lead-capture settings on that page.

  1. URL — HTTPS only. We reject HTTP and SSRF-suspicious hosts at save time (private IPs, localhost, .internal TLDs).
  2. Secret — click Generate. We show it once; copy it to your destination immediately. If you lose it, regenerate — existing signatures become invalid.
  3. Custom headers (optional) — extra headers the destination needs, typically an API bearer token. Stored encrypted at rest (AES-256-GCM); you won't see the full value again after saving.
  4. Payload template (optional) — see below.
  5. Hit Test. We fire a synthetic lead at your endpoint and show the response code + timing. If it's anything other than 2xx, fix and retry before saving.
  6. Hit Save.

Default request shape

When no template is set, every request looks like this:

{
  "event": "lead.created",
  "tenantId": "…",
  "leadId": "…",
  "email": "visitor@example.com",
  "phone": "+491234567890",
  "conversationId": "…",
  "extractedFromMessageId": "…",
  "createdAt": "2026-04-22T10:15:00.000Z",
  "lastReengagedAt": null
}

Re-engagement events fire "event": "lead.re_engaged" with the same shape; lastReengagedAt carries the re-engagement timestamp.

Every request carries X-Wilow-Signature: sha256=<hex> — HMAC of the raw body using your secret. Verify server-side before trusting the payload.

Payload templates

If your destination expects a specific JSON shape (HubSpot's /crm/v3/objects/contacts endpoint, for example), write a template:

{
  "properties": {
    "email": {{email}},
    "phone": {{phone}},
    "name": {{name}},
    "lead_source": "Wilow widget"
  }
}

Available placeholders: {{event}}, {{tenantId}}, {{leadId}}, {{email}}, {{phone}}, {{name}}, {{conversationId}}, {{extractedFromMessageId}}, {{createdAt}}, {{lastReengagedAt}}. Write placeholders as bare tokens (no surrounding quotes): the renderer JSON-encodes each value, so {{email}} already comes out quoted, and "{{email}}" would double-quote and break the JSON. Missing values render as null. If the rendered result isn't valid JSON, we fall back to the default shape rather than dropping the delivery.

Verifying the signature

See Webhook security for the full reference — signing scheme, headers (X-Wilow-Signature, X-Wilow-Event, X-Wilow-Delivery-Id), verification code in Node / Python / Go / Ruby, raw-body pitfalls per framework, and secret rotation. Implement the receiver against that page, then reuse the same code for the ticket webhook and any future Wilow webhook.

One-liner summary: HMAC-SHA256 of the raw request body, keyed by your secret, hex-encoded, sent as sha256=<hex>. Verify with a constant-time compare. Reject on mismatch with 401.

Pitfalls

  • Turning the feature off preserves the URL — if we disable the lead webhook on your workspace, the URL + secret stay on your config but we stop delivering. Re-enabling turns it on again without re-entering anything.
  • Failures on your endpoint are logged, not retried. A flaky CRM endpoint means missed leads — point us at something reliable. If you need retries, front your CRM with a queue you control.
  • 2xx means delivered. We don't inspect the response body. If your CRM returns 200 but silently dropped the lead, that's invisible to us.
  • Idempotency. Each lead fires once per unique visitor per contact. If we were to retry (we don't), leadId would stay the same — dedupe on that on your side if you ever front the webhook with a queue.