WhatsApp API

Connect, receive, and send WhatsApp messages

This guide follows the order you will implement the integration: create an assisted setup request, wait for that request to become connected, confirm the separate connection record is active, then receive signed messages and send replies.

Already connected but missing the tenant and connection IDs? Find the existing connection using its phone number instead of starting setup again.

01

Before you start

Lead Intel gives you a platform API key. Your backend uses that key for every setup, connection, and send request.

You also need a public HTTPS webhook URL. After a number connects, Lead Intel sends its incoming WhatsApp messages to that URL.

Create a separate webhook signing secret for each connection. Store the API key and signing secrets only on your backend.

Authenticated request
curl https://app.leadsintel.net/api/external-platform/whatsapp/assisted-requests \
  -H "Authorization: Bearer li_platform_..."

Alternative headerYou may send the same key as X-LeadIntel-Platform-Key: li_platform_... instead of using bearer authentication.

Decide these IDs before your first request Use the customer's ID in your system as externalPlatformTenantId. Create one externalConnectionId for each WhatsApp number. You will use both IDs after setup to read, update, disconnect, and send through that connection.
02

Connect a WhatsApp number

Send the minimum required fields below from your backend. Lead Intel queues the number for assisted setup and returns a requestId.

Save that requestId only for tracking this setup attempt. After the number connects, use your tenant ID and connection ID—not the request ID—for all connection and message calls.

Request fields Required and optional fields
Tenant ID RequiredexternalPlatformTenantId identifies the customer or account that owns the number in your platform.
Connection ID RequiredexternalConnectionId is a unique ID from your platform for this WhatsApp connection. Keep using the same ID until the connection is disconnected.
Phone RequiredcountryCode is the country calling code and phoneNumber is the local number, both sent as digits.
Business details RequiredbusinessName is the business being connected. displayName is the name requested for the WhatsApp profile.
User ID OptionalexternalUserId associates the connection with a specific user in your tenant. Lead Intel stores and returns it with the connection.
Inbound URL RequiredinboundWebhookUrl is a publicly reachable HTTPS endpoint for this specific WhatsApp connection. Lead Intel sends signed incoming messages there.
Webhook secret RequiredinboundWebhookSecret is generated and stored by you. Use it to verify Lead Intel's inbound signatures. API replies use the platform key, not this secret.
Webhook timeout OptionalinboundWebhookTimeoutMs controls how long Lead Intel waits for an immediate reply. The default is 10,000 ms; allowed values are 1,000–30,000 ms.
Business profile Optional
Profile imageprofileImageUrl must be a publicly reachable HTTPS JPG/JPEG URL larger than 192 × 192 pixels and no larger than 5 MB. Use a square image where possible.
Aboutabout is the short profile text, up to 139 characters.
Descriptiondescription explains the business, up to 512 characters.
Addressaddress is the business address, up to 256 characters.
Emailemail is the public business contact email.
Websiteswebsites accepts up to two website URLs.
Categoryvertical is Meta's business category.
POST/external-platform/whatsapp/assisted-requests
Your backend

Create one request for each WhatsApp number. Call this endpoint from your backend; browser requests and frontend callbacks are not supported.

Request
POST /api/external-platform/whatsapp/assisted-requests
Authorization: Bearer li_platform_...
Content-Type: application/json

{
  "externalPlatformTenantId": "tenant_42",
  "externalUserId": "user_108",
  "externalConnectionId": "wa_primary",
  "countryCode": "254",
  "phoneNumber": "700000000",
  "businessName": "Acme Stores",
  "displayName": "Acme Support",
  "businessProfile": {
    "profileImageUrl": "https://cdn.example.com/acme-logo-512x512.jpg",
    "about": "Customer support and online orders",
    "description": "Acme Stores provides retail products and customer support.",
    "address": "123 Business Street, Riyadh",
    "email": "support@example.com",
    "websites": ["https://example.com"],
    "vertical": "RETAIL"
  },
  "inboundWebhookUrl": "https://api.your-platform.example/whatsapp/inbound",
  "inboundWebhookSecret": "your-signing-secret",
  "inboundWebhookTimeoutMs": 10000
}
Accepted response
{
  "requestId": "request_...",
  "status": "pending"
}

What you get: 202 Accepted means the setup request was saved. It does not mean the number is connected. Save requestId, then use the status endpoint below.

Optional fields are real setup controls: omit externalUserId, businessProfile, or inboundWebhookTimeoutMs only when you do not need them. If you include businessProfile, Lead Intel applies it after WhatsApp verification; you can also change it later with PATCH.

Verified name limitation: displayName is submitted to Meta as the number's verified name during initial setup. It is not an ordinary profile field and cannot be changed through the connection PATCH endpoint. Meta limits how often a verified name can be submitted or changed. If the status response contains verified_name_change_limit_reached, stop retrying the setup request and wait for Meta's limit to clear or contact Meta support.

