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.
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.
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.
externalPlatformTenantId. Create one externalConnectionId for each WhatsApp number. You will use both IDs after setup to read, update, disconnect, and send through that connection.
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
externalPlatformTenantId identifies the customer or account that owns the number in your platform.externalConnectionId is a unique ID from your platform for this WhatsApp connection. Keep using the same ID until the connection is disconnected.countryCode is the country calling code and phoneNumber is the local number, both sent as digits.businessName is the business being connected. displayName is the name requested for the WhatsApp profile.externalUserId associates the connection with a specific user in your tenant. Lead Intel stores and returns it with the connection.inboundWebhookUrl is a publicly reachable HTTPS endpoint for this specific WhatsApp connection. Lead Intel sends signed incoming messages there.inboundWebhookSecret is generated and stored by you. Use it to verify Lead Intel's inbound signatures. API replies use the platform key, not this secret.inboundWebhookTimeoutMs 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
profileImageUrl 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.about is the short profile text, up to 139 characters.description explains the business, up to 512 characters.address is the business address, up to 256 characters.email is the public business contact email.websites accepts up to two website URLs.vertical is Meta's business category./external-platform/whatsapp/assisted-requestsCreate one request for each WhatsApp number. Call this endpoint from your backend; browser requests and frontend callbacks are not supported.
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
}
{
"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.
/external-platform/whatsapp/assisted-requests/:requestIdUse 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
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.
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.
/external-platform/whatsapp/connections/lookupCall 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.
+ 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.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.
{
"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
- Save
externalPlatformTenantIdas yourtenantIdandexternalConnectionIdas yourconnectionIdagainst the correct customer in your system. Finding a number does not authenticate that customer; enforce your own customer permissions before exposing it. - If
statusisactive, reuse the existing connection. SavephoneNumberIdto identify incoming webhooks. - 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. - 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
invalid_phone_numberSupply a valid international-format string in phoneNumber, including the leading + and country code.invalid_platform_keyThe key is missing, invalid, or revoked. Check your backend key configuration; do not start number setup again.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.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.Retry-After if present. Save recovered IDs rather than looking up the phone before every message.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.
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.
/external-platform/whatsapp/existing-numbers/lookupCheck whether a number is connected in Lead Intel and can be claimed. Body: {"phoneNumber":"+254700000000"}.
{ "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.
/external-platform/whatsapp/existing-numbers/claimsSends the SMS code to the number and returns a claimId. The code expires in 10 minutes.
+.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"}'
{ "claimId": 17, "status": "code_sent", "sentTo": "+254700000000", "expiresAt": "2026-10-12T21:10:00.000Z" }
/external-platform/whatsapp/existing-numbers/claims/{claimId}/verifyBody: {"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.
{
"status": "connected",
"externalPlatformTenantId": "tenant_42",
"externalUserId": null,
"externalConnectionId": "wa_primary",
"phoneNumberId": "123456789012345",
"displayPhoneNumber": "+254700000000"
}
Errors and recovery
invalid_phone_number / invalid_claim_requestFix the request body.invalid_codeWrong code; attemptsLeft says how many tries remain (5 per code).number_not_connectedLead Intel doesn't hold this number. Use assisted setup.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.code_expiredThe code expired, was used, or ran out of attempts. Start a new claim.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.
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.
/external-platform/whatsapp/connections/:tenantId/:connectionIdUse 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
{
"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"
}
/external-platform/whatsapp/connections/:tenantId/:connectionIdUpdate 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
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./external-platform/whatsapp/connections/:tenantId/:connectionIdThis 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
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.
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.
your registered inboundWebhookUrlEvery 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
X-LeadIntel-Event-Id. If you already processed it, return the same successful result without repeating your work.metadata.phone_number_id to the phoneNumberId returned by the connection endpoint.messages[i].from, the message type from messages[i].type, and its content from the matching field.200, or save the event and return 202/204 to reply later.Incoming request parameters
value object. Use metadata.phone_number_id to identify the connection and read incoming messages from messages[].Content-Type: application/json
X-LeadIntel-Event-Id: external-message:...
X-LeadIntel-Timestamp: 1788206400000
X-LeadIntel-Signature: lowercase-hex-hmac-sha256
{
"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
X-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.metadata.phone_number_id to the phoneNumberId returned by the connection status endpoint. Do not require tenant or connection IDs in the webhook body.messages[0].from is the WhatsApp user who sent the message and is the to value when replying through the send endpoint.value object. It does not include Meta's outer entry[].changes[] wrapper, and Lead Intel does not add product, recipient, or callback fields.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.// 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.
HMAC-SHA256Build 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
X-LeadIntel-Timestamp, included exactly as received.const expected = createHmac('sha256', inboundWebhookSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
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.
Immediate replyUse this when the reply is ready before the webhook timeout. The response body becomes the WhatsApp reply to the sender.
Reply parameters
text, media, template, or an interactive type: buttons, list, link_button (see Buttons and menus).mediaType and a public HTTPS url; caption and filename are optional.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.
{
"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.
Asynchronous replyUse 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
messages[i].from value.buttons, list, link_button) fields using the same format as an immediate reply.400 with valid_idempotency_key_required.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.
Webhook response handlingReview 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.2xxReturn valid immediate reply JSON in the body. An empty or invalid body is treated as a failed delivery.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).
buttons{"id", "title"}. id up to 256 characters, title up to 20. Both must be unique.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" }
]
}
listheader and footer are optional (60 characters).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.{
"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" }
] }
]
}
link_buttonheader and footer are optional (60 characters).{
"type": "link_button",
"body": "Your invoice is ready.",
"displayText": "Pay now",
"url": "https://pay.example.com/inv/123"
}
When the customer tapsThe tap arrives as a normal incoming message on your webhook. Route on the id you set, not on the title.
messages[i].type is interactive; read messages[i].interactive.button_reply.id and .title.messages[i].type is interactive; read messages[i].interactive.list_reply.id, .title and .description.messages[i].type is button; read messages[i].button.payload and .text.{
"messages": [{
"from": "254700000000",
"type": "interactive",
"interactive": {
"type": "button_reply",
"button_reply": { "id": "book_demo", "title": "Book a demo" }
}
}]
}
Errors
invalid_whatsapp_messageSend endpoint only: a limit was broken (for example 4 buttons or a 21-character title). detail says which.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.send_failedMeta refused the message, most often because the 24-hour window has closed. Send an approved template instead.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.
/external-platform/whatsapp/connections/:tenantId/:connectionId/sendChoose the active connection in the URL and put the recipient in to. This minimum example sends text.
Authorization: Bearer li_platform_...
Idempotency-Key: prosuite-message-abc-123
Content-Type: application/json
{
"to": "254700000000",
"text": "Your message"
}
Request parameters
type: "text" is optional.400 with valid_idempotency_key_required.Outbound message fields
to is the recipient's WhatsApp phone number.to and non-empty text up to 4,000 characters. type: "text" may be included but is not required.type: "media", a supported mediaType, and a public HTTPS url. Caption and filename are optional.type: "template" with the approved template name, language, and optional components.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.
{
"to": "254700000000",
"type": "media",
"mediaType": "image",
"url": "https://cdn.prosuite.example/property.jpg",
"caption": "Available property"
}
{
"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.
/external-platform/whatsapp/connections/:tenantId/:connectionId/templatesCreate 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.
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
{{1}} (customer name) and {{2}} (your business or product name) are supported, used in order.en.MARKETING (default) or UTILITY.none (default), image or video. Media headers require the sample file.headerType is not none.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.
/external-platform/whatsapp/connections/:tenantId/:connectionId/templatesList 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.
/external-platform/whatsapp/connections/:tenantId/:connectionId/templates/:nameDelete 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.
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.
/external-platform/leads/:externalPlatformTenantId/:leadId/responseAuthenticate 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.
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.
{
"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.
Go-live checklist
Complete these checks before you use the integration with real customers.
Go-live checklist
X-LeadIntel-Signature. Reject requests more than five minutes from the current time.X-LeadIntel-Event-Id before processing the message, and send a unique, non-empty Idempotency-Key with every outbound request.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.202 or 204. Send the reply later through the authenticated connection endpoint.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.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.