WhatsApp API
WhatsApp Business runs on Meta’s Cloud API. A WhatsApp profile is a messaging profile: it sends and receives direct messages, manages the message templates that open conversations, and administers the phone numbers, business profile, blocked users, groups and Click-to-WhatsApp conversions of a WhatsApp Business Account (WABA).
It does not publish posts. There are no comments and no post insights — POST /api/posts with a WhatsApp profile returns 422, and WhatsApp profiles don’t appear in the app’s composer.
Conversations themselves — chats, sending, receiving, reactions, read receipts, typing — live in the Direct Messages API and behave like the other DM networks. This page covers the WhatsApp-specific parameters and everything around the conversation.
Every request below uses the base URL https://api.postproxy.dev and an Authorization: Bearer YOUR_API_KEY header. Replace YOUR_API_KEY and the example IDs with your own.
Profiles and profile groups
Section titled “Profiles and profile groups”A connected WABA is one profile, and each phone number on it is a placement of that profile. Reference the profile in a request by its id (the prof_abc123 in the examples below) or by the platform name "whatsapp", which selects the group’s WhatsApp profile (a group holds at most one profile per platform). List what’s connected with GET /api/profiles.
Profiles live in profile groups — containers that organize the accounts for one brand, client, or project. List groups with GET /api/profile_groups, and connect a WABA with the Initialize Connection endpoint — see Connecting a WhatsApp Business Account below.
Example: get profiles and profile groups
List the profiles you can message from — GET /api/profiles:
curl -X GET "https://api.postproxy.dev/api/profiles" \ -H "Authorization: Bearer YOUR_API_KEY"{ "data": [ { "id": "prof_abc123", "name": "Acme Coffee", "platform": "whatsapp", "status": "active", "profile_group_id": "grp_xyz789", "expires_at": null, "post_count": 0, "avatar_url": null } ]}List your profile groups — GET /api/profile_groups:
curl -X GET "https://api.postproxy.dev/api/profile_groups" \ -H "Authorization: Bearer YOUR_API_KEY"{ "data": [ { "id": "grp_xyz789", "name": "Main Brand", "profiles_count": 5 }, { "id": "grp_def456", "name": "Client Project", "profiles_count": 3 } ]}At a glance
Section titled “At a glance”| Platform ID | whatsapp |
| Publishing | No — messaging only. POST /api/posts returns 422 |
| Formats | — |
| Media | Images 5 MB, video and audio 16 MB, documents 100 MB |
| Placements | Phone numbers — the placement id is Meta’s phone_number_id |
| Comments | No |
| Direct messages | Yes — text, media, templates, interactive messages, location, contacts, reactions, read receipts |
| Messaging window | 24 hours from the customer’s last message. Outside it: a template, or a utility Direct Send |
| Group chats | Yes — inbound and outbound |
| Post chains | No |
Connecting a WhatsApp Business Account
Section titled “Connecting a WhatsApp Business Account”WhatsApp connects through Meta’s Embedded Signup: a Facebook login that grants Postproxy access to a WABA and its phone numbers. Start it like any OAuth network with Initialize Connection:
curl -X POST "https://api.postproxy.dev/api/profile_groups/grp_xyz789/initialize_connection" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "whatsapp", "redirect_url": "https://myapp.com/oauth/callback", "onboarding": "api" }'The onboarding parameter
Section titled “The onboarding parameter”onboarding says where the number lives today. It is WhatsApp-only.
onboarding | Use when | What happens |
|---|---|---|
business_app | The number is in the WhatsApp Business app on a phone and the team answers customers from it | Coexistence. The user types the number, Meta shows a QR code, they scan it from WhatsApp Business (Settings → Linked devices). The phone keeps working, Postproxy is added next to it and sees the same chats, and up to 6 months of history is imported |
api | The number is already on the Cloud API (another provider, your own Meta setup), or is a fresh number that will only be used from Postproxy | Standard Cloud API onboarding. The number is claimed for the API and stops working in the WhatsApp app. A new number is verified by SMS or voice call during signup. No old chats are imported |
| (omitted) | You don’t know | The connect page opens with a “Where is your number now?” chooser that explains both options and lets the user pick before the Facebook login |
Meta asks the user to connect their account to 64Bit Labs — the company behind Postproxy.
Coexistence limits
Section titled “Coexistence limits”A coexistence number (one still living in the WhatsApp Business app) carries Meta-imposed limits worth telling your users about up front:
- Lower sending throughput than an API-only number.
- No Groups API access.
- Meta disconnects the number if the phone stays offline for about 14 days.
- Numbers in the personal WhatsApp app aren’t eligible — they have to move to the free WhatsApp Business app first.
- Very new numbers with no chat history can be refused by Meta.
A coexistence number reports a platform_type other than CLOUD_API in its placement metadata.
What a connection creates
Section titled “What a connection creates”One connected WABA becomes one profile (platform: "whatsapp", platform_id = the WABA id, name = the WABA name), and each of its phone numbers becomes a placement. If the Facebook login grants several WABAs at once, one profile is created per WABA.
After profile.connected, Postproxy:
- Pulls the phone numbers into placements —
profile.placements_syncedfires when they’re ready. - Subscribes the WABA to webhooks, so messages, template reviews and number updates arrive.
- Registers every number for Cloud API messaging. A number that already has its own two-step verification PIN keeps working for everything except sending until it is re-registered with that PIN; Meta’s error is recorded on the placement as
metadata.registration_warning. - Mirrors the WABA’s message templates.
The profile’s platform_url is https://wa.me/<first number> and username repeats the WABA name.
A WhatsApp profile is disconnected (profile.disconnected with origin: "automated") when Meta reports that Postproxy was removed from the WABA, the WABA was disabled or deleted, or the token stopped working. Reconnect with refresh_connection: true.
Phone numbers
Section titled “Phone numbers”Every phone number on the WABA is a placement of the profile. Its id is Meta’s phone number id, and that same value is:
- the
phone_number_idevery number-level endpoint on this page takes, - the
placement_idyou pass when creating a chat, - the
external_placement_idon every WhatsApp chat.
curl -X GET "https://api.postproxy.dev/api/profiles/prof_abc123/placements" \ -H "Authorization: Bearer YOUR_API_KEY"{ "data": [ { "id": "106540352242922", "name": "Acme Coffee", "avatar_url": null, "metadata": { "display_phone_number": "+1 555-010-0001", "quality_rating": "GREEN", "messaging_limit_tier": "TIER_1K", "name_status": "APPROVED", "status": "CONNECTED", "platform_type": "CLOUD_API", "is_official_business_account": false, "throughput": { "level": "STANDARD" } } } ]}metadata key | Description |
|---|---|
display_phone_number | The number in international format |
quality_rating | Meta’s quality rating: GREEN, YELLOW, RED, or UNKNOWN |
messaging_limit_tier | Business-initiated conversations per 24 h: TIER_50, TIER_250, TIER_1K, TIER_10K, TIER_100K, TIER_UNLIMITED |
name_status | Review state of the display name: APPROVED, PENDING_REVIEW, DECLINED, EXPIRED, AVAILABLE_WITHOUT_REVIEW, NONE |
status | Number status on the Cloud API — e.g. CONNECTED, PENDING, OFFLINE, FLAGGED, RESTRICTED |
platform_type | CLOUD_API for an API-only number. Anything else (SMB_APP, NOT_APPLICABLE) marks a coexistence number still living in the WhatsApp Business app |
is_official_business_account | Whether the number carries the green Official Business Account badge |
throughput | Meta’s throughput object — { "level": "STANDARD" | "HIGH" } |
registration_warning | Present only when the registration after connect failed; carries Meta’s error message. Clears once Register Phone Number succeeds |
Metadata is refreshed on every Number Info call and whenever Meta pushes a quality, name or account update — see Webhooks.
Numbers can be assigned to other profile groups like any placement. A group-scoped key only sees the numbers in its group, and every endpoint on this page resolves phone_number_id through the placements that key can reach — a number outside the scope returns 404.
Direct messages
Section titled “Direct messages”WhatsApp conversations use the Direct Messages API — the same chats and messages endpoints as Messenger, Instagram, Telegram and Bluesky.
| Capability | Supported |
|---|---|
| Send / receive text | Yes |
| Attachments | Yes — image, video, audio, document, sticker |
| Message templates | Yes — the way to message outside the 24-hour window |
| Interactive messages | Yes — reply buttons, lists, CTA URLs, catalog products, Flows |
| Location and contacts | Yes, both directions |
| Reply threading | Yes — reply_to_external_id quotes a message |
| Reactions | Yes, both directions — any emoji |
| Read receipts | Yes — Mark read sends the blue ticks |
| Typing indicator | Yes |
| Edit outbound message | No — PATCH /api/messages/:id returns 422 |
| Group chats | Yes |
| Meta quick replies / buttons | No — use interactive or template buttons |
HUMAN_AGENT tag | No — Facebook and Instagram only |
| Private reply to comment | No |
| Backfill chats | No — history only arrives via coexistence onboarding |
| Inbound delivery | Webhook |
Chats are pinned to a number
Section titled “Chats are pinned to a number”POST /api/profiles/:id/chats requires placement_id on WhatsApp — the phone number the conversation runs on. A customer who writes to two of your numbers has two chats.
participant_external_id is the customer’s phone number. Pass it in any format; it’s reduced to digits, Meta’s wa_id form (13105550007).
curl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/chats" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "placement_id": "106540352242922", "participant_external_id": "+1 310 555 0007" }'WhatsApp chats carry external_placement_id (the number), group, and within_messaging_window.
The 24-hour window
Section titled “The 24-hour window”Meta only permits free-form messages within 24 hours of the customer’s last inbound message. A Click-to-WhatsApp referral opens the window too.
within_messaging_window on the chat tells you which case you’re in. Outside the window there are exactly two ways through:
- Send an approved message template — also the only way to start a conversation.
- Send a plain text with
category: "utility", where the account is eligible for Meta’s Direct Send.
WhatsApp has no HUMAN_AGENT tag — that’s a Facebook and Instagram mechanism.
One payload per send
Section titled “One payload per send”Exactly one of body, media, template, interactive, location or contacts per call to POST /api/chats/:id/messages. Outbound media carries no caption.
| Parameter | Type | Description |
|---|---|---|
body | string | Message text. Free-form sends need an open window |
media | array | One attachment URL |
template | object | An approved message template. Works inside or outside the window |
interactive | object | Reply buttons, lists, CTA URLs, products, Flows. Session message — needs an open window |
location | object | { "latitude", "longitude", "name", "address" } — latitude and longitude required |
contacts | array | Meta contact objects |
category | string | "utility" for Direct Send. Only valid with body |
reply_to_external_id | string | The wamid of a message in the chat — renders the send as a quoted reply |
link_preview | boolean | Render a preview for a link in body |
voice_note | boolean | Send an audio attachment as a voice note |
filename | string | Filename shown for a document attachment |
quick_replies, buttons and card are Facebook and Instagram parameters and return 422 on a WhatsApp chat. reply_markup stays Telegram-only.
# Free-form reply inside the 24-hour windowcurl -X POST "https://api.postproxy.dev/api/chats/chat_wa789/messages" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "body": "Your order is roasting now — it ships tomorrow." }'Media limits
Section titled “Media limits”Enforced at send time.
| Media | Max size |
|---|---|
| Image | 5 MB |
| Video | 16 MB |
| Audio | 16 MB |
| Document | 100 MB |
Inbound attachments are mirrored to Postproxy storage and served from attachments[].url — Meta’s own media URLs expire and require the WABA token.
Interactive messages
Section titled “Interactive messages”interactive takes Meta’s interactive object unchanged. Reply buttons:
curl -X POST "https://api.postproxy.dev/api/chats/chat_wa789/messages" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "interactive": { "type": "button", "body": { "text": "Your order is ready. Pick up or deliver?" }, "action": { "buttons": [ { "type": "reply", "reply": { "id": "PICKUP", "title": "Pick up" } }, { "type": "reply", "reply": { "id": "DELIVER", "title": "Deliver" } } ] } } }'A list:
{ "interactive": { "type": "list", "header": { "type": "text", "text": "Roasts" }, "body": { "text": "Which one would you like?" }, "action": { "button": "Choose", "sections": [ { "title": "Espresso", "rows": [ { "id": "ESP-1", "title": "House blend", "description": "Chocolate, nutty" } ] }, { "title": "Filter", "rows": [ { "id": "FLT-1", "title": "Ethiopia Guji", "description": "Floral, citrus" } ] } ] } }}The customer’s choice arrives as an inbound message. There is no tapped_action on WhatsApp — read the id you assigned from interactive:
{ "direction": "inbound", "body": "Deliver", "interactive": { "type": "button_reply", "button_reply": { "id": "DELIVER", "title": "Deliver" } }}cta_url, product, product_list, flow and Meta’s other types use the same envelope. Interactive messages are session messages: they need an open window and can’t go to a group.
A tap on a template quick-reply button arrives as a plain text message whose body is the button label.
Direct Send (category: "utility")
Section titled “Direct Send (category: "utility")”Meta lets an eligible business send a plain utility text outside the 24-hour window without a template — order updates, appointment reminders, account notices. Pass category: "utility" with body and nothing else:
curl -X POST "https://api.postproxy.dev/api/chats/chat_wa789/messages" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "body": "Your order ORD-12345 was delivered. Thanks for shopping with Acme.", "category": "utility" }'The window check is skipped and Meta decides. An account that isn’t eligible gets Meta’s rejection back as message.failed. category alongside media, template or any other payload returns 422.
Mark a chat read
Section titled “Mark a chat read”POST /api/chats/:id/mark_read
Sends the blue-tick read receipt on WhatsApp, and stamps metadata.read_at on every network.
curl -X POST "https://api.postproxy.dev/api/chats/chat_wa789/mark_read" \ -H "Authorization: Bearer YOUR_API_KEY"start_typing works on WhatsApp too, and marks the newest inbound message read as Meta requires.
Group chats
Section titled “Group chats”A message in a WhatsApp group your number belongs to arrives as a chat whose participant_external_id is the group id and whose group flag is true. The individual sender is in platform_data.from.
Text, media, template, location and contacts sends go to a group. interactive does not. Administering groups — creating them, participants, invite links — is under Groups.
Coexistence: messages sent from the phone
Section titled “Coexistence: messages sent from the phone”On a coexistence number, messages the team sends from the WhatsApp Business app on the phone show up as outbound messages with source: "synced" and platform_data.source: "whatsapp_business_app", and fire message.sent. The history imported at connect does not fire webhooks.
What comes in
Section titled “What comes in”Inbound text, images, video, audio, documents and stickers (attachments[].type), location, contact cards, interactive replies, template button taps, catalog orders (platform_data.order), reactions, read receipts, deletions and ad referrals all arrive through the standard DM webhooks. Types Meta can’t deliver arrive with is_unsupported: true.
A message the customer deletes for everyone stamps external_deleted_at and fires message.deleted.
Failed sends
Section titled “Failed sends”A message Meta accepts and later fails to deliver — blocked number, invalid recipient, paused template, closed window — moves to status: "failed" with error_details and fires message.failed, possibly some minutes after message.sent.
Once Meta reports a send as delivered, the conversation category and pricing Meta attached are stored under platform_data.conversation and platform_data.pricing.
Message templates
Section titled “Message templates”WhatsApp only lets a business start a conversation, or continue one more than 24 hours after the customer’s last message, with a pre-approved message template. Templates belong to the WABA — the profile — not to a number, and every number on the account can send them. Meta reviews each one and can pause or disable it later based on customer feedback.
Postproxy keeps a mirror of the WABA’s templates, so sends are validated without a round trip to Meta and review outcomes arrive as webhooks instead of needing a poll.
Endpoints
Section titled “Endpoints”| Method | Endpoint | Description |
|---|---|---|
GET | /api/profiles/:profile_id/templates | List templates |
GET | /api/profiles/:profile_id/templates/:id | Get a template |
POST | /api/profiles/:profile_id/templates | Create a template |
PATCH | /api/profiles/:profile_id/templates/:id | Update a template |
DELETE | /api/profiles/:profile_id/templates/:id | Delete a template |
GET | /api/profiles/:profile_id/templates/library | Look up Meta’s template library |
A template :id accepts the Postproxy hashid, Meta’s template id, or the template name. A name is shared by every language variant, so pass ?language= alongside it. A name that exists in several languages without a language returns 409:
{ "error": "Template order_update exists in several languages; pass language", "code": "ambiguous_template", "languages": ["en_US", "pt_BR"]}Non-WhatsApp profiles return 422 on every template endpoint.
Template object
Section titled “Template object”{ "id": "tpl_8f2k1a", "external_id": "1234567890123456", "profile_id": "prof_abc123", "name": "order_update", "language": "en_US", "category": "UTILITY", "status": "APPROVED", "parameter_format": "POSITIONAL", "components": [ { "type": "HEADER", "format": "TEXT", "text": "Order {{1}}" }, { "type": "BODY", "text": "Hi {{1}}, your order {{2}} has been confirmed and ships on {{3}}.", "example": { "body_text": [["Ana", "ORD-12345", "Friday"]] } }, { "type": "FOOTER", "text": "Acme Coffee" }, { "type": "BUTTONS", "buttons": [ { "type": "URL", "text": "Track order", "url": "https://shop.example.com/orders/{{1}}", "example": ["ORD-12345"] }, { "type": "QUICK_REPLY", "text": "Stop notifications" } ] } ], "variable_count": 5, "created_at": "2026-09-14T10:00:00.000Z", "updated_at": "2026-09-14T10:02:31.000Z"}| Field | Type | Description |
|---|---|---|
id | string | Postproxy hashid |
external_id | string|null | Meta’s template id |
profile_id | string | The WABA profile |
name | string | Lowercase letters, digits and underscores, starting with a letter. Together with language it identifies the template on Meta |
language | string | Meta language code — e.g. en_US, pt_BR, de |
category | string | UTILITY, MARKETING or AUTHENTICATION. Meta can recategorize during review — see message_template.category_updated |
status | string | Meta review status: APPROVED, PENDING, REJECTED, PAUSED, DISABLED, IN_APPEAL, PENDING_DELETION. Only APPROVED templates can be sent |
parameter_format | string|null | POSITIONAL ({{1}}, {{2}}) or NAMED ({{customer_name}}) |
components | array | Meta’s component array — HEADER, BODY, FOOTER, BUTTONS — exactly as stored on Meta |
message_send_ttl_seconds | integer | Present when the template overrides Meta’s default time-to-live for undelivered sends |
rejected_reason | string | Present when Meta rejected the template |
quality_score | object | Present once Meta has scored the template — e.g. { "score": "GREEN" } |
variable_count | integer | How many variables a send expects: header text variables, then body variables, then dynamic URL buttons. See Sending a template |
created_at, updated_at | string | ISO 8601 |
List templates
Section titled “List templates”GET /api/profiles/:profile_id/templates
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | No | Exact template name |
language | string | No | Language code |
status | string | No | Review status, case-insensitive (approved, pending, …) |
refresh | boolean | No | true pulls the WABA’s templates from Meta before answering. Use it right after creating templates elsewhere, or when a status looks stale |
Returns every matching template under data, ordered by name then language. Not paginated.
curl -X GET "https://api.postproxy.dev/api/profiles/prof_abc123/templates?status=approved" \ -H "Authorization: Bearer YOUR_API_KEY"{ "data": [ { "id": "tpl_8f2k1a", "name": "order_update", "language": "en_US", "status": "APPROVED", "...": "..." } ]}A refresh=true that fails at Meta — rate limit, revoked token — returns 422 with Meta’s message rather than stale data.
Get template
Section titled “Get template”GET /api/profiles/:profile_id/templates/:id
curl -X GET "https://api.postproxy.dev/api/profiles/prof_abc123/templates/order_update?language=en_US" \ -H "Authorization: Bearer YOUR_API_KEY"Returns the Template object. 404 when nothing matches, 409 ambiguous_template when a name matches several languages and no language was given.
Create template
Section titled “Create template”POST /api/profiles/:profile_id/templates
Two ways to create one: a custom template you design with components, or a copy of one of Meta’s pre-approved library templates, which is approved immediately.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Lowercase letters, digits and underscores, starting with a letter. Unique per language on the WABA |
category | string | Yes | UTILITY, MARKETING or AUTHENTICATION (case-insensitive) |
language | string | Yes | Meta language code |
components | array | Yes for custom | Meta’s component array. Variables are {{1}}-style for POSITIONAL or {{name}}-style for NAMED; a component with variables needs an example for review |
parameter_format | string | No | POSITIONAL (default) or NAMED |
message_send_ttl_seconds | integer | No | Time-to-live for undelivered sends. Ranges: AUTHENTICATION 30–900, UTILITY 30–43200, MARKETING 43200–2592000. -1 on AUTHENTICATION or UTILITY keeps Meta’s default explicitly |
library_template_name | string | Yes for library | Name of the Meta library template to copy — see Template library. components isn’t needed then |
library_template_body_inputs | object | No | Body inputs for the library template, in Meta’s shape |
library_template_button_inputs | array | No | Button inputs for the library template, in Meta’s shape |
curl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/templates" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "order_update", "category": "UTILITY", "language": "en_US", "components": [ { "type": "BODY", "text": "Hi {{1}}, your order {{2}} has been confirmed.", "example": { "body_text": [["Ana", "ORD-12345"]] } }, { "type": "FOOTER", "text": "Acme Coffee" } ] }'Copying a library template:
curl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/templates" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "account_created", "category": "UTILITY", "language": "en_US", "library_template_name": "account_creation_confirmation_2", "library_template_button_inputs": [ { "type": "URL", "url": { "base_url": "https://acme.example/login" } } ] }'Returns 201 with the Template object. A custom template starts as PENDING and moves to APPROVED or REJECTED when Meta finishes review — subscribe to message_template.status_updated or poll Get template. A library copy comes back APPROVED.
Errors: 400 for a missing or malformed field (name format, unknown category, components missing on a custom template, TTL outside the category’s range); 422 when Meta rejects the template (duplicate name in that language, invalid components, unsupported language); 403 when the token lacks template permissions.
Update template
Section titled “Update template”PATCH /api/profiles/:profile_id/templates/:id
| Parameter | Type | Required | Description |
|---|---|---|---|
components | array | One of | Replaces the whole component array. Editing components sends the template back to review — status returns to PENDING and it can’t be sent until approved again |
message_send_ttl_seconds | integer | One of | New TTL, within the category’s range. -1 is not accepted on update |
curl -X PATCH "https://api.postproxy.dev/api/profiles/prof_abc123/templates/order_update?language=en_US" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message_send_ttl_seconds": 3600 }'Returns the updated template. 400 when neither field is sent or the TTL is out of range; 422 when Meta rejects the edit. Meta allows only a limited number of component edits per template per day, and those rejections pass through as 422.
Delete template
Section titled “Delete template”DELETE /api/profiles/:profile_id/templates/:id
Deleting by name with no language removes every language variant. Deleting by hashid, Meta id, or name plus ?language= removes one variant.
# One languagecurl -X DELETE "https://api.postproxy.dev/api/profiles/prof_abc123/templates/order_update?language=pt_BR" \ -H "Authorization: Bearer YOUR_API_KEY"{ "deleted": true, "scope": "language", "name": "order_update", "language": "pt_BR" }# Every languagecurl -X DELETE "https://api.postproxy.dev/api/profiles/prof_abc123/templates/order_update" \ -H "Authorization: Bearer YOUR_API_KEY"{ "deleted": true, "scope": "all_languages", "name": "order_update" }Meta keeps a deleted template’s name reserved for 30 days and keeps delivering already-sent messages for the same period; the template reads as PENDING_DELETION on Meta meanwhile. Messages already sent keep their template reference with the name and language.
Template library
Section titled “Template library”GET /api/profiles/:profile_id/templates/library
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Library template name |
language | string | No | Language code |
Looks a template up in Meta’s pre-approved library and returns Meta’s entry under template (null when the name is unknown) — its body, the inputs it expects and its buttons, everything you need to fill library_template_body_inputs and library_template_button_inputs on Create template.
curl -X GET "https://api.postproxy.dev/api/profiles/prof_abc123/templates/library?name=account_creation_confirmation_2&language=en_US" \ -H "Authorization: Bearer YOUR_API_KEY"Sending a template
Section titled “Sending a template”Templates go out through the regular Send message endpoint with a template object instead of body. A template send works inside or outside the 24-hour window and re-opens it. Only APPROVED templates can be sent.
curl -X POST "https://api.postproxy.dev/api/chats/chat_wa789/messages" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "template": { "name": "order_update", "language": "en_US", "variables": ["Ana", "ORD-12345", "Friday"] } }'template. field | Type | Description |
|---|---|---|
id | string | Template hashid or Meta id. Either id or name is required |
name | string | Template name. Add language when the name exists in several languages, otherwise 409 |
language | string | Language code |
variables | array | Values as strings, in this order: header text variables, then body variables, then one value per dynamic URL button. For POSITIONAL templates the count must equal variable_count (422 otherwise); for NAMED templates values are matched to the placeholders in order of appearance |
button_params | array | Overrides for buttons needing runtime parameters — each { "index": 0, "sub_type": "url", "parameters": [...] } in Meta’s shape. sub_type is url, copy_code, flow, quick_reply or voice_call. A dynamic URL button given here no longer consumes a value from variables |
header_media | object | { "link": "https://..." } or { "id": "<meta media id>" } for templates with an IMAGE, VIDEO or DOCUMENT header. Omitted, the sample media the template was submitted with is used |
header_location | object | { "latitude", "longitude", "name", "address" } — required for templates with a LOCATION header |
The stored message’s body is the template body rendered with the variables, and the message carries a template object so you can see which template went out:
{ "id": "msg_222", "direction": "outbound", "status": "pending", "body": "Hi Ana, your order ORD-12345 has been confirmed and ships on Friday.", "template": { "id": "tpl_8f2k1a", "external_id": "1234567890123456", "name": "order_update", "language": "en_US", "variables": ["Ana", "ORD-12345", "Friday"] }}Errors: 404 template not found; 409 the name exists in several languages (pass template.language); 422 the template isn’t APPROVED or the wrong number of variables was given — the message names how many the template expects.
Meta’s per-template pacing, marketing opt-out handling and TTL apply to the send exactly as they do outside Postproxy.
Template sync
Section titled “Template sync”The mirror is refreshed from Meta after connect, on GET .../templates?refresh=true, and after every create, update and delete. Meta’s template webhooks keep individual rows current in between and fire message_template.status_updated and message_template.category_updated, so a review outcome is usually available without polling. A template created directly in Business Manager first appears through that webhook. A template deleted on Meta drops out of the mirror on the next full sync.
Number management
Section titled “Number management”Every endpoint in this section takes phone_number_id — the placement id.
Number info
Section titled “Number info”GET /api/profiles/:profile_id/whatsapp/number_info
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
Reads the number’s live state from Meta together with the WABA’s, and refreshes the placement’s metadata from what comes back.
curl -X GET "https://api.postproxy.dev/api/profiles/prof_abc123/whatsapp/number_info?phone_number_id=106540352242922" \ -H "Authorization: Bearer YOUR_API_KEY"{ "phone": { "display_phone_number": "+1 555-010-0001", "verified_name": "Acme Coffee", "name_status": "APPROVED", "new_name_status": null, "quality_rating": "GREEN", "messaging_limit_tier": "TIER_1K", "throughput": { "level": "STANDARD" }, "status": "CONNECTED", "platform_type": "CLOUD_API", "is_official_business_account": false }, "waba": { "name": "Acme Coffee", "account_review_status": "APPROVED", "business_verification_status": "verified", "timezone_id": "1", "ownership_type": "CLIENT_OWNED" }, "placement_id": "106540352242922"}waba is null when the account-level read fails; the phone part still answers. A number Meta no longer shares with Postproxy returns 422 asking for a reconnect.
Register phone number
Section titled “Register phone number”POST /api/profiles/:profile_id/whatsapp/register_phone_number
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
pin | string | No | The number’s 6-digit two-step verification PIN. Defaults to the PIN used at connect |
Registers the number for Cloud API messaging. Needed when the registration after connect left a registration_warning on the placement — typically because the number already has its own PIN. Pass that PIN here. Registering an already-registered number is a no-op.
{ "registered": true, "already_registered": false, "placement_id": "106540352242922" }400 when pin isn’t 6 digits; 422 with Meta’s message when the PIN doesn’t match the one set on the number.
Request verification code
Section titled “Request verification code”POST /api/profiles/:profile_id/whatsapp/request_verification_code
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
method | string | No | SMS (default) or VOICE |
language | string | No | Locale for the message, default en_US |
Asks Meta to send an ownership verification code to the number. Only needed for a number that Embedded Signup left unverified (status: "PENDING" in its metadata).
{ "requested": true, "method": "SMS", "placement_id": "106540352242922" }Verify phone number
Section titled “Verify phone number”POST /api/profiles/:profile_id/whatsapp/verify_phone_number
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
code | string | Yes | The code received |
{ "verified": true, "placement_id": "106540352242922" }Business profile
Section titled “Business profile”What a customer sees when they open the number’s details in WhatsApp: about text, address, description, email, websites, industry and the profile photo. One per number.
Get business profile
Section titled “Get business profile”GET /api/profiles/:profile_id/whatsapp/business_profile
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
{ "business_profile": { "about": "Neighbourhood coffee since 2009.", "address": "1 Main St, Springfield", "description": "Roasted in-house. Open daily 7–19.", "websites": ["https://acme.example"], "vertical": "RESTAURANT", "profile_picture_url": "https://pps.whatsapp.net/..." }, "placement_id": "106540352242922"}Meta omits fields that were never set, so keys can be absent.
Update business profile
Section titled “Update business profile”PATCH /api/profiles/:profile_id/whatsapp/update_business_profile
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
about | string | No | Up to 139 characters |
address | string | No | Street address |
description | string | No | Up to 512 characters |
email | string | No | Contact email |
websites | array | No | Up to 2 URLs |
vertical | string | No | Meta industry value — e.g. RETAIL, RESTAURANT, PROF_SERVICES, OTHER |
Send only what changes. At least one field is required, otherwise 400.
curl -X PATCH "https://api.postproxy.dev/api/profiles/prof_abc123/whatsapp/update_business_profile" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number_id": "106540352242922", "about": "Now with a roastery.", "websites": ["https://acme.example", "https://shop.acme.example"] }'{ "updated": true, "placement_id": "106540352242922" }Update business profile photo
Section titled “Update business profile photo”POST /api/profiles/:profile_id/whatsapp/update_business_profile_photo
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
url | string | One of | Public https:// URL of the image. Redirects are followed |
data | string | One of | Base64-encoded image, with or without a data:image/...;base64, prefix |
content_type | string | No | MIME type for data when it carries no data-URI prefix; otherwise detected |
JPEG or PNG, at most 5 MB.
{ "updated": true, "placement_id": "106540352242922" }Display name
Section titled “Display name”The verified business name shown next to the number. Meta reviews every change.
Get display name
Section titled “Get display name”GET /api/profiles/:profile_id/whatsapp/display_name
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
{ "display_name": { "name": "Acme Coffee", "status": "APPROVED", "new_name_status": "PENDING_REVIEW", "phone_number": "+1 555-010-0001" }}status is the review state of the current name. new_name_status is the state of a pending change request, null when there is none.
Request display name change
Section titled “Request display name change”POST /api/profiles/:profile_id/whatsapp/request_display_name_change
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
display_name | string | Yes | The new name, following Meta’s display name guidelines |
{ "display_name": { "name": "Acme Coffee Roasters", "status": "PENDING_REVIEW" }, "placement_id": "106540352242922"}The outcome arrives as a placement.name_status_updated webhook with Meta’s decision; the placement’s name and metadata.name_status are updated at the same time.
Username
Section titled “Username”A handle customers can reach the number at instead of the phone number. One per number; Meta may reserve it before it becomes active.
Get username
Section titled “Get username”GET /api/profiles/:profile_id/whatsapp/username
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
{ "username": "acmecoffee", "status": "approved", "placement_id": "106540352242922" }username is null and status is none when the number has no username. Other status values follow Meta’s, lowercased — e.g. reserved, pending.
Set username
Section titled “Set username”PUT /api/profiles/:profile_id/whatsapp/set_username
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
username | string | Yes | Letters, digits, periods and underscores; must contain a letter; no leading, trailing or consecutive periods |
transfer_action | string | No | none (default), or force_transfer to move a username the same business already holds on another number |
Returns the same shape as Get username. 400 for a malformed username; 422 when Meta refuses it — taken, reserved, or not eligible.
Delete username
Section titled “Delete username”DELETE /api/profiles/:profile_id/whatsapp/delete_username
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
{ "deleted": true, "placement_id": "106540352242922" }Username suggestions
Section titled “Username suggestions”GET /api/profiles/:profile_id/whatsapp/username_suggestions
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
{ "suggestions": ["acmecoffee", "acme.coffee", "acmecoffee_1"] }Blocked users
Section titled “Blocked users”Blocking a number stops its messages from reaching this phone number. Blocks are per number, not per WABA.
List blocked users
Section titled “List blocked users”GET /api/profiles/:profile_id/whatsapp/blocked_users
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
limit | integer | No | Page size |
after | string | No | Cursor from a previous response’s next_cursor |
{ "blocked_users": [ { "wa_id": "13105550007" } ], "next_cursor": null }Blocked user status
Section titled “Blocked user status”GET /api/profiles/:profile_id/whatsapp/blocked_user_status
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
user | string | Yes | Phone number in any format; non-digits are stripped |
{ "blocked": true, "wa_id": "13105550007" }Block users
Section titled “Block users”POST /api/profiles/:profile_id/whatsapp/block_users
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
users | array | Yes | Up to 1000 phone numbers or wa_ids |
curl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/whatsapp/block_users" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number_id": "106540352242922", "users": ["+1 310 555 0007", "13105550008"] }'{ "succeeded": [ { "input": "+1 310 555 0007", "wa_id": "13105550007" }, { "input": "13105550008", "wa_id": "13105550008" } ], "failed": [], "action": "blocked"}Numbers Meta could not block are listed in failed with Meta’s reasons; the call itself still returns 200. Meta restricts blocking to numbers that have messaged this phone number, so an unknown number lands in failed.
Unblock users
Section titled “Unblock users”POST /api/profiles/:profile_id/whatsapp/unblock_users
Same parameters and response shape as Block users, with "action": "unblocked" and the removed numbers under succeeded.
Groups
Section titled “Groups”WhatsApp groups created and administered by a business number through the API.
Messages in a group are ordinary chats whose participant_external_id is the group id and whose group flag is true.
List groups
Section titled “List groups”GET /api/profiles/:profile_id/whatsapp/groups
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
limit | integer | No | Page size |
after | string | No | Cursor from a previous response’s next_cursor |
{ "groups": [ { "id": "120363012345678901", "subject": "VIP customers", "created_at": 1758000000 } ], "next_cursor": null}Create group
Section titled “Create group”POST /api/profiles/:profile_id/whatsapp/create_group
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
subject | string | Yes | Group name, up to 128 characters |
description | string | No | Up to 2048 characters |
join_approval_mode | string | No | approval_required or auto_approve |
{ "group": { "id": "120363012345678901", "invite_link": "https://chat.whatsapp.com/AbCdEf...", "subject": "VIP customers" }}Get group
Section titled “Get group”GET /api/profiles/:profile_id/whatsapp/group
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
group_id | string | Yes | Group id |
{ "group": { "id": "120363012345678901", "subject": "VIP customers", "description": "Early access to new roasts", "join_approval_mode": "approval_required", "invite_link": "https://chat.whatsapp.com/AbCdEf...", "participants": [ { "user": "13105550007" } ], "participant_count": 1, "created_at": 1758000000, "is_suspended": false }}Update group
Section titled “Update group”PATCH /api/profiles/:profile_id/whatsapp/update_group
Takes phone_number_id, group_id, and at least one of subject, description or join_approval_mode — same rules as Create group.
{ "updated": true, "group_id": "120363012345678901" }Delete group
Section titled “Delete group”DELETE /api/profiles/:profile_id/whatsapp/delete_group
Takes phone_number_id and group_id.
{ "deleted": true, "group_id": "120363012345678901" }Add and remove participants
Section titled “Add and remove participants”POST /api/profiles/:profile_id/whatsapp/add_group_participants
POST /api/profiles/:profile_id/whatsapp/remove_group_participants
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
group_id | string | Yes | Group id |
phone_numbers | array | Yes | Up to 8 phone numbers per call — a group holds 8 participants |
{ "added": ["13105550007", "13105550008"], "group_id": "120363012345678901" }Remove answers with removed instead of added.
Create invite link
Section titled “Create invite link”POST /api/profiles/:profile_id/whatsapp/create_group_invite_link
Takes phone_number_id and group_id. Generates a fresh invite link, invalidating the previous one.
{ "invite_link": "https://chat.whatsapp.com/GhIjKl...", "group_id": "120363012345678901" }Join requests
Section titled “Join requests”GET /api/profiles/:profile_id/whatsapp/group_join_requests
POST /api/profiles/:profile_id/whatsapp/approve_group_join_requests
POST /api/profiles/:profile_id/whatsapp/reject_group_join_requests
For groups in approval_required mode. The GET takes phone_number_id and group_id; the two POSTs also take phone_numbers, the requesters to approve or reject.
{ "join_requests": [ { "user": "13105550009", "timestamp": 1758003600 } ], "group_id": "120363012345678901"}{ "approved": ["13105550009"], "group_id": "120363012345678901" }Click-to-WhatsApp conversions
Section titled “Click-to-WhatsApp conversions”When a customer reaches the number from a Click-to-WhatsApp ad, Meta tags the first message with a click id (ctwa_clid). Postproxy captures it on the chat as metadata.ctwa_clid and metadata.ctwa_captured_at, stores the ad’s details in metadata.ctwa (source_id, source_url, headline, body, media URLs), and fires a referral.received webhook.
Meta’s own detected conversions — a purchase or lead it spotted in the conversation — arrive as whatsapp.automatic_event and backfill ctwa_clid on the chat when it wasn’t captured yet.
To report your own outcomes back to Meta for ad optimization, the WABA needs a Conversions API dataset; each event is then sent against the chat that came from the ad. Both dataset endpoints are WABA-level and take no phone_number_id.
Get dataset
Section titled “Get dataset”GET /api/profiles/:profile_id/whatsapp/dataset
{ "dataset_id": "1234567890123456" }dataset_id is null when the WABA has no dataset yet. Read live from Meta; nothing is stored.
Create dataset
Section titled “Create dataset”POST /api/profiles/:profile_id/whatsapp/create_dataset
Idempotent — returns the existing dataset with created: false when there is one.
{ "dataset_id": "1234567890123456", "created": true }Send conversion event
Section titled “Send conversion event”POST /api/profiles/:profile_id/whatsapp/send_conversion_event
| Parameter | Type | Required | Description |
|---|---|---|---|
event_name | string | Yes | LeadSubmitted, Purchase, AddToCart, InitiateCheckout or ViewContent |
event_id | string | Yes | Your unique id for the event — Meta dedupes on it |
chat_id | string | One of | The chat the customer arrived in |
phone | string | One of | The customer’s phone number instead; the most recently active chat with it is used |
event_time | integer|string | No | When it happened — Unix seconds or ISO 8601. Defaults to now |
value | number | No | Monetary value |
currency | string | No | ISO 4217 code — e.g. USD |
content_ids | array | No | Product or content ids |
email | string | No | Customer email. SHA-256 hashed before it leaves Postproxy |
external_id | string | No | Your customer id. SHA-256 hashed before it leaves Postproxy |
test_code | string | No | Meta test event code, to see the event in Events Manager’s test tab without affecting optimization |
dataset_id | string | No | Send to a specific dataset instead of the WABA’s own |
curl -X POST "https://api.postproxy.dev/api/profiles/prof_abc123/whatsapp/send_conversion_event" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chat_id": "chat_wa789", "event_name": "Purchase", "event_id": "order-98213", "value": 42.5, "currency": "USD", "content_ids": ["SKU-ESPRESSO-1KG"] }'{ "dataset_id": "1234567890123456", "chat_id": "chat_wa789", "events_received": 1, "events_failed": 0, "trace_id": "AbCdEf..."}Errors: 400 for an unknown event_name, a missing event_id, or neither chat_id nor phone; 404 when the chat can’t be found; 422 when the chat has no captured ctwa_clid — the conversation didn’t start from a Click-to-WhatsApp ad — or the WABA has no dataset yet.
GET /api/profiles/:profile_id/whatsapp/media
| Parameter | Type | Required | Description |
|---|---|---|---|
phone_number_id | string | Yes | Placement id |
media_id | string | Yes | Meta media id |
Streams a Meta media object’s bytes with its content type, for the rare media id that isn’t already an attachment on a message. Inbound attachments are mirrored to Postproxy storage and served from attachments[].url, which is the normal way to read them.
Webhooks
Section titled “Webhooks”Beyond the standard message events and profile events, WhatsApp profiles emit a WhatsApp event group. Subscribe individually or with * in the Webhooks API.
| Event | Fires when |
|---|---|
message.received / .sent | Inbound / outbound message |
message.delivered / .read | Meta confirmed delivery / the customer read it |
message.deleted | The customer deleted a message for everyone |
message.failed | An outbound message failed permanently — Meta’s error in error_details |
reaction.received | The customer reacted to a message |
referral.received | A Click-to-WhatsApp ad or wa.me link referral opened a conversation |
message_template.status_updated | Meta approved, rejected, paused, disabled or re-enabled a template, or changed its quality score |
message_template.category_updated | Meta recategorized a template — e.g. UTILITY → MARKETING |
placement.name_status_updated | Meta decided on a display name change request |
placement.quality_updated | A number’s quality rating or messaging limit tier changed |
whatsapp.automatic_event | Meta detected a conversion in a Click-to-WhatsApp conversation |
profile.connected / .disconnected | Connection state changed |
profile.placements_synced | The number list was refreshed |
curl -X POST "https://api.postproxy.dev/api/webhooks" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/postproxy", "events": ["message.received", "message_template.status_updated", "placement.quality_updated"] }'message_template.status_updated
Section titled “message_template.status_updated”{ "object": "event", "type": "message_template.status_updated", "created_at": "2026-09-14T10:02:31.000Z", "data": { "object": { "id": "tpl_8f2k1a", "external_id": "1234567890123456", "profile_id": "prof_abc123", "platform": "whatsapp", "name": "order_update", "language": "en_US", "category": "UTILITY", "status": "APPROVED", "rejected_reason": null, "quality_score": null } }}status is the new review status. A rejection adds reason with Meta’s rejection reason. A quality change fires the same event with the new quality_score.
message_template.category_updated
Section titled “message_template.category_updated”The same object plus previous_category, new_category, and — when Meta only flagged a recommended category — correct_category.
placement.name_status_updated
Section titled “placement.name_status_updated”{ "object": "event", "type": "placement.name_status_updated", "created_at": "2026-09-15T08:12:00.000Z", "data": { "object": { "id": "106540352242922", "name": "Acme Coffee Roasters", "platform": "whatsapp", "profile_group_id": "grp_xyz789", "profile_id": "prof_abc123", "profile_name": "Acme Coffee", "metadata": { "display_phone_number": "+1 555-010-0001", "name_status": "APPROVED", "quality_rating": "GREEN" }, "decision": "APPROVED", "requested_name": "Acme Coffee Roasters", "rejection_reason": null } }}placement.quality_updated
Section titled “placement.quality_updated”The same placement object plus event (Meta’s event name — ONBOARDING, UPGRADE, DOWNGRADE, FLAGGED, UNFLAGGED), current_limit and old_limit. metadata.quality_rating and metadata.messaging_limit_tier already carry the new values when the event arrives.
whatsapp.automatic_event
Section titled “whatsapp.automatic_event”{ "object": "event", "type": "whatsapp.automatic_event", "created_at": "2026-09-15T09:30:00.000Z", "data": { "object": { "profile_id": "prof_abc123", "chat_id": "chat_wa789", "participant_external_id": "13105550007", "external_message_id": "wamid.HBgL...", "event_name": "Purchase", "ctwa_clid": "ARAkLkA8rmlFeiCktEJQ-QTwRiyYHAFDLMNDBH0CD3qpjd0HR4irJ6LEkR7JwFF4XvnO2E4Nx0", "custom_data": { "currency": "USD", "value": 42.5 }, "detected_at": "2026-09-15T09:30:00.000Z" } }}chat_id is null when the customer has no chat with the profile yet.
Sandbox
Section titled “Sandbox”Connect whatsapp in a sandbox profile group and you get a sandbox WhatsApp profile: one phone number placement with a fabricated +1 555 … number, two seeded approved templates — hello_world (en_US, no variables) and order_update (en_US, Hi {{1}}, your order {{2}} has been confirmed.) — and every endpoint on this page answering production-shaped responses.
Sends return a wamid. message id, create_template returns PENDING (or APPROVED for a library copy), the block list and groups start empty, and create_dataset returns a stable dataset id. Inbound messages, statuses, reactions and referrals are fabricated from the Sandbox page in the dashboard and go through the same ingestion as Meta’s webhooks. See Direct Messages → Sandbox.
Errors
Section titled “Errors”| Status | Meaning |
|---|---|
400 | A missing required parameter (phone_number_id, group_id, …) or a value outside its documented limit — lengths, counts, PIN or username format, TTL range, unknown enum value |
403 | Meta refused for permission reasons — most often a number whose two-step PIN isn’t registered (re-register it with its PIN), or a token lacking a scope |
404 | Profile not found, phone_number_id isn’t a placement the key can reach, or template or chat not found |
409 | A template name matches several languages and no language was given — code: "ambiguous_template", with languages listing them |
422 | The profile isn’t a WhatsApp profile, the number is no longer shared with Postproxy (reconnect), the connection needs re-authentication, or Meta rejected the payload — Meta’s message is passed through |
429 | Meta is rate-limiting the WABA. Retry after the Retry-After header |
502 | Meta’s API could not be reached. Retry |
What you can’t do
Section titled “What you can’t do”WhatsApp is a messaging channel, not a publishing one, so some things available elsewhere in Postproxy don’t apply:
- Publish posts.
POST /api/postswith a WhatsApp profile returns422, and WhatsApp doesn’t appear in the composer. - Comments, post insights and profile stats. WhatsApp has no public feed.
- Edit an outbound message.
PATCH /api/messages/:idreturns422. - Free-form messages outside the 24-hour window. Send a template or a
utilityDirect Send. - Backfill conversations on demand. History only arrives through coexistence onboarding at connect.
- Groups on a coexistence number, and interactive messages in any group.
- One WABA is one profile; each of its phone numbers is a placement, and the placement
idis Meta’sphone_number_id. placement_idis required when creating a chat — a customer messaging two of your numbers has two chats.- Check
within_messaging_windowbefore a free-form send; fall back to a template. variableson a template send are positional across header, body, then dynamic URL buttons —variable_counton the template tells you how many to supply.- Meta’s messaging limit tier caps business-initiated conversations per 24 hours; watch
placement.quality_updatedto see it move.