Duplicate number: before accepting the request, Lead Intel checks whether the number is already connected to a Lead Intel workspace, product, or active external connection, or has appeared in any earlier external assisted-setup request—including failed or cancelled requests. If it exists, the API returns 409 with {"error":"phone_number_already_connected"}. Do not retry with the same number or start Meta verification.

GET/external-platform/whatsapp/assisted-requests/:requestId
Your backend

Use the requestId from step 1. Check every few seconds while setup is running. This endpoint reports the assisted setup request, not the live connection record.

Path parameter
requestId RequiredThe ID returned when you created this setup request.
Request and important response fields
GET /api/external-platform/whatsapp/assisted-requests/{requestId}
Authorization: Bearer li_platform_...

{
  "requestId": "request_...",
  "status": "connected",
  "externalPlatformTenantId": "tenant_42",
  "externalConnectionId": "wa_primary",
  "displayPhoneNumber": "+254700000000",
  "errorCode": null,
  "errorMessage": null,
  "updatedAt": "2026-09-01T12:10:00.000Z"
}
What to do for each status
pendingSetup is queued or in progress. Keep checking.
awaiting_codeThe customer must provide a verification code to Lead Intel. Keep checking while they complete verification.
connectedAssisted setup completed. Stop polling this request, then retrieve the connection by externalPlatformTenantId and externalConnectionId and confirm its separate status is active before sending.
failedShow or log errorCode and errorMessage, then stop normal polling. Common codes include profile_image_resolution_low (use a JPG larger than 192 × 192 pixels), meta_permission_denied, verification_code_rejected, phone_registration_failed, and verified_name_change_limit_reached. Do not submit the phone number again: Lead Intel reserves previously submitted numbers and will reject a duplicate request. Fix the reported issue and ask Lead Intel to retry this existing request; resume polling only when Lead Intel confirms recovery has started.
cancelledStop checking. Do not create another request for the same phone number because previously submitted numbers remain reserved. Contact Lead Intel if the customer wants to restart setup.

The example shows the fields your application normally needs. The full response also echoes the submitted setup details and timestamps. It never returns provider credentials, verification codes, or your webhook secret.

API

Find an existing connection by phone number

Recover a connected number's tenant and connection IDs using only its international phone number and your platform API key. This read-only lookup does not connect, verify, reactivate, or change the number.

POST/external-platform/whatsapp/connections/lookup
Your backend

Call from your backend with Content-Type: application/json and either bearer authentication or X-LeadIntel-Platform-Key. No tenant ID, connection ID, Meta login, QR scan, or SMS code is required for this lookup.

phoneNumber RequiredA string beginning with + and the country calling code, followed by the full number (7–15 digits total). Spaces, hyphens, and parentheses are accepted. Do not supply a local-only number, an extension, or Meta's numeric phoneNumberId.
Request — run on your backend
curl --request POST "$LEAD_INTEL_API_BASE_URL/external-platform/whatsapp/connections/lookup" \
  --header "X-LeadIntel-Platform-Key: $LEAD_INTEL_PLATFORM_KEY" \
  --header "Content-Type: application/json" \
  --data '{"phoneNumber":"+254700000000"}'

Set LEAD_INTEL_API_BASE_URL to the API base URL shown above (including /api) and LEAD_INTEL_PLATFORM_KEY to your issued key in your backend's secret configuration. Replace the sample number with the connected WhatsApp business number. Never put the key in browser code, URLs, or logs.

200 — existing connection found (example)
{
  "externalPlatformTenantId": "tenant_42",
  "externalConnectionId": "wa_primary",
  "phoneNumberId": "123456789012345",
  "displayPhoneNumber": "+254700000000",
  "status": "active",
  "disconnectedAt": null,
  "updatedAt": "2026-09-28T08:00:00.000Z"
}

Use the recovered connection

  1. Save externalPlatformTenantId as your tenantId and externalConnectionId as your connectionId against the correct customer in your system. Finding a number does not authenticate that customer; enforce your own customer permissions before exposing it.
  2. If status is active, reuse the existing connection. Save phoneNumberId to identify incoming webhooks.
  3. Use GET /api/external-platform/whatsapp/connections/{tenantId}/{connectionId} for current settings, or the documented send endpoint to send messages. URL-encode both IDs when building paths.
  4. If the returned connection is not active, do not send through it. Follow your recovery process or contact Lead Intel; this lookup does not reactivate it. A permanently deleted connection cannot be recovered with this endpoint.

Errors and recovery

400 invalid_phone_numberSupply a valid international-format string in phoneNumber, including the leading + and country code.
401 invalid_platform_keyThe key is missing, invalid, or revoked. Check your backend key configuration; do not start number setup again.
404 connection_not_foundNo matching connection is visible to this key. Check the full number and that you are using the key associated with its original setup. Connections under other keys, unfinished setup requests, and permanently deleted records are not returned. Do not automatically reconnect on this error.
409 ambiguous_connectionMore than one connection matches within your key's scope. No candidate details are returned. Ask Lead Intel to resolve the ambiguity instead of choosing an account or reconnecting.
429 — rate limitedWait before retrying; honor Retry-After if present. Save recovered IDs rather than looking up the phone before every message.
Network or server errorRetry with bounded backoff. A failed lookup is not proof that the number needs connecting again.

