LLM Integration Prompt
Copy the text below and paste it into your LLM (Claude, ChatGPT, etc.) when you need to implement the WhatsApp Cloud API integration in another product. It contains the full API surface, onboarding flow, webhook payloads, and message types — everything an LLM needs to write your integration code.
The Prompt
Paste everything below the line into your LLM conversation.
You are integrating with the WhatsApp Cloud API. This document describes the full API surface you need to implement against. Use the base URL and API key provided in the project's environment configuration.
Authentication
All API requests require a Bearer token:
Authorization: Bearer <API_KEY>
Error responses: 401 (missing/invalid key), 403 (no access).
Data Model
Organization
├── API Keys
├── Onboarding Links
├── Shared Inbox Links
├── Outgoing Webhooks
└── WABAs (WhatsApp Business Accounts)
└── Phone Numbers
└── Conversations
└── Messages
- Organization — Top-level account. All resources belong to an organization.
- WABA — WhatsApp Business Account from Meta. Has its own access token, can have multiple phone numbers.
- Phone Number — A WhatsApp number connected to a WABA. Messages are sent/received through phone numbers.
- Conversation — A thread between a phone number and a contact (identified by phone number).
- Message — A single message within a conversation (inbound or outbound).
- Shared Inbox Link — A public URL bound to one phone number's inbox with a restricted permission set.
IDs are UUID v4 (internal) or numeric strings (Meta IDs in webhook payloads).
Response Format
Success (single): { "data": { ... } }
Success (list): { "data": [...], "meta": { "pagination": { "page": 1, "limit": 50, "total": 120, "hasMore": true } } }
Error: { "error": { "message": "...", "code": "NOT_FOUND" } }
Error codes: VALIDATION_ERROR (400), UNAUTHORIZED (401), FORBIDDEN (403), NOT_FOUND (404), INTERNAL_ERROR (500).
Step 1: Connect a WhatsApp Number (Onboarding Links)
Onboarding links let you onboard end-users' WhatsApp numbers without building any frontend. You create a link via the API, send the hosted URL to your end-user, and they complete Meta's Embedded Signup in the browser. After completion, we redirect back to your app and fire a webhook event.
Flow
- Your Server → API:
POST /v1/onboarding-linkswith{name, externalId, redirectUrl, webhookUrl?, webhookSecret?, webhookEvents?} - API → Your Server: Returns
{id, hostedUrl, status: "pending", ...} - Your Server → End-User: Send the
hostedUrl(email, in-app link, etc.) - End-User → API: Opens
hostedUrlin browser — serves the hosted signup page - End-User → Meta: Clicks "Connect WhatsApp" —
FB.login()popup for Embedded Signup - Meta → End-User: Returns OAuth code after user completes signup
- End-User → API:
POST /signup/:id/callbackwith the OAuth code - API: Processes signup — exchanges code for tokens, sets up WABA & phone number
- API → Your Server: Fires
phone_number.connectedwebhook event - API → End-User: Redirects to
redirectUrl?onboarding_link_id=x&external_id=y&status=success - Your Server → API:
GET /v1/onboarding-links/:id— fetch the completed result
Create an Onboarding Link
POST /v1/onboarding-links
{
"name": "Acme Corp",
"externalId": "customer-123",
"redirectUrl": "https://myapp.com/whatsapp/callback",
"webhookUrl": "https://myapp.com/webhooks/whatsapp",
"webhookSecret": "whsec_my_secret_key_123",
"webhookEvents": ["message.received", "message.sent"]
}
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Display name shown in signup page |
| externalId | string | No | Your internal identifier for this link |
| redirectUrl | string | No | URL to redirect to after signup completes |
| webhookUrl | string | No | URL for an outgoing webhook auto-registered when the phone connects |
| webhookSecret | string | No | HMAC signing secret for the webhook (16-256 chars) |
| webhookEvents | string[] | No | Event types for the webhook (required if webhookUrl is set) |
When webhookUrl is provided, an outgoing webhook is automatically created and scoped to the new WABA once the phone number connects. This saves a separate POST /v1/outgoing-webhooks call.
Response (201):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corp",
"status": "pending",
"externalId": "customer-123",
"redirectUrl": "https://myapp.com/whatsapp/callback",
"hostedUrl": "https://api.example.com/signup/550e8400-...",
"phoneNumberId": null,
"phoneNumber": null,
"wabaId": null,
"isPhoneRegistered": null,
"webhookUrl": "https://myapp.com/webhooks/whatsapp",
"webhookSecret": "whsec_my_secret_key_123",
"webhookEvents": ["message.received", "message.sent"],
"createdAt": "2024-01-15T10:30:00.000Z",
"completedAt": null
}
}
Redirect After Completion
On success: redirectUrl?onboarding_link_id=x&external_id=customer-123&status=success
On error: redirectUrl?onboarding_link_id=x&external_id=customer-123&status=error&error=brief+message
If no redirectUrl is set, the hosted page shows a success/error message inline.
Get Onboarding Link (Enriched)
GET /v1/onboarding-links/{id}
When completed:
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corp",
"status": "completed",
"externalId": "customer-123",
"redirectUrl": "https://myapp.com/whatsapp/callback",
"hostedUrl": "https://api.example.com/signup/550e8400-...",
"phoneNumberId": "660e8400-e29b-41d4-a716-446655440001",
"phoneNumber": "+5491155551234",
"wabaId": "770e8400-e29b-41d4-a716-446655440002",
"isPhoneRegistered": true,
"webhookUrl": "https://api.example.com/webhook/phone/660e8400-...",
"createdAt": "2024-01-15T10:30:00.000Z",
"completedAt": "2024-01-15T10:35:00.000Z"
}
}
Use phoneNumberId from this response to identify the phone, then check readiness before sending traffic.
Check Phone Health / Readiness
GET /v1/phones/{phoneId}/health
Read the fields in data.capabilities:
canReceiveMessages: true= the phone is ready for inbound trafficcanSendMessages: true= the phone is ready for outbound messagingcanSendTemplates: true= the phone is ready for outbound template messagingfalse= the phone is not fully ready yet, even if it is already connected or registered
This endpoint also triggers an asynchronous resync against Meta in the background. If local state was stale, a later call may return updated readiness values.
Other Onboarding Link Endpoints
GET /v1/onboarding-links— List all (with pagination)DELETE /v1/onboarding-links/{id}— Delete (only when status ispending)
Step 2: Set Up Webhooks
Register a webhook to get notified of incoming messages, status updates, and phone connections.
POST /v1/outgoing-webhooks
{
"url": "https://yourapp.com/webhooks/whatsapp",
"secret": "whsec_your_secret_key_min_16_chars",
"events": ["message.received", "message.sent", "phone_number.connected"],
"isActive": true
}
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Your HTTPS endpoint URL |
| secret | string | No | Signing secret for HMAC-SHA256 verification (16-256 chars) |
| events | string[] | Yes | At least one event type |
| isActive | boolean | No | Whether active (default: true) |
Event Types
| Event | Description |
|---|---|
message.received |
Message received from a contact |
message.sent |
Message sent to a contact |
message.status_updated |
Message status changed (delivered, read, failed) |
phone_number.connected |
Phone number connected via onboarding |
* |
Subscribe to all events |
Note: phone_number.connected means onboarding finished. It does not guarantee the phone is operationally ready for traffic. Use GET /v1/phones/{phoneId}/health and check the relevant field in data.capabilities for that.
Optional: Shared Inbox Links
Shared inbox links let you expose one phone number's inbox through the hosted inbox frontend without giving the external user your organization API key.
Create one with:
POST /v1/shared-inbox-links
{
"phoneNumberId": "phone-uuid",
"name": "Support access",
"expiresAt": null,
"permissions": {
"readInbox": true,
"readDetails": true,
"markRead": true,
"sendText": true,
"sendMedia": true,
"sendTemplates": true,
"sendAdvanced": false
}
}
Response includes publicUrl. Send that URL to the external user. The frontend exchanges the public token into a short-lived shared session automatically.
Permission keys:
readInboxreadDetailsmarkReadsendTextsendMediasendTemplatessendAdvanced
Useful endpoints:
GET /v1/shared-inbox-links— List linksGET /v1/shared-inbox-links/{sharedInboxLinkId}— Get onePATCH /v1/shared-inbox-links/{sharedInboxLinkId}— Update name, expiry, or permissionsDELETE /v1/shared-inbox-links/{sharedInboxLinkId}— Revoke immediatelyPOST /v1/shared-inbox-links/{sharedInboxLinkId}/rotate— Generate a fresh public URL
Notes:
- links are scoped to exactly one
phoneNumberId expiresAt: nullmeans no expiration- rotating changes the public URL
- revoking invalidates active shared sessions on the next request
Webhook Payload
{
"event": "message.received",
"timestamp": "2026-03-03T15:30:00.000Z",
"organizationId": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"messageId": "msg-uuid",
"conversationId": "conv-uuid",
"phoneNumberId": "phone-uuid",
"phoneNumber": "+15551234567",
"contactPhone": "5491155551234",
"contactName": "John Doe",
"direction": "inbound",
"type": "text",
"content": { "body": "Hi, I'd like to place an order" },
"wamid": "wamid.HBgLNTQ5MTE1NTU...",
"status": "delivered",
"externalId": "customer-123",
"error": null
}
}
The externalId field is present in all message events (message.received, message.sent, message.status_updated). It contains the externalId from the onboarding link that connected the phone, or null if not applicable.
Media messages: For inbound media messages (image, video, audio, document, sticker), the content object includes both a metadata URL and a direct download URL:
{
"type": "image",
"content": {
"mediaId": "1234567890",
"mediaUrl": "https://api.example.com/v1/phones/phone-uuid/media/1234567890",
"downloadUrl": "https://api.example.com/v1/phones/phone-uuid/media/1234567890/download",
"caption": "Photo of the product"
}
}
Use downloadUrl to stream the actual file bytes.
Use mediaUrl when you need metadata (id, mimeType, fileSize, filename) plus the temporary Meta download URL.
Both endpoints require Authorization: Bearer <apiKey>.
phone_number.connected Payload
{
"event": "phone_number.connected",
"timestamp": "2024-01-15T10:35:00.000Z",
"organizationId": "org_uuid",
"data": {
"onboardingLinkId": "link_uuid",
"externalId": "customer-123",
"phoneNumberId": "phone_uuid",
"phoneNumber": "+5491155551234",
"wabaId": "waba_uuid",
"metaWabaId": "meta_waba_id",
"isPhoneRegistered": true
}
}
HTTP Headers on Every Delivery
| Header | Description |
|---|---|
Content-Type |
application/json |
User-Agent |
WhatsApp-CloudAPI-Webhook/1.0 |
X-Webhook-Event |
The event type |
X-Webhook-Timestamp |
ISO 8601 timestamp |
X-Webhook-Delivery |
Unique delivery ID (for deduplication) |
X-Webhook-Signature-256 |
HMAC-SHA256 signature (only if secret is set) |
Verifying Signatures
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const received = signatureHeader.replace("sha256=", "");
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(received, "hex"));
}
Your endpoint must return a 2xx status within 30 seconds.
Other Webhook Endpoints
GET /v1/outgoing-webhooks— List allGET /v1/outgoing-webhooks/{webhookId}— Get onePATCH /v1/outgoing-webhooks/{webhookId}— UpdateDELETE /v1/outgoing-webhooks/{webhookId}— DeletePOST /v1/outgoing-webhooks/{webhookId}/test— Send test deliveryGET /v1/outgoing-webhooks/{webhookId}/logs— View delivery logs
Step 3: Send Messages
All message types use a single endpoint. The type field determines the payload shape.
POST /v1/phones/{phoneId}/messages
Common fields: type (string, required), to (string, required — recipient phone with country code).
Response: { "data": { "messageId": "uuid", "wamid": "wamid.HBgL..." } }
Text
{ "type": "text", "to": "5491155551234", "text": "Hello!", "previewUrl": false }
Image / Video / Audio / Document / Sticker
By URL:
{ "type": "image", "to": "5491155551234", "mediaUrl": "https://example.com/photo.jpg", "caption": "Check this out!" }
By pre-uploaded media ID:
{ "type": "document", "to": "5491155551234", "mediaId": "1234567890", "caption": "Report", "filename": "report.pdf" }
Either mediaId or mediaUrl must be provided, not both.
Upload media: POST /v1/phones/{phoneId}/media (multipart/form-data, file field) returns { "data": { "id": "1234567890" } }
Template
{
"type": "template",
"to": "5491155551234",
"templateName": "order_confirmation",
"languageCode": "en_US",
"parameters": {
"header": [{ "type": "text", "text": "Order #1234" }],
"body": [{ "type": "text", "text": "John" }, { "type": "text", "text": "$99.00" }],
"buttons": [{ "type": "button", "subType": "url", "index": 0, "parameters": [{ "type": "text", "text": "order-1234" }] }]
}
}
Interactive Button
{
"type": "interactive",
"to": "5491155551234",
"interactiveType": "button",
"body": "How would you like to proceed?",
"header": { "type": "text", "text": "Choose an option" },
"footer": "Reply within 24 hours",
"buttons": [
{ "id": "btn_buy", "title": "Buy Now" },
{ "id": "btn_info", "title": "More Info" }
]
}
Interactive List
{
"type": "interactive",
"to": "5491155551234",
"interactiveType": "list",
"body": "Browse our catalog:",
"buttonText": "View Products",
"sections": [
{ "title": "Electronics", "rows": [
{ "id": "phone_1", "title": "Smartphone X", "description": "Latest model" }
]}
]
}
CTA URL
{
"type": "cta_url",
"to": "5491155551234",
"bodyText": "Visit our store",
"buttonText": "Open Store",
"url": "https://store.example.com",
"headerText": "Special Offer",
"footerText": "Offer expires soon"
}
Flow
{
"type": "flow",
"to": "5491155551234",
"bodyText": "Complete your registration",
"flowToken": "token_abc123",
"flowId": "1234567890",
"flowCta": "Start Registration",
"flowAction": "navigate",
"screen": "WELCOME_SCREEN",
"data": { "userId": "usr_123" }
}
Mark as Read
POST /v1/phones/{phoneId}/messages/{wamid}/read
{ "showTypingIndicator": false }
Step 4: Read Conversations & Messages
List Conversations
GET /v1/phones/{phoneId}/conversations?page=1&limit=50
{
"data": [
{
"id": "conv-uuid",
"contactPhone": "5491155551234",
"contactName": "John Doe",
"lastMessageAt": "2026-03-10T14:30:00.000Z",
"createdAt": "2026-03-01T10:00:00.000Z"
}
],
"meta": { "pagination": { "page": 1, "limit": 50, "total": 12, "hasMore": false } }
}
Get Messages
GET /v1/phones/{phoneId}/conversations/{conversationId}/messages?page=1&limit=50
{
"data": [
{
"id": "msg-uuid",
"wamid": "wamid.HBgLNTQ5MTE1NTU...",
"direction": "inbound",
"type": "text",
"content": { "body": "Hi there!" },
"status": "delivered",
"timestamp": "2026-03-10T14:30:00.000Z"
}
]
}
Message Statuses (outbound only)
pending → sent → delivered → read (or failed at any point)
Content by Message Type
| Type | Content |
|---|---|
| text | { body } |
| image | { url, caption?, mimeType? } |
| video | { url, caption?, mimeType? } |
| audio | { url, mimeType? } |
| document | { url, caption?, filename? } |
| sticker | { url } |
| location | { latitude, longitude, name? } |
| contacts | { contacts: [] } |
| interactive | { type, ... } (button/list reply) |
| button | { text, payload } |
| reaction | { emoji } |
Implementation Checklist
- Create onboarding link —
POST /v1/onboarding-linkswithredirectUrlpointing to your app. Optionally includewebhookUrl,webhookSecret, andwebhookEventsto auto-register a webhook when the phone connects. - Display hosted URL — Show
hostedUrlto end-users so they can connect their WhatsApp number - Handle redirect — Parse
onboarding_link_id,external_id, andstatusfrom redirect query params - Handle webhook — Listen for
phone_number.connectedfor server-to-server reliability - Fetch result —
GET /v1/onboarding-links/:idto getphoneNumberId,phoneNumber,wabaId - Store phone number ID — Save
phoneNumberId— this is the key for all messaging operations - Register webhook —
POST /v1/outgoing-webhooksto receivemessage.receivedevents (skip if you usedwebhookUrlin step 1) - Send messages —
POST /v1/phones/{phoneId}/messageswith the appropriate type - Verify webhook signatures — HMAC-SHA256 with your secret for security
- Read conversations —
GET /v1/phones/{phoneId}/conversations+ messages endpoint - Handle media in webhooks — Use
content.downloadUrlto download the file bytes, orcontent.mediaUrlto fetch metadata (both require Bearer auth)