Skip to main content

    Documentation

    Connect Leadpoet to your systems

    Guides for connecting HubSpot or sending Leadpoet leads to your own webhook endpoint.

    Custom Webhook

    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.

    Eventlead.publishedDeliveryat-least-onceSigningHMAC-SHA256

    How 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 2xx

    You build and host one endpoint. Leadpoet handles reliable, signed, retrying delivery.

    In Leadpoet

    Set up your destination

    1. 1Open Account. Find the Integrations row and select Manage.
    2. 2Select Custom Webhook, then choose Add destination.
    3. 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.
    4. 4Authorize delivery and add the destination. Leadpoet will show its signing secret once.
    5. 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.

    1. Find your connection with GET /api/outbound-webhook-destinations. Use its id as :id below and check its payload_version.
    2. Pause the connection with POST /api/outbound-webhook-destinations/:id/pause. Wait for any in-flight delivery to finish. A version change returns 409 while a delivery is still in flight.
    3. Send the version update shown below with PATCH /api/outbound-webhook-destinations/:id.
    4. Run POST /api/outbound-webhook-destinations/:id/test. Your receiver must verify the signed endpoint.test, confirm api_version: "2026-09-11", and return 2xx. Continue only when the test response reports ok: true.
    5. Resume with POST /api/outbound-webhook-destinations/:id/resume. You can also use the Test and Resume controls in settings after changing the version.
    PATCH body
    { "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):

    1. 1Read the raw body and the X-Leadpoet-Webhook-Timestamp, X-Leadpoet-Event-Id, and X-Leadpoet-Webhook-Signature headers.
    2. 2Reject the request if the timestamp is more than 5 minutes from your clock.
    3. 3Build the signed message: "<timestamp>.<event_id>." followed by the raw body bytes (note the two literal dots).
    4. 4Compute base64url( HMAC_SHA256( secret, signed_message ) ).
    5. 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:

    reference — node.js
    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);
    }
    HeaderValue
    Content-Typeapplication/json
    User-AgentLeadpoet-Webhooks/1.0
    X-Leadpoet-Event-IdEvent UUID — stable across retries; your primary dedup key
    X-Leadpoet-Event-Typelead.published (or endpoint.test)
    X-Leadpoet-Webhook-TimestampUnix time in seconds, at send
    X-Leadpoet-Webhook-Signaturev1=<base64url HMAC-SHA256>
    X-Leadpoet-Delivery-AttemptAttempt 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.

    application/json
    {
      "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_status is the published status as-is — don’t treat an email as valid unless this says so.
    • phone has 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 as null.

    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 fieldSource
    Leadpoet Lead ID · dedup keydata.lead.id
    Leadpoet Event IDid
    Published Atdata.lead.published_at
    Name · Email · Email Status · Phone · Roledata.lead.*
    LinkedIn · Company · Website · Company LinkedIndata.lead.*
    Industry · City · State · Country · Employee Countdata.lead.*
    Company Descriptiondata.lead.description
    Why this account / Intent Detailsdata.lead.icp_details
    Intent Scoredata.lead.intent_score

    Step 5

    Responses

    200–299Accepted. Done — return 204.
    408 · 425 · 429 · 5xx · timeoutRetried with backoff
    429 / 503 + Retry-AfterHonored when between 30s and 6h
    400 · 405Marked failed, not retried
    401 · 403 · 404 · 410 · 413 · 415 · 422Not retried — delivery pauses until fixed

    After 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 id and data.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) and data.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

    1. 1Return to your destination and select Test. Leadpoet sends a signed, PII-free endpoint.test event. Verify it like a lead event and return 2xx.
    2. 2Confirm the destination shows Verified, then select Activate. Delivery begins with newly published leads; activation does not backfill earlier leads.
    3. 3Open Deliveries to review each attempt and its result without exposing request or response bodies.
    4. 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.
    5. 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.

    endpoint.test
    {
      "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

    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

    Questions during integration? Contact your Leadpoet point of contact. More integration guides are on the way.