Scope and privacyResults are restricted to connections owned by the supplied platform key. This is not a global phone directory or personal-account SMS verification. Provider access tokens and webhook signing secrets are never returned. Responses use Cache-Control: no-store; avoid logging phone numbers or caching these responses in shared infrastructure.

API

Connect a number already on Lead Intel

Take over a WhatsApp number that a Lead Intel business already uses. Lead Intel texts a 6-digit code to that number; when your customer enters it, the number becomes one of your connections with your webhook. Meta is not contacted: the number keeps its registration, display name and message templates, and the Lead Intel business keeps sending its own outreach from it.

POST/external-platform/whatsapp/existing-numbers/lookup
Your backend

Check whether a number is connected in Lead Intel and can be claimed. Body: {"phoneNumber":"+254700000000"}.

200 — number found
{ "connected": true, "claimable": true, "displayPhoneNumber": "+254700000000" }

When claimable is false, reason is phone_number_already_connected (another platform owns it) or number_not_claimable. A number Lead Intel doesn't hold returns 404 number_not_connected; use assisted setup instead.

POST/external-platform/whatsapp/existing-numbers/claims
Your backend

Sends the SMS code to the number and returns a claimId. The code expires in 10 minutes.

phoneNumber RequiredInternational format with a leading +.
externalPlatformTenantId RequiredYour customer's ID (max 512 characters).
externalConnectionId RequiredYour ID for this number; must not already be used for that tenant.
externalUserId OptionalThe user in your system who started the claim.
inboundWebhookUrl, inboundWebhookSecret RequiredWhere Lead Intel delivers this number's incoming messages, and the secret used to sign them. Same contract as numbers you set up through Lead Intel.
inboundWebhookTimeoutMs Optional1000–30000, default 10000.
Request — run on your backend
curl --request POST "$LEAD_INTEL_API_BASE_URL/external-platform/whatsapp/existing-numbers/claims" \
  --header "X-LeadIntel-Platform-Key: $LEAD_INTEL_PLATFORM_KEY" \
  --header "Content-Type: application/json" \
  --data '{"phoneNumber":"+254700000000","externalPlatformTenantId":"tenant_42","externalConnectionId":"wa_primary","inboundWebhookUrl":"https://example.com/leadintel/whatsapp","inboundWebhookSecret":"your-signing-secret"}'
202 — code sent
{ "claimId": 17, "status": "code_sent", "sentTo": "+254700000000", "expiresAt": "2026-10-12T21:10:00.000Z" }
POST/external-platform/whatsapp/existing-numbers/claims/{claimId}/verify
Your backend

Body: {"code":"123456"}. On success the number is connected to your platform immediately, and its incoming messages go to your webhook. Use the returned IDs with the connection, send and lookup endpoints exactly as for any other connection.

200 — connected
{
  "status": "connected",
  "externalPlatformTenantId": "tenant_42",
  "externalUserId": null,
  "externalConnectionId": "wa_primary",
  "phoneNumberId": "123456789012345",
  "displayPhoneNumber": "+254700000000"
}

Errors and recovery

400 invalid_phone_number / invalid_claim_requestFix the request body.
400 invalid_codeWrong code; attemptsLeft says how many tries remain (5 per code).
404 number_not_connectedLead Intel doesn't hold this number. Use assisted setup.
409 phone_number_already_connected, number_not_claimable, connection_id_in_useAnother platform owns the number, it can't be moved, or your connection ID is taken for this tenant.
410 code_expiredThe code expired, was used, or ran out of attempts. Start a new claim.
502 sms_not_sentThe SMS could not be sent. Retry shortly.

OwnershipOnly whoever holds the number's SIM can complete a claim. Leads of the Lead Intel business that used the number still have their replies routed to that business; everyone else goes to your webhook.

03

Manage a connected number when needed

After assisted setup returns connected, stop using requestId. Retrieve the connection using your tenantId and connectionId; only its active status can use connection and send endpoints.

All calls use your platform key. The signing secret is write-only and is never returned.

You can skip this section during the first integration. Use it later to inspect settings, change the webhook or profile, or remove a connected number from both Meta and Lead Intel.

GET/external-platform/whatsapp/connections/:tenantId/:connectionId
Your backend

Use this call when you need the current connection state or webhook configuration. Save phoneNumberId: incoming webhook bodies include that value, so it tells you which connection received the message.

