Webhook integration guide
Receive a signed event for every new lead and create one record in your CRM. Follow the steps below end to end to get live leads flowing.
lead.publishedDeliveryat-least-onceSigningHMAC-SHA256How it works
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 reliable, signed, retrying delivery.
In Leadpoet
Set up your destination
- 1Open Account. Find the Integrations 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.
Team access
Team owners and admins can add, test, activate, pause, rotate, and disconnect destinations. Team members have view-only access to destination status and delivery history.
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. A team owner or admin must change it through the API; settings does not yet have a format selector. Contact Leadpoet if you need 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 the signed-in team owner or admin. 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.
Step 1
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.
Step 2
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 (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.
Step 3
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": "rudolph-podio",
"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",
"email_status": "valid",
"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
- •
email_statusis the published status as-is — don’t treat an email as valid unless this says so. - •
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.
Step 4
Create the record (exactly once)
Store data.lead.id in a dedicated field and check it before creating. 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.
| Your CRM field | Source |
|---|---|
| Leadpoet Lead ID · dedup key | data.lead.id |
| Leadpoet Event ID | id |
| Published At | data.lead.published_at |
| Name · Email · Email Status · 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 |
Step 5
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.
Reliability
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.
Recovery
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. Replay is an admin operation, not a customer settings control.
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.
Launch
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": "rudolph-podio",
"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.
Before go-live
Checklist
Questions during integration? Contact your Leadpoet point of contact. More integration guides are on the way.