Connect, receive, and send WhatsApp messages
This guide follows the order you will implement the integration: create a connection request, wait until the number is connected, receive signed messages, then reply or send new messages.
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. The response tells you whether to keep waiting, start using the connection, or show an error.
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.connectedStop checking. The number is ready. Use externalPlatformTenantId and externalConnectionId for connection and send endpoints.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.
Manage a connected number when needed
After setup returns connected, stop using requestId. Use your tenantId and connectionId for every endpoint in this section.
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, or template.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.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 Meta accepted the send request. Reuse the same Idempotency-Key only when retrying the same recipient and message; using it for 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, or template 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.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 or media. Outside that window, send an approved template. Include an idempotency key so a retry cannot send the message twice.
/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.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.HTTP/1.1 202 Accepted
{ "messageId": "wamid....", "status": "accepted" }
What you get: 202 means Meta accepted the message and returns its messageId when available. It does not guarantee final delivery. 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.
{
"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.
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 Idempotency-Key with every outbound request.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.