Request parameters
tenantId RequiredYour external platform tenant ID, passed in the URL path.
connectionId RequiredThe connection ID used when this number was created.
Important response fields
{
  "externalConnectionId": "wa_primary",
  "phoneNumberId": "provider-phone-id",
  "displayPhoneNumber": "+254700000000",
  "status": "active",
  "inboundWebhookUrl": "https://api.your-platform.example/whatsapp/inbound",
  "inboundWebhookTimeoutMs": 10000,
  "businessProfile": {
    "profileImageConfigured": true
  },
  "updatedAt": "2026-09-01T12:10:00.000Z"
}
PATCH/external-platform/whatsapp/connections/:tenantId/:connectionId
Your backend

Update the receiver configuration and/or the public WhatsApp Business Profile for an active connection. Send only the fields you want to change; omitted fields stay unchanged. When businessProfile is included, Lead Intel calls Meta using the connected number's credentials before saving the new profile locally. A 204 response means the requested update succeeded in Meta and Lead Intel.

Request parameters
tenantId RequiredYour external platform tenant ID, passed in the URL path.
connectionId RequiredThe connection ID used when this number was created.
inboundWebhookUrl OptionalReplace the HTTPS receiver URL. Changing only this value preserves the existing signing secret.
inboundWebhookSecret OptionalRotate the signing secret. Keep the new value on your backend; it is never returned.
inboundWebhookTimeoutMs OptionalSet the immediate response timeout from 1,000 to 30,000 ms.
businessProfile OptionalChange public WhatsApp profile details. Lead Intel applies them to Meta before saving the update.
Request
PATCH /api/external-platform/whatsapp/connections/tenant_42/wa_primary
Authorization: Bearer li_platform_...
Content-Type: application/json

{
  "businessProfile": {
    "profileImageUrl": "https://cdn.acme.example/new-profile.png",
    "about": "Acme support, now 24/7",
    "address": "Nairobi, Kenya",
    "description": "Customer support for Acme customers",
    "email": "help@acme.example",
    "websites": ["https://acme.example/help"],
    "vertical": "RETAIL"
  }
}

Send inboundWebhookSecret only when you want a new signing secret. Send inboundWebhookTimeoutMs only when you want to change how long Lead Intel waits for an immediate reply. Inside businessProfile, supported fields are profileImageUrl, about, address, description, email, websites (up to two HTTP(S) URLs), and vertical. A profile image must be a publicly reachable HTTPS JPG/JPEG URL, larger than 192 × 192 pixels, and no larger than 5 MB. Use a square image where possible; Lead Intel downloads it and uploads it to Meta.

Response and error handling
204 SuccessMeta accepted the profile update and/or Lead Intel saved the receiver configuration.
400 Invalid requestThe profile or receiver configuration failed validation. Nothing is saved.
401 UnauthorizedThe platform API key is missing, invalid, or revoked.
404 Not foundThe connection does not exist for this platform key and tenant.
409 InactiveThe connection is not active and cannot update Meta.
502 Meta/provider failureMeta or the upstream request rejected the profile update. The response is {"error":"whatsapp_business_profile_update_failed"}; the raw provider response is logged by Lead Intel but not exposed.
DELETE/external-platform/whatsapp/connections/:tenantId/:connectionId
Your backend

This is the single cleanup call. Lead Intel first deregisters the number from Meta, then permanently removes the connection and its Lead Intel records. A successful request returns 204 only after both steps complete. If an older request already marked the local connection disconnected without completing Meta cleanup, this endpoint reconciles it with Meta instead of silently assuming deregistration succeeded.

Request parameters
tenantId RequiredYour external platform tenant ID, passed in the URL path.
connectionId RequiredThe connection ID used when this number was created.
Request
DELETE /api/external-platform/whatsapp/connections/tenant_42/wa_primary
Authorization: Bearer li_platform_...

Provider-first safety: if Meta rejects deregistration, Lead Intel returns 502 with {"error":"meta_deregistration_failed"} and deletes nothing. Retry the same call after resolving the Meta issue. This endpoint removes the connection, matching setup requests and sessions, lifecycle events, and delivery history. The external tenant and platform key remain available for a new number.

To cancel a setup request that is still pending, call DELETE /external-platform/whatsapp/assisted-requests/{requestId} instead.

04

Incoming message webhook

Once a number is active, Lead Intel sends each incoming WhatsApp event to that connection's inboundWebhookUrl.

Handle each request in this order: verify the signature, reject duplicate event IDs, identify the connection, read the message, then return a response.

POSTyour registered inboundWebhookUrl
Lead Intel → your platform

Every delivery attempt is a signed POST. Lead Intel may retry temporary failures, so duplicate protection is required. It never sends a GET or HEAD request to test the URL.

