Skip to content

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.

EventFires when
account.createdA new account lands in the company (manual, import, lookup, inbound)
account.status_changedAn account actually moves in the pipeline — prospect → contacted, → customer, → do_not_contact, …
message.inboundAn 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.

  1. Open Settings → Webhooks (per company).
  2. Add the destination URL (must be https://), pick the events, save.
  3. Copy the secret immediately — it’s shown once, at creation. If you lose it, rotate it (rotation invalidates the old one).
  4. Hit Test — LeadHunter sends a synthetic webhook.test event through the real delivery pipeline so you can verify your receiver end-to-end before real traffic flows.

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 id is 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.

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 sendcustom_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.

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.

Richer two-way sync (bulk upserts, field-level sync rules) builds on the upsert endpoint above; webhooks + upsert already close the basic loop.