WhatsApp API
Send WhatsApp messages with RelayAPI — text messages, images, videos, documents, templates, and link previews via the WhatsApp Business Cloud API.
Quick Reference
| Property | Value |
|---|---|
| Platform key | whatsapp |
| Auth method | WhatsApp Business Cloud API Token |
| Text limit | 4,096 characters |
| Caption limit | 1,024 characters (media captions) |
| Media per message | 1 |
| Image formats | JPEG, PNG |
| Image max size | 5 MB |
| Video formats | MP4, 3GPP |
| Video max size | 16 MB |
| Message types | Text, Image, Video, Document, Audio, Template |
| Administration | Capability-gated groups, block lists, business usernames, and template library/editing |
| Scheduling | Yes |
| Analytics | No (use WhatsApp Business Manager) |
Client options — TypeScript · Python REST/OpenAPI · Go · Java · REST API
Before You Start
WhatsApp requires a WhatsApp Business Account with access to the Cloud API (via Meta for Developers). Messages outside the 24-hour conversation window require pre-approved message templates. Every message requires a recipient phone number in E.164 format (without the + prefix). Only 1 media item per message is supported. Image, video, and document captions are limited to 1,024 characters. Audio attachments cannot include post content as a caption; RelayAPI rejects that combination before provider I/O.
WhatsApp administration and messaging require an embedded token with both
whatsapp_business_management and whatsapp_business_messaging. Reconnect or
repeat embedded signup for an older connection whose token lacks either grant;
refreshing the existing token cannot add permissions.
Quick Start
Send a text message via WhatsApp:
import Relay from '@relayapi/sdk';
const client = new Relay();
const post = await client.posts.create({
content: 'Hello from RelayAPI!',
targets: ['whatsapp'],
scheduled_at: 'now',
target_options: {
whatsapp: {
to: '14155238886'
}
}
});
console.log(post.id); // post_abc123Content Types
Text Message
Plain text message up to 4,096 characters.
const post = await client.posts.create({
content: 'Hello from RelayAPI!',
targets: ['whatsapp'],
scheduled_at: 'now',
target_options: {
whatsapp: {
to: '14155238886'
}
}
});Text with Link Preview
Enable URL previews in text messages.
const post = await client.posts.create({
content: 'Check out our new feature: https://example.com/launch',
targets: ['whatsapp'],
scheduled_at: 'now',
target_options: {
whatsapp: {
to: '14155238886',
preview_url: true
}
}
});Image Message
Send a single image with an optional caption (max 1,024 chars).
const post = await client.posts.create({
content: 'Check out this photo!',
targets: ['whatsapp'],
media: [
{ url: 'https://cdn.example.com/photo.jpg', type: 'image' }
],
scheduled_at: 'now',
target_options: {
whatsapp: {
to: '14155238886'
}
}
});Video Message
const post = await client.posts.create({
content: 'Watch our latest update!',
targets: ['whatsapp'],
media: [
{ url: 'https://cdn.example.com/video.mp4', type: 'video' }
],
scheduled_at: 'now',
target_options: {
whatsapp: {
to: '14155238886'
}
}
});Document Message
Send a file as a document attachment.
const post = await client.posts.create({
content: 'Here is your invoice.',
targets: ['whatsapp'],
media: [
{ url: 'https://cdn.example.com/invoice.pdf', type: 'document' }
],
scheduled_at: 'now',
target_options: {
whatsapp: {
to: '14155238886'
}
}
});Template Message
Use pre-approved message templates for outbound messages outside the 24-hour window.
const post = await client.posts.create({
targets: ['whatsapp'],
scheduled_at: 'now',
target_options: {
whatsapp: {
to: '14155238886',
template_name: 'order_confirmation',
template_language: 'en_US',
template_components: [
{
type: 'body',
parameters: [
{ type: 'text', text: 'John' },
{ type: 'text', text: '#12345' }
]
}
]
}
}
});Templates must be pre-approved in the WhatsApp Business Manager before use. Template messages bypass the 24-hour conversation window restriction.
Media Requirements
Images
| Property | Requirement |
|---|---|
| Max per message | 1 |
| Formats | JPEG, PNG |
| Max file size | 5 MB |
Videos
| Property | Requirement |
|---|---|
| Max per message | 1 |
| Formats | MP4, 3GPP |
| Max file size | 16 MB |
Documents
| Property | Requirement |
|---|---|
| Max per message | 1 |
| Formats | PDF, DOC, DOCX, PPT, PPTX, XLS, XLSX, and more |
| Max file size | 100 MB |
target_options Fields
All fields go inside target_options.whatsapp on your post request.
| Field | Type | Default | Description |
|---|---|---|---|
content | string | — | Override message content for WhatsApp specifically |
media | object[] | — | Override media for WhatsApp specifically |
to | string | — | Required. Recipient phone number in E.164 format without + (e.g., "14155238886") |
preview_url | boolean | false | Enable link preview in text messages |
template_name | string | — | Pre-approved template name for outbound messages |
template_language | string | "en_US" | Template language code |
template_components | object[] | — | Template parameter components |
Delivery status lifecycle
The Cloud API's message-create response supplies a WhatsApp message ID and confirms acceptance, not recipient delivery. RelayAPI keeps that outbound post target nonterminal while it waits for a signed WhatsApp status callback.
| Provider event | RelayAPI outcome | Terminal? |
|---|---|---|
| Initial message acknowledgement | Accepted; message ID retained for callback correlation | No |
sent | Accepted by WhatsApp, but not proven delivered to the recipient | No |
delivered | Delivered | Yes, success |
read | Delivered/read | Yes, success |
failed | Failed, with Meta's error code/message when supplied | Yes, failure |
Status callbacks are processed only after RelayAPI verifies the raw request's
X-Hub-Signature-256 with the configured Meta app secret. Callback delivery is
at-least-once and can be reordered, so RelayAPI applies monotonic transitions:
a duplicate or late sent/delivered event cannot regress a terminal read
or failed result. A terminal callback persists the target and its publish
attempt together and schedules any parent-post/thread continuation in the same
fenced transaction.
If no terminal callback arrives, the target remains nonterminal; do not treat a
sent event as proof of handset delivery.
Common Errors
| Error | Cause | Fix |
|---|---|---|
| Missing recipient | No to phone number provided | Add to in target_options.whatsapp with E.164 format (without +). |
| Content too long | Text exceeds 4,096 characters | Shorten the content. |
| Empty content | No content or media provided | Add text content or at least one media item. |
| Template not found | Template name not approved or misspelled | Verify the template exists and is approved in WhatsApp Business Manager. |
| Outside conversation window | Sending non-template message outside 24h window | Use a template message instead. |
Known Quirks
- 24-hour conversation window — you can only send free-form messages within 24 hours of the user's last message. Outside this window, use templates.
- 1 media item per message — no multi-image or gallery support.
- Audio messages have no caption — captions are only supported on image, video, and document messages. A publisher request that combines audio with nonblank post content or a WhatsApp content override is rejected before provider I/O.
- Media captions limited to 1,024 characters — shorter than the 4,096-character text limit.
- Phone numbers use E.164 format without the
+— e.g.,14155238886not+14155238886. - Template messages require pre-approval — submit templates in the WhatsApp Business Manager.
- Media must be publicly accessible via HTTPS — private URLs will fail.
RelayAPI also sends provider-native read receipts for persisted inbound WhatsApp messages. See Published Edits and Social Actions.
Business administration
The authenticated /v1/whatsapp/admin surface uses the exact connected
WhatsApp phone-number identity and its stored WABA. Start with a live capability
probe rather than assuming every Meta account has the same rollout:
const capabilities = await client.whatsapp.admin.capabilities({
account_id: 'acc_whatsapp123',
});
if (capabilities.capabilities.groups !== 'supported') {
console.log(capabilities.requirements);
}Every field uses the same conservative state vocabulary:
| State | Meaning |
|---|---|
supported | A feature-specific, non-mutating read succeeded for this exact connected account |
requires_eligibility | RelayAPI implements the write/webhook feature, but no safe read can prove this account's program eligibility or app configuration |
unavailable | The feature-specific read was rejected as unauthorized, forbidden, or not found, or the required WABA identity is absent |
unverified | A generic provider rejection or transient failure did not prove support either way |
not_yet_available | Reserved for an implementation that is not exposed yet |
Groups, block lists, business usernames, and the template library each have an independent read probe. Template editing and BSUID features cannot be certified with a non-mutating account probe, so their state is eligibility-based. Treat the snapshot as implementation and account evidence, not as a promise that a later provider mutation will be accepted.
Meta currently restricts the Groups API to eligible Official Business Accounts
without coexistence. RelayAPI returns WHATSAPP_GROUPS_UNAVAILABLE before a
group write unless the exact Groups read probe returned supported. A
non-supported state does not by itself diagnose which Meta eligibility or
configuration requirement is missing.
Groups and participants
const operation = await client.whatsapp.admin.createGroup({
account_id: 'acc_whatsapp123',
idempotency_key: 'create-customer-group-20260810',
subject: 'Customer launch group',
description: 'Launch coordination',
join_approval_mode: 'approval_required',
});
const current = await client.whatsapp.admin.getOperation(operation.id, {
account_id: 'acc_whatsapp123',
});Group create and delete are asynchronous at Meta. A provider acknowledgement
does not prove the group is active or deleted: RelayAPI keeps a local
creating/deleting projection and applies the terminal lifecycle from signed
group_create/group_delete webhooks. List or retrieve groups after the
provider lifecycle event to get the current projection.
| Capability | TypeScript SDK method |
|---|---|
| List, create, retrieve, update, delete | listGroups, createGroup, retrieveGroup, updateGroup, deleteGroup |
| Read or rotate invite link | getInviteLink, resetInviteLink |
| List and approve/reject join requests | listJoinRequests, approveJoinRequests, rejectJoinRequests |
| Remove participants | removeParticipants (1–8 identities) |
| Send text, image, video, document, audio, or template | sendGroupMessage |
| Pin or unpin a group message | pinGroupMessage |
RelayAPI does not invent a manual-participant-add operation because Meta's
official contract does not provide one. Use invite links and join approvals.
Group media accepts exactly one Meta media id or public link. Pinning
requires expiration_days from 1–30; unpinning must omit it.
Block lists
await client.whatsapp.admin.blockUsers({
account_id: 'acc_whatsapp123',
idempotency_key: 'block-abuse-batch-1',
users: [{ user: '15550001111' }],
});
const blocked = await client.whatsapp.admin.listBlockedUsers({
account_id: 'acc_whatsapp123',
limit: 100,
});Use unblockUsers() with the same input shape to remove entries. Meta can
partially apply a batch, so inspect the durable operation result rather than
assuming all identities changed together.
Business usernames and BSUIDs
getUsername(), setUsername(), deleteUsername(), and
usernameSuggestions() expose Meta's business-username contract. Username
activation is asynchronous; poll getUsername() for RESERVED or ACTIVE.
RelayAPI intentionally does not subscribe to
phone_number_username_update until that webhook can be resolved to one exact
connected phone number without ambiguity.
WhatsApp webhook ingestion accepts phone-optional business-scoped user IDs (BSUIDs). RelayAPI derives tenant-separated conversation keys, encrypts the raw BSUID at rest, and resolves it only at the provider boundary for a reply in the same authorized conversation. Callers do not supply or retrieve the stored raw BSUID.
Template library and editing
const library = await client.whatsapp.admin.templateLibrary({
account_id: 'acc_whatsapp123',
category: 'UTILITY',
language: 'en_US',
});
await client.whatsapp.admin.createTemplateFromLibrary({
account_id: 'acc_whatsapp123',
idempotency_key: 'create-order-update-template',
name: 'order_update',
language: 'en_US',
category: 'UTILITY',
library_template_name: 'order_update_1',
});editTemplate() updates only Meta's documented editable fields: components,
category, parameter format, message-send TTL, and CTA link-tracking opt-out.
Template review/approval remains authoritative at Meta.
Durable mutation behavior
Every WhatsApp admin write requires Idempotency-Key (idempotency_key in the
TypeScript SDK) and returns a durable operation. Reuse the same key only for the
same logical write. An ambiguous provider outcome is parked as unknown and is
not replayed automatically. Poll it with client.whatsapp.admin.getOperation()
before deciding whether any manual provider reconciliation is needed. See
Published Edits and Social Actions
for the shared status contract.
An HTTP 2xx alone is not enough to complete an administration operation.
RelayAPI requires the operation-specific success flag or durable group,
request, template, or message ID. An unrecognized successful response is
recorded as WHATSAPP_PROVIDER_RESPONSE_INVALID with an unknown outcome so it
cannot be replayed as though Meta had proved no effect.
RelayAPI does not provide WhatsApp calling/telephony, a proprietary sandbox, manual participant addition, or proprietary template/Flow version history. Those are outside this administration contract.
Automations
WhatsApp is a Tier 1 automation platform. Template messages are required for any send outside the 24-hour customer-service window.
Triggers
| Type | Fires on |
|---|---|
whatsapp_message | Inbound message (text / media / sticker / reaction etc.) |
whatsapp_keyword | Reserved trigger alias; use whatsapp_message with keyword config today |
whatsapp_button_click | Reply from an interactive button |
whatsapp_list_reply | Reply from an interactive list |
whatsapp_flow_submit | User completed a Flow |
whatsapp_reaction | Message reaction |
whatsapp_status_update | Delivery / read status change |
Send nodes
All nodes hit POST {GRAPH_BASE.facebook}/{phone-number-id}/messages with messaging_product: "whatsapp".
| Node | Required fields |
|---|---|
whatsapp_send_text | text, optional preview_url |
whatsapp_send_media | url, media_type ∈ image|video|audio|document|sticker; optional caption only for image, video, or document. RelayAPI rejects audio captions before provider I/O; sticker captions are unsupported by the provider. |
whatsapp_send_template | template_name, language (default en_US), optional components |
whatsapp_send_interactive | text + either buttons[] (≤3 reply buttons) or list (sectioned rows) |
whatsapp_send_flow | flow_id, flow_token, text, optional cta / flow_action |
whatsapp_send_location | latitude, longitude, optional name / address |
whatsapp_send_contacts | contacts[] (array of WA contact objects) |
whatsapp_react | emoji, optional message_id (defaults to inbound message from state) |
whatsapp_mark_read | optional message_id |
Outside the 24-hour window, only pre-approved template messages can be sent. Use whatsapp_send_template; free-form whatsapp_send_text will be rejected by Meta.
Official WhatsApp references
- Meta's WhatsApp Cloud API documentation
- Meta WhatsApp Groups documentation
- Meta business-scoped user IDs
- Meta template library
- Meta's official Block Users API request
- Webhook payload and message-status reference
- Message status update notifications
Found something wrong? Help us improve this page.