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 → Widget → Lead webhook.

  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": "[email protected]",
  "phone": "+491234567890",
  "conversationId": "…",
  "capturedAt": "2026-04-22T10:15:00.000Z"
}

Re-engagement events fire "event": "lead.re_engaged" with the same shape plus lastConversationAt.

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}}",
    "firstname": "{{firstName}}",
    "lead_source": "Wilow widget"
  }
}

Available placeholders: {{event}}, {{tenantId}}, {{leadId}}, {{email}}, {{phone}}, {{firstName}}, {{conversationId}}, {{capturedAt}}, {{lastConversationAt}}. Missing values render as an empty string. The templating is string-based — you can put placeholders inside strings, but object keys have to be literal.

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.