Push events to your tools — webhooks
Webhooks push LeadHunter events to any system that can receive an HTTPS POST — your CRM, Zapier, n8n, Make, a Slack relay, your own backend. Deep-link custom fields let you jump into your other tools; webhooks are the other direction: LeadHunter telling your tools what just happened.
What gets pushed
Section titled “What gets pushed”| Event | Fires when |
|---|---|
account.created | A new account lands in the company (manual, import, lookup, inbound) |
account.status_changed | An account actually moves in the pipeline — prospect → contacted, → customer, → do_not_contact, … |
message.inbound | An inbound reply is logged on a conversation |
Every payload carries the account’s core fields including
imported_id — so if the account came from your CRM, the event
arrives already labelled with your record id and matching it on the
receiving side is a lookup, not a fuzzy search. Custom fields ride
along too.
Set one up
Section titled “Set one up”- Open Settings → Webhooks (per company).
- Add the destination URL (must be
https://), pick the events, save. - Copy the secret immediately — it’s shown once, at creation. If you lose it, rotate it (rotation invalidates the old one).
- Hit Test — LeadHunter sends a synthetic
webhook.testevent through the real delivery pipeline so you can verify your receiver end-to-end before real traffic flows.
Verifying the signature
Section titled “Verifying the signature”Every delivery is signed so your receiver can prove it came from
LeadHunter and wasn’t tampered with. The X-LeadHunter-Signature
header carries sha256=<hex> — the HMAC-SHA256 of the raw request
body with your webhook’s secret as the key. Recompute it on your
side and compare:
import hashlib, hmac
expected = "sha256=" + hmac.new( secret.encode(), raw_body, hashlib.sha256).hexdigest()ok = hmac.compare_digest(expected, request.headers["X-LeadHunter-Signature"])Two more headers help with bookkeeping: X-LeadHunter-Event (the
event type) and X-LeadHunter-Delivery (the delivery id).
Delivery semantics — what your receiver should expect
Section titled “Delivery semantics — what your receiver should expect”- At-least-once. Failures are retried with backoff (a 500, a
timeout, a 429). The payload’s top-level
idis stable across retries and redeliveries of the same event — dedupe on it if your handler isn’t naturally idempotent. - Respond fast with a 2xx. Anything else counts as a failure. Do the heavy work async on your side.
- Permanent rejections aren’t retried. A 404/401/410 marks the delivery failed immediately — retrying wouldn’t heal it.
- Dead endpoints switch themselves off. After 20 consecutive failures the webhook is auto-disabled (you’ll see “auto-disabled after repeated failures” on the settings page). Fix the receiver, hit Test, then re-enable — the failure streak resets.
Replaying
Section titled “Replaying”Each webhook’s Recent deliveries panel shows every attempt with its status, response code, and error. Any delivery can be redelivered with one click — same payload, fresh attempt — which is the tool for “our endpoint was down for an hour” recoveries. Delivery history is kept for 90 days; anything older is pruned automatically.
Pushing data back in — upsert by imported id
Section titled “Pushing data back in — upsert by imported id”Webhooks are LeadHunter → your system. The return path is one endpoint:
POST /api/accounts/upsert-by-imported-id/{ "project": "my-company", "imported_id": "crm-4711", "status": "customer", "custom_fields": {"plan": "pro"} }LeadHunter finds the account whose imported_id matches (the same
correlation key the webhook payloads carry) and patches only the
fields you send — custom_fields merge per key, and a null
deletes a key. No match → the account is created. Status changes go
through the normal pipeline, so the audit trail and your other
webhooks fire exactly as if an operator made the change — and pushing
the same status twice is a no-op, so periodic full-state re-syncs
don’t pollute the history. Authenticate with your API token like any
other endpoint.
Together that’s the full loop: a webhook tells your CRM an account
replied; your CRM closes the deal and pushes status: customer back.
Promo-code attribution in the same call
Section titled “Promo-code attribution in the same call”If your channels carry promo codes, add the code the customer used and the conversion attributes itself:
POST /api/accounts/upsert-by-imported-id/{ "project": "my-company", "imported_id": "shop-1042", "name": "New Customer SL", "promo_code": "laia20", "status": "customer", "captured_value": "890.00", "captured_value_currency": "EUR" }The code matches case-insensitively (codes are letters, digits,
hyphens and underscores, up to 64 characters, unique per company) and
the account lands on that code’s channel with its acquisition date
stamped. An unknown code never fails the push — the response includes
promo_code_matched: true|false so your flow can log misses. One
call: attributed, valued, closed.
What’s next
Section titled “What’s next”Richer two-way sync (bulk upserts, field-level sync rules) builds on the upsert endpoint above; webhooks + upsert already close the basic loop.
Read next
Section titled “Read next”- Custom fields → Deep links into external systems — the inbound-click half of integrations.
- Import accounts — where
imported_idcomes from.