Process the webhook in this order
1. VerifyCheck the timestamp and HMAC signature against the exact request bytes before parsing JSON.
2. Ignore duplicatesSave X-LeadIntel-Event-Id. If you already processed it, return the same successful result without repeating your work.
3. Identify the connectionMatch metadata.phone_number_id to the phoneNumberId returned by the connection endpoint.
4. Read the messageRead the sender from messages[i].from, the message type from messages[i].type, and its content from the matching field.
5. RespondReturn reply JSON with 200, or save the event and return 202/204 to reply later.
Incoming request parameters
X-LeadIntel-Event-Id RequiredA unique delivery ID. Save it before processing the message, and ignore the request if you have already processed the same ID.
X-LeadIntel-Timestamp RequiredThe signed timestamp in milliseconds. Reject requests outside the five-minute acceptance window.
X-LeadIntel-Signature RequiredThe lowercase HMAC-SHA256 value used to verify that Lead Intel sent the request and that the body was not changed.
Request body RequiredThe Meta change value object. Use metadata.phone_number_id to identify the connection and read incoming messages from messages[].
Headers
Content-Type: application/json
X-LeadIntel-Event-Id: external-message:...
X-LeadIntel-Timestamp: 1788206400000
X-LeadIntel-Signature: lowercase-hex-hmac-sha256
Direct Meta change value
{
  "messaging_product": "whatsapp",
  "metadata": {
    "display_phone_number": "254 700 000000",
    "phone_number_id": "provider-phone-id"
  },
  "messages": [{
    "id": "wamid...",
    "from": "254700000000",
    "timestamp": "1788206400",
    "type": "text",
    "text": { "body": "Hello" }
  }]
}
Webhook fields and media
Event identityX-LeadIntel-Event-Id uniquely identifies the delivery and may appear only in the header. Save it before processing the message so retries do not repeat your work.
Connection identityMatch metadata.phone_number_id to the phoneNumberId returned by the connection status endpoint. Do not require tenant or connection IDs in the webhook body.
Sendermessages[0].from is the WhatsApp user who sent the message and is the to value when replying through the send endpoint.
Body formatThe request body is the Meta change value object. It does not include Meta's outer entry[].changes[] wrapper, and Lead Intel does not add product, recipient, or callback fields.
MediaFor media messages, read the original object at messages[i].image, audio, video, document, or sticker. Lead Intel preserves Meta's id and adds a signed downloadUrl and expiresAt to that same object. The URL is valid for at most 10 minutes, or until the delivery deadline when that is sooner.
ProSuite media handling
// 1. Verify X-LeadIntel-Signature over the exact raw body.
// 2. Parse the body and inspect messages[].
const message = body.messages[0];
const media = message[message.type];

if (media?.downloadUrl) {
  // 3. Download immediately; no Meta token is needed.
  const file = await fetch(media.downloadUrl);
  if (!file.ok) throw new Error(`Media download failed: ${file.status}`);
  const bytes = await file.arrayBuffer();
  // 4. Store/process bytes before media.expiresAt.
}

Supported inbound mediaUse the signed URL to download image, audio (including voice notes), video, document, or sticker files. Download from your backend, do not add a Meta access token, and do not retry a URL after it expires.

This page documents external-platform connections Lead Intel also supports a separate product-level receiver integration with a Lead Intel envelope and callback URL. Do not implement that wrapped format for numbers connected through this API.
SIGNHMAC-SHA256
Incoming webhooks

Build the signed text as {timestamp}.{exactRawBody}. Use the connection's inboundWebhookSecret as the HMAC key, compare the lowercase hexadecimal result with a timing-safe check, and reject timestamps more than five minutes from the current time.

Signature inputs
timestamp RequiredThe value from X-LeadIntel-Timestamp, included exactly as received.
exactRawBody RequiredThe unparsed request bytes, with no whitespace or serialization changes.
inboundWebhookSecret RequiredThe connection signing secret stored by your backend.
Node.js verification
const expected = createHmac('sha256', inboundWebhookSecret)
  .update(`${timestamp}.${rawBody}`)
  .digest('hex');
05

Immediate and asynchronous replies

Choose one reply method for each incoming message.

Immediate: return reply JSON with 200 from the webhook. Delayed: save the event, return 202 or 204, then call the send endpoint when the reply is ready.

200Immediate reply
Your webhook response

Use this when the reply is ready before the webhook timeout. The response body becomes the WhatsApp reply to the sender.

Reply parameters
type RequiredUse text, media, template, or an interactive type: buttons, list, link_button (see Buttons and menus).
text Required for textNon-empty reply text up to 4,000 characters.
media fields Required for mediaUse mediaType and a public HTTPS url; caption and filename are optional.
template fields Required for templateUse the approved template name, language, and optional components.
Content-Type: application/json

{ "type": "text", "text": "Thanks — how can we help?" }

Text must be non-empty and no longer than 4,000 characters.

Immediate media reply
{
  "type": "media",
  "mediaType": "document",
  "url": "https://cdn.prosuite.example/quote.pdf",
  "caption": "Your quote",
  "filename": "quote.pdf"
}

The media URL must be public HTTPS so Meta can download it. Supported mediaType values are image, video, audio, and document. For any other successful response, return valid reply JSON or Lead Intel will reject the delivery.

202Asynchronous reply
Your webhook response

