Webhooks
Receive published leads in your CRM or application.
Overview
Whenever Leadpoet publishes a new lead for the connected account, we send a signed HTTPS POST to an endpoint you host. Your endpoint verifies the signature, checks it hasn’t seen the lead before, and creates one record in your CRM.
Leadpoet publishes a lead
→ POST (signed JSON) to your HTTPS endpoint
→ you verify the signature and deduplicate
→ you create one record
→ you return 2xxYou build and host one endpoint. Leadpoet handles signed delivery and retries. These events report published leads; they are not completion notifications for every workspace API operation. See the API guide for request polling and workflow automation.
Set up your destination
- 1Open Settings. Find the Developer row and select Manage.
- 2Select Custom Webhook, then choose Add destination.
- 3Enter a name and endpoint URL. The endpoint must be public HTTPS on port 443. You can also add an optional account reference if one receiver handles several accounts.
- 4Authorize delivery and add the destination. Leadpoet will show its signing secret once.
- 5Copy and save the signing secret immediately. Store the full
lpwhsec_…value in your secret manager; it cannot be viewed again.
Managing destinations
Available controls depend on your workspace permissions. If you have view-only access, ask the person who manages your workspace to update the destination.
To include Why this account in each lead, complete the payload format setup below before testing and activating your destination.
Payload format and API setup
Select 2026-09-11 to include data.lead.icp_details. Connections created in settings use 2026-07-01, which omits this field. Existing connections keep their selected format. Change it through the API below with permission to manage integrations, or contact Leadpoet for help.
First, update your receiver to accept this version and save data.lead.icp_details as Why this account / Intent Details. It may be null. Keep signature verification and lead deduplication enabled.
Make the API calls below to https://leadpoet.com with Authorization: Bearer <LEADPOET_SESSION_ACCESS_TOKEN> from a signed-in account with permission to manage integrations. Use Content-Type: application/json for request bodies. These management endpoints use a Leadpoet session access token; your webhook signing secret and Leadpoet API key are not accepted. Keep the session token private.
- Find your connection with
GET /api/outbound-webhook-destinations. Use itsidas:idbelow and check itspayload_version. - Pause the connection with
POST /api/outbound-webhook-destinations/:id/pause. Wait for any in-flight delivery to finish. A version change returns409while a delivery is still in flight. - Send the version update shown below with
PATCH /api/outbound-webhook-destinations/:id. - Run
POST /api/outbound-webhook-destinations/:id/test. Your receiver must verify the signedendpoint.test, confirmapi_version: "2026-09-11", and return2xx. Continue only when the test response reportsok: true. - Resume with
POST /api/outbound-webhook-destinations/:id/resume. You can also use the Test and Resume controls in settings after changing the version.
{ "payload_version": "2026-09-11" }Changing format clears the previous verification. Queued deliveries from the old configuration become skipped with configuration_superseded; they are not automatically converted or resent. Leads published while paused are not queued. Arrange the change between lead runs and review Past deliveries for any recovery needed.
For a new connection created through POST /api/outbound-webhook-destinations, include payload_version: "2026-09-11" with the other connection fields, then test and activate. Omitting it uses 2026-07-01.
Your endpoint
- Public HTTPS on port 443, with a valid, publicly-trusted TLS certificate.
- Read and keep the raw request body exactly as received — you need the original bytes to verify the signature. Do not parse and re-serialize before verifying.
- Respond with a 2xx status (we recommend 204) within 5 seconds, after durably accepting the event. Queue slower CRM work and monitor that queue; do not acknowledge before saving the event.
Verify the signature
Every request carries the headers below. Verify each one with the signing secret Leadpoet shows when you add the destination (the full lpwhsec_… string):
- 1Read the raw body and the
X-Leadpoet-Webhook-Timestamp,X-Leadpoet-Event-Id, andX-Leadpoet-Webhook-Signatureheaders. - 2Reject the request if the timestamp is more than 5 minutes from your clock.
- 3Build the signed message:
"<timestamp>.<event_id>."followed by the raw body bytes (note the two literal dots). - 4Compute
base64url( HMAC_SHA256( secret, signed_message ) ). - 5Compare it, in constant time, to the value after the
v1=prefix. Reject on mismatch.
Any language with an HMAC-SHA256 library works. Node.js reference:
const crypto = require("crypto");
function isValid(rawBody, headers, signingSecret) {
if (!Buffer.isBuffer(rawBody) || !headers ||
typeof signingSecret !== "string" || !signingSecret) return false;
const ts = headers["x-leadpoet-webhook-timestamp"];
const eventId = headers["x-leadpoet-event-id"];
const header = headers["x-leadpoet-webhook-signature"];
// Reject missing/duplicate headers and unsupported signature formats.
if (typeof ts !== "string" || !/^[0-9]+$/.test(ts) ||
typeof eventId !== "string" || !eventId.trim() ||
typeof header !== "string" || !/^v1=[A-Za-z0-9_-]{43}$/.test(header)) return false;
// Reject stale timestamps (±5 minutes)
const timestamp = Number(ts);
if (!Number.isSafeInteger(timestamp) ||
Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) return false;
const signed = Buffer.concat([Buffer.from(`${ts}.${eventId}.`), rawBody]);
const expected = crypto.createHmac("sha256", signingSecret).update(signed).digest();
const provided = Buffer.from(header.slice(3), "base64url");
return provided.length === expected.length &&
crypto.timingSafeEqual(provided, expected);
}| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Leadpoet-Webhooks/1.0 |
X-Leadpoet-Event-Id | Event UUID — stable across retries; your primary dedup key |
X-Leadpoet-Event-Type | lead.published, lead.replied, lead.opted_out, lead.meeting_booked (or endpoint.test) |
X-Leadpoet-Webhook-Timestamp | Unix time in seconds, at send |
X-Leadpoet-Webhook-Signature | v1=<base64url HMAC-SHA256> |
X-Leadpoet-Delivery-Attempt | Attempt number, increments on retry |
No credentials, tenant IDs, emails, or phone numbers are ever placed in headers. Keep the signing secret in your secret manager and never log it.
The payload
Every documented field for the selected version is always present. When a value isn’t available it is null (never omitted), so your field mapping stays stable.
{
"object": "event",
"id": "5e3da3f2-92fd-4377-a7bd-b21895985e99",
"type": "lead.published",
"api_version": "2026-09-11",
"created_at": "2026-07-23T18:00:00.000Z",
"account_reference": "sales-workspace",
"data": {
"lead": {
"object": "lead",
"id": "5903ff3f-e9aa-42d8-a939-bf31ca5684e8",
"request_id": "97438d9b-ebaa-46ae-a6d0-d399a58afec7",
"published_at": "2026-07-23T17:59:58.000Z",
"name": "Jane Rivera",
"email": "jane@example.com",
"phone": "+12125550123",
"role": "VP of Operations",
"linkedin_url": "https://www.linkedin.com/in/example",
"company": "Example Company",
"company_website": "https://example.com",
"company_linkedin_url": "https://www.linkedin.com/company/example",
"industry": "Financial Services",
"sub_industry": null,
"city": "New York",
"state": "NY",
"country": "US",
"hq_state": "NY",
"hq_country": "US",
"employee_count": "51-200",
"description": "Provides financial operations software",
"intent_score": 90,
"contact_source": "deepline",
"enriched_at": "2026-07-23T17:59:40.000Z",
"icp_details": "The company announced a finance team expansion, matching the published account criteria."
}
}
}data.lead.icp_details is the published Why this account / Intent Details text used by the English pipeline export. Localized exports may display a translation. Missing or blank intent is null; Leadpoet does not invent replacement evidence.
description is the company overview and intent_score is a separate optional number. This example uses 2026-09-11. To receive icp_details, follow the payload format setup.
A few things to respect when mapping
- •
phonehas no verification status in this version — don’t label it verified. - • There is no postal or street address in this event.
- • Any nullable field (
sub_industry,phone,linkedin_url, …) may arrive asnull.
Create records without duplicates
Store data.lead.id in a dedicated field. Enforce a unique event and lead identity in durable storage before creating a record; a separate lookup and insert can race when deliveries overlap. Create new records only — do not update or merge based on fuzzy identity. Use Leadpoet Lead ID (data.lead.id) as your durable dedup key. A lead.replied, lead.opted_out or lead.meeting_booked event can carry the same lead ID as an earlier lead.published, so dedupe leads per event type. Match it to your record by that exact ID, and create the record when you have none.
| Your CRM field | Source |
|---|---|
| Leadpoet Lead ID · dedup key | data.lead.id |
| Leadpoet Event ID | id |
| Published At | data.lead.published_at |
| Name · Email · Phone · Role | data.lead.* |
| LinkedIn · Company · Website · Company LinkedIn | data.lead.* |
| Industry · City · State · Country · Employee Count | data.lead.* |
| Company Description | data.lead.description |
| Why this account / Intent Details | data.lead.icp_details |
| Intent Score | data.lead.intent_score |
Responses
200–299Accepted. Done — return 204.408 · 425 · 429 · 5xx · timeoutRetried with backoff429 / 503 + Retry-AfterHonored when between 30s and 6h400 · 405Marked failed, not retried401 · 403 · 404 · 410 · 413 · 415 · 422Not retried — delivery pauses until fixedAfter repeated authorization or validation errors Leadpoet pauses the destination. Existing queued attempts wait for the destination to resume; leads published while it remains paused are not queued.
Delivery guarantees
Delivery is at-least-once.
A network timeout is ambiguous — you may have accepted an event whose response we never received — so you can receive the same event more than once. Deduplication is how one lead becomes exactly one record.
- Retries reuse the same
idanddata.lead.id; only the timestamp and signature change. - Up to 8 attempts, backing off 30s → 2m → 10m → 30m → 1h → 2h → 6h.
- Deduplicate on both
id(event) anddata.lead.id(lead) before creating.
Past deliveries and missing fields
Activating, resuming, or changing format does not backfill historical leads. Open Deliveries to review status. If a delivery failed or was skipped, contact Leadpoet with the affected request and delivery IDs so we can assess replay.
Eligible failed (dead) or skipped deliveries can be replayed after the destination is verified and active. A replay uses its current format and keeps the original event and lead IDs. Your receiver must still prevent duplicate CRM records.
A delivery marked succeeded cannot be replayed through the normal replay operation. A 2xx response confirms receipt, not that your CRM saved every field. If an accepted lead is missing intent, use its pipeline spreadsheet export for a manual reconciliation in your system, or contact Leadpoet to agree a recovery plan. Upgrading the webhook does not update existing CRM records.
Test, activate, and operate
- 1Return to your destination and select Test. Leadpoet sends a signed, PII-free
endpoint.testevent. Verify it like a lead event and return 2xx. - 2Confirm the destination shows Verified, then select Activate. Delivery begins with newly published leads; activation does not backfill earlier leads.
- 3Open Deliveries to review each attempt and its result without exposing request or response bodies.
- 4Pause and Resume when your receiver is under maintenance. Existing queued attempts wait, but leads published while paused are not queued; Resume applies to future leads.
- 5Rotate from Draft or Paused if the signing secret needs replacing, then test again before activating. Disconnect destination permanently disables that destination and skips queued deliveries while preserving delivery history.
The test below shows the intent-details version; tests on legacy connections report 2026-07-01. Connectivity tests contain no lead data and must not create a CRM record. After a successful test, check the next authorized lead.published event against the English export and confirm the intent is saved once for that lead.
{
"object": "event",
"id": "8d95c149-1f75-4202-b0b6-6b57d8cb05cd",
"type": "endpoint.test",
"api_version": "2026-09-11",
"created_at": "2026-09-11T18:00:00.000Z",
"account_reference": "sales-workspace",
"data": { "message": "Leadpoet webhook endpoint test" }
}If your receiver reports sig_ok: false, check the raw request bytes, full current signing secret, timestamp in seconds, and base64url encoding. Reject invalid signatures. Changing the payload version does not repair signature verification.
Checklist
- Public HTTPS endpoint on port 443 with a valid TLS certificate
- Reads the raw request body before parsing
- Verifies the v1= signature and rejects stale timestamps
- Signing secret stored in a secret manager, never logged
- Deduplicates on id and data.lead.id
- Creates records only (no updates or merges)
- Returns 2xx within 5 seconds
- Passed the endpoint.test event
- Destination shows Verified and Active
- Intent details version enabled if your CRM needs Why this account
- First lead checked against the export and saved without duplicates
Need help with setup or delivery? Contact Leadpoet with the request or delivery ID. Never include signing secrets or lead data.