Use this when your application needs more time. Save the event first, return 202 or 204 with no reply body, then call the send endpoint when the reply is ready.

Send request parameters
tenantId and connectionId RequiredUse the connection that received the incoming message.
to RequiredUse the incoming message's messages[i].from value.
message fields RequiredSend text, media, template, or interactive (buttons, list, link_button) fields using the same format as an immediate reply.
Idempotency-Key RequiredUse a unique value for this reply. A missing or empty key, or one longer than 200 characters, returns 400 with valid_idempotency_key_required.
Asynchronous send request
POST /api/external-platform/whatsapp/connections/tenant_42/wa_primary/send
Authorization: Bearer li_platform_...
Idempotency-Key: reply-wamid-1
Content-Type: application/json

{
  "to": "254700000000",
  "type": "text",
  "text": "Your asynchronous reply"
}

What you get: 202 Accepted means Lead Intel accepted and submitted the message to its WhatsApp provider; it is not delivery confirmation. Reuse the same Idempotency-Key only when retrying the same recipient and message. A completed replay returns the saved result without sending again; reuse with different content returns 409.

STATUSWebhook response handling
Lead Intel

Review the response codes Lead Intel accepts from your receiver and how each one affects delivery retries.

Webhook response status codes
200The webhook was processed immediately. Return valid text, media, template, or interactive reply JSON in the body.
202 or 204You saved the event but are not replying yet. Send the reply later through the authenticated connection endpoint.
400, 401, or 409Your receiver rejected the request permanently. Lead Intel records the failure and does not retry it.
408, 425, or 429Your receiver reported a temporary problem. Lead Intel retries until the delivery time or attempt limit is reached.
5xxYour receiver's server reported a temporary problem. Lead Intel retries until the delivery time or attempt limit is reached.
Other 2xxReturn valid immediate reply JSON in the body. An empty or invalid body is treated as a failed delivery.
API

Buttons and menus

Send WhatsApp interactive messages: up to 3 reply buttons, a list menu, or a single button that opens a link. Use them anywhere a reply is accepted: as the immediate webhook response (200 with the JSON below, without to), or in the body of the send endpoint (with to). The fields are identical in both. They need no template approval, but WhatsApp only delivers them inside the 24-hour customer-service window; outside it, use an approved template (templates can carry their own buttons).

typebuttons
Reply buttons
body RequiredMessage text, up to 1,024 characters.
header, footer OptionalPlain text, up to 60 characters each.
buttons Required1–3 items of {"id", "title"}. id up to 256 characters, title up to 20. Both must be unique.
Send reply buttons
POST /api/external-platform/whatsapp/connections/tenant_42/wa_primary/send
Idempotency-Key: menu-wamid-1
Content-Type: application/json

{
  "to": "254700000000",
  "type": "buttons",
  "header": "Pointify POS",
  "body": "Hi! What would you like to do?",
  "footer": "Tap an option",
  "buttons": [
    { "id": "book_demo", "title": "Book a demo" },
    { "id": "prices", "title": "See prices" },
    { "id": "agent", "title": "Talk to a person" }
  ]
}
typelist
List menu
body RequiredMessage text, up to 1,024 characters. header and footer are optional (60 characters).
buttonText RequiredLabel of the button that opens the menu, up to 20 characters.
sections Required1–10 sections, each with rows of {"id", "title", "description"}: id up to 200, title up to 24, optional description up to 72 characters. At most 10 rows in total, unique ids. Each section needs a title (24 characters) when there is more than one.
Immediate list reply
{
  "type": "list",
  "body": "Choose a plan to see details.",
  "buttonText": "View plans",
  "sections": [
    { "title": "Monthly", "rows": [
      { "id": "basic_m", "title": "Basic", "description": "KES 1,000 / month" },
      { "id": "pro_m", "title": "Pro", "description": "KES 2,500 / month" }
    ] },
    { "title": "Yearly", "rows": [
      { "id": "pro_y", "title": "Pro yearly", "description": "2 months free" }
    ] }
  ]
}
typelink_button
Link button
body RequiredMessage text, up to 1,024 characters. header and footer are optional (60 characters).
displayText RequiredButton label, up to 20 characters.
url RequiredHTTPS link the button opens.
Immediate link button reply
{
  "type": "link_button",
  "body": "Your invoice is ready.",
  "displayText": "Pay now",
  "url": "https://pay.example.com/inv/123"
}
INWhen the customer taps
Incoming webhooks

The tap arrives as a normal incoming message on your webhook. Route on the id you set, not on the title.

Reply buttonmessages[i].type is interactive; read messages[i].interactive.button_reply.id and .title.
List rowmessages[i].type is interactive; read messages[i].interactive.list_reply.id, .title and .description.
Template quick-reply buttonmessages[i].type is button; read messages[i].button.payload and .text.
Incoming button tap (excerpt)
{
  "messages": [{
    "from": "254700000000",
    "type": "interactive",
    "interactive": {
      "type": "button_reply",
      "button_reply": { "id": "book_demo", "title": "Book a demo" }
    }
  }]
}

Errors

400 invalid_whatsapp_messageSend endpoint only: a limit was broken (for example 4 buttons or a 21-character title). detail says which.
Invalid immediate replyYour webhook already answered 200, so you receive no error response. Lead Intel marks the delivery failed, sends nothing to the customer, and does not post the message to your webhook again. The reason appears in the connection's traffic history in Lead Intel. Validate replies before returning them, or use the send endpoint to get the error back.
502 send_failedMeta refused the message, most often because the 24-hour window has closed. Send an approved template instead.
06

Proactive outbound messages

Use the same send endpoint for messages that are not direct webhook replies.

Within Meta's 24-hour customer-service window, send text, media, or interactive messages (buttons, lists, link buttons). Outside that window, send an approved template. Every send requires an idempotency key so the same request can be retried safely.

POST/external-platform/whatsapp/connections/:tenantId/:connectionId/send
Your backend

Choose the active connection in the URL and put the recipient in to. This minimum example sends text.

Request
Authorization: Bearer li_platform_...
Idempotency-Key: prosuite-message-abc-123
Content-Type: application/json

{
  "to": "254700000000",
  "text": "Your message"
}
Request parameters
tenantId RequiredThe customer ID used when this connection was created.
connectionId RequiredThe connection ID used when this number was created.
Authorization RequiredYour platform key in the bearer header.
to RequiredThe recipient's WhatsApp number, including country code.
text Required for textThe message text, from 1 to 4,000 characters. type: "text" is optional.
Idempotency-Key RequiredA unique, non-empty header value no longer than 200 characters. Missing or invalid values return 400 with valid_idempotency_key_required.
Outbound message fields
Destinationto is the recipient's WhatsApp phone number.
TextSend to and non-empty text up to 4,000 characters. type: "text" may be included but is not required.
MediaUse type: "media", a supported mediaType, and a public HTTPS url. Caption and filename are optional.
TemplateUse type: "template" with the approved template name, language, and optional components.
IdempotencyUse a new Idempotency-Key for each message. Reuse it only when retrying the same recipient and message body; a completed replay does not send again, while a changed request returns 409.
HTTP/1.1 202 Accepted
{ "messageId": "wamid....", "status": "accepted" }

What you get: 202 means Lead Intel accepted and submitted the message to its WhatsApp provider and returns its messageId when available. It does not guarantee final delivery. 400 means the idempotency key or message is invalid; 404 means the connection was not found; 409 means the connection is inactive or the key was reused for different content; and 502 means the send failed. Monitor provider delivery events or your own conversation records when delivery status matters.

Proactive media
{
  "to": "254700000000",
  "type": "media",
  "mediaType": "image",
  "url": "https://cdn.prosuite.example/property.jpg",
  "caption": "Available property"
}
Approved template
{
  "to": "254700000000",
  "type": "template",
  "name": "appointment_reminder",
  "language": "en",
  "components": []
}

When retrying, reuse an idempotency key only with the same recipient and message body. Retry a 502 deliberately with that same key; do not create a new key for an uncertain attempt, because that can create another send.

POST/external-platform/whatsapp/connections/:tenantId/:connectionId/templates
Your backend

Create a WhatsApp message template on the connection's own WhatsApp Business Account and submit it to Meta for review. Send multipart/form-data (required when uploading a header sample) or plain form fields. Once the template is approved, send it with the template message type above.

Request
curl --request POST "$LEAD_INTEL_API_BASE_URL/external-platform/whatsapp/connections/tenant_42/wa_primary/templates" \
  --header "Authorization: Bearer $LEAD_INTEL_PLATFORM_KEY" \
  --form name=welcome_offer \
  --form language=en \
  --form category=MARKETING \
  --form bodyText="Hi {{1}}, thanks for your interest in {{2}}! Want a quick call?" \
  --form 'buttons=[{"type":"QUICK_REPLY","text":"Yes please"},{"type":"QUICK_REPLY","text":"Not now"}]'
Request parameters
tenantId, connectionId RequiredThe IDs used when the connection was created. The connection must be active.
Authorization RequiredYour platform key in the bearer header.
name RequiredTemplate name: lowercase letters, digits and underscores only. Must be unique on the connection's WhatsApp Business Account.
bodyText RequiredMessage body, 10 to 1,024 characters. Only {{1}} (customer name) and {{2}} (your business or product name) are supported, used in order.
languageTemplate language code. Defaults to en.
categoryMARKETING (default) or UTILITY.
headerTypenone (default), image or video. Media headers require the sample file.
sampleSample image or video file, up to 30 MB, sent as a multipart file field. Required when headerType is not none.
buttonsJSON array. Up to 3 QUICK_REPLY buttons, or up to 2 call-to-action buttons (URL with url and optional urlType static/dynamic; PHONE_NUMBER with phoneNumber such as +254700000000). Quick replies and call-to-action buttons cannot be mixed. Button text is at most 25 characters.
HTTP/1.1 201 Created
{ "name": "welcome_offer", "language": "en", "category": "MARKETING",
  "status": "pending", "metaId": "123456789", "variableCount": 2 }

What you get: 201 means Meta accepted the template for review; status is normally pending. Poll the list endpoint until it is approved or rejected. 400 carries an error code (invalid_template_name, invalid_body_text, invalid_buttons, invalid_variables, sample_required, invalid_sample_file or template_submission_failed) and a detail message, including Meta's reason when it rejects the submission. 404 means the connection was not found and 409 means it is not active. Media headers also require the platform's Meta App ID to be configured.

GET/external-platform/whatsapp/connections/:tenantId/:connectionId/templates
Your backend

List the templates on the connection's WhatsApp Business Account, straight from Meta. Add ?status=approved (or pending, rejected) to filter.

HTTP/1.1 200 OK
{ "templates": [
  { "name": "welcome_offer", "status": "approved", "category": "MARKETING",
    "language": "en", "rejectedReason": null, "components": [ ... ] }
] }

A 502 with template_list_failed means Meta could not be reached or refused the request.

DELETE/external-platform/whatsapp/connections/:tenantId/:connectionId/templates/:name
Your backend

Delete a template by name from the connection's WhatsApp Business Account. Returns 204; a template that no longer exists at Meta also returns 204. A 502 with template_delete_failed means Meta refused the deletion.

Template writes are limited to 60 requests per 10 minutes and reads to 120 per 10 minutes.

05

Report a confirmed lead response

When your platform confirms that a lead replied, notify Lead Intel so the response is recorded and that lead's queued follow-up work stops immediately.

POST/external-platform/leads/:externalPlatformTenantId/:leadId/response
Your backend

Authenticate with the platform key using Authorization: Bearer li_platform_... or X-LeadIntel-Platform-Key. The URL tenant and lead references are resolved together with the authenticated key and the required connection ID; a raw workspace or lead ID is never authorized alone. leadId is the Lead Intel lead ID, scoped by the authenticated platform key, externalPlatformTenantId, and externalConnectionId.

Request
POST /api/external-platform/leads/tenant_42/12345/response
Authorization: Bearer li_platform_...
Content-Type: application/json

{
  "externalConnectionId": "wa_primary",
  "eventId": "reply_evt_8f2",
  "channel": "whatsapp",
  "body": "Yes, please send details.",
  "occurredAt": "2026-09-02T12:10:00.000Z"
}

The channel must be email, sms, or whatsapp. body is optional when only the confirmed response state is available; Lead Intel stores an explicit marker, Response confirmed by external platform (message content not provided)., rather than pretending to know the user's text. eventId (or messageId) is required. Idempotency is scoped to the authenticated platform key, tenant, connection, Lead Intel lead, and channel, so the same event ID can safely exist in separate scopes; safely retry the same request as often as needed.

Success
{
  "ok": true,
  "duplicate": false,
  "leadId": 12345,
  "status": "replied",
  "queueStopped": true
}

A body records an inbound message and activity, advances a new or contacted lead to replied, updates its latest message, and halts its queue. Replays return success with duplicate: true without another message or activity. A confirmed response never marks a lead not_interested.

Lead Intel's own connected email, SMS, and WhatsApp channels report replies internally and apply the same stop behavior; use this endpoint only for confirmed replies received through your external platform.

07

Go-live checklist

Complete these checks before you use the integration with real customers.

Go-live checklist
Protect credentialsKeep the platform key and inbound webhook secret on your backend. Never send them to browser code or customers.
Verify every webhookCheck the exact raw body, timestamp, and X-LeadIntel-Signature. Reject requests more than five minutes from the current time.
Prevent duplicatesSave X-LeadIntel-Event-Id before processing the message, and send a unique, non-empty Idempotency-Key with every outbound request.
Check prerequisites and monitorBefore sending, confirm the separate connection record is active, use the applicable customer-service window or approved template, and monitor provider delivery events or recorded conversations when delivery matters. 202 Accepted is not delivery confirmation.
Queue slow workSave the event before returning 202 or 204. Send the reply later through the authenticated connection endpoint.
Remove cleanlyWhen a customer removes WhatsApp, call DELETE /external-platform/whatsapp/connections/{tenantId}/{connectionId}. This one provider-first call deregisters the number at Meta and permanently removes its Lead Intel connection records.
Follow Meta's windowSend text, media, or interactive messages within the 24-hour customer-service window. Outside that window, use an approved template.
Media security Direct meta_change deliveries contain the Meta change value object. For inbound images, video, audio/voice notes, documents, and stickers, the matching media object includes a signed downloadUrl that is valid for at most 10 minutes, or until the delivery deadline when sooner. Download it from your backend before expiresAt; Meta credentials are never returned.