WhatsApp Business Messaging in the Same API as Your Social DMs
WhatsApp Business is usually a separate integration with its own auth, templates, and messaging window. Here's what it takes to run it through the same chats and messages endpoints as Messenger, Instagram, and Telegram.
WhatsApp Business runs on Meta's Cloud API, which uses different auth, a 24-hour messaging window, and pre-approved templates for business-initiated messages. Postproxy exposes it through the same /api/profiles/:id/chats and /api/chats/:id/messages endpoints as Messenger, Instagram, Telegram, and Bluesky, so one connected WhatsApp Business Account becomes a profile and each of its phone numbers a placement.
Why is WhatsApp usually a separate integration?
Most teams that support customers on social already have something working. Inbound webhooks land in a queue, a worker normalizes them into a conversation model, agents or an automation reply through one send function. Adding a network to that is usually a day of work.
WhatsApp is the one that breaks the pattern, for reasons that are all Meta’s rather than anybody’s implementation:
- Different onboarding. There’s no ordinary OAuth “connect your account” button. WhatsApp uses Embedded Signup, which grants access to a WhatsApp Business Account (WABA) rather than a single profile, and the numbers on that account have to be registered for Cloud API messaging afterwards.
- A hard messaging window. You can reply freely for 24 hours after the customer’s last message. After that, free-form text is rejected.
- Templates as a gate. Opening a conversation, or continuing one past the window, requires a template Meta has reviewed and approved. Templates can be paused, disabled, or recategorized later based on customer feedback.
- Numbers, not accounts, are the unit. One WABA can hold several phone numbers, each with its own quality rating and its own cap on how many conversations it can start per day.
None of that is hard to understand. It’s just enough structural difference that WhatsApp tends to end up as a second messaging stack with its own auth, its own data model, and its own on-call surprises.
What does it look like as one API?
The approach we took is to keep WhatsApp inside the existing Direct Messages API rather than beside it. A connected WABA is a profile. Each of its phone numbers is a placement of that profile. Conversations are chats, and chats hold messages — the same objects, with the same field names, as every other network.
So the reply that answers an Instagram DM:
curl -X POST "https://api.postproxy.dev/api/chats/chat_ig123/messages" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "body": "Your order ships tomorrow." }'is the same call that answers a WhatsApp one:
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 ships tomorrow." }'What’s genuinely WhatsApp-specific shows up as additional fields on those same objects, not as a parallel API. A WhatsApp chat carries external_placement_id for the number it runs on, group when it’s a group conversation, and within_messaging_window. A WhatsApp message can carry template, interactive, or location.
That matters mostly for code you’ve already written. A unified inbox, a routing rule, a reporting query — anything that reads chats and messages keeps working, and the WhatsApp-only branches are the few places where the platform genuinely differs.
How do you handle the 24-hour window?
This is the part worth designing for, because getting it wrong produces failed sends rather than a clear error at call time.
Rather than making you track the window yourself, every WhatsApp chat reports it:
{ "id": "chat_wa789", "platform": "whatsapp", "external_placement_id": "106540352242922", "participant_external_id": "13105550007", "within_messaging_window": false, "last_inbound_at": "2026-09-26T08:12:00.000Z"}When within_messaging_window is false, there are two ways through.
Send an approved template. This is also the only way to start a conversation with someone who hasn’t messaged you.
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"] } }'The variables are positional — header variables first, then body, then one value per dynamic URL button. The template’s variable_count tells you how many to supply, and a mismatch is rejected at call time rather than failing later at Meta.
Or send a utility notification. For order updates, appointment reminders, and account notices, Meta’s Direct Send lets an eligible account send a plain text outside the window without a template:
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.", "category": "utility" }'Worth knowing: WhatsApp has no HUMAN_AGENT tag. If you’ve built against Messenger, that’s the habit to unlearn — the tag that buys you seven days on Facebook and Instagram doesn’t exist here, and a tagged send is rejected.
Do you have to manage templates in Business Manager?
No, and this is where most of the operational annoyance lives if you do.
Templates are managed through /api/profiles/:id/templates. You can design one from components:
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"]] } } ] }'Or copy one of Meta’s pre-approved library templates, which comes back APPROVED immediately instead of waiting on review — useful for the standard shapes like order confirmations and account notices.
The part that saves real work is the review outcome. A template’s status moves without you doing anything: Meta approves it, and later may pause it, disable it, or recategorize UTILITY to MARKETING, which changes what it costs and when you’re allowed to send it. Those arrive as message_template.status_updated and message_template.category_updated webhooks, so nothing in your system has to poll Business Manager to find out whether a template is still safe to use.
Can you connect a number that’s already in use?
This is the question that decides whether WhatsApp is a migration project or an afternoon.
Most businesses starting out already answer customers from the WhatsApp Business app on a phone. The default Cloud API onboarding claims that number for the API, which means it stops working in the app — a real cutover, with a support line going dark in the middle of it.
Coexistence onboarding avoids that. Pass onboarding: "business_app" when starting the 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": "business_app" }'The user scans a QR code from WhatsApp Business, and the phone keeps working. Replies the team types there arrive in Postproxy as outbound messages with source: "synced", so your records stay complete even for conversations you didn’t handle through the API. Up to 6 months of history is imported.
The trade-offs are Meta’s: lower sending throughput than an API-only number, no access to the groups API, and Meta disconnects a coexistence number whose phone stays offline for around 14 days. For a team that wants to build against the API without moving their live support line first, it’s usually the right trade.
If you don’t know which case a given user is in, omit onboarding and the connect page asks them.
What about buttons and menus?
Text is the floor. WhatsApp’s interactive messages are what make menu-driven flows workable, and Postproxy passes Meta’s object through unchanged:
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" } } ] } } }'The customer’s tap comes back as an inbound message carrying the id you assigned:
{ "direction": "inbound", "body": "Deliver", "interactive": { "type": "button_reply", "button_reply": { "id": "DELIVER", "title": "Deliver" } }}Reading your own id rather than matching on the button’s display text is what keeps a flow from breaking the first time someone translates a label. Lists, call-to-action URLs, catalog products, and Flows use the same envelope.
One constraint to plan around: interactive messages are session messages. They need an open window, and they can’t be sent to a group.
What ships today
- Conversations — text, media (images, video, audio, documents, stickers), locations, contact cards, reactions with any emoji, quoted replies, typing indicators, and read receipts via
mark_read. - Templates — create from components or copy Meta’s library, update, delete per language or across all of them, with review outcomes as webhooks.
- Interactive messages — reply buttons, lists, CTA URLs, catalog products, Flows.
- Group chats, inbound and outbound.
- Number administration — business profile, display name change requests,
wa.meusername, blocked users, groups, and live quality and messaging-tier status. - Click-to-WhatsApp — ad referrals captured on the chat, plus the Conversions API to report purchases and leads back to Meta.
- A sandbox WhatsApp profile with a fake number and two seeded approved templates, so you can build the whole flow before Meta review.
WhatsApp is messaging-only: it has no feed, so POST /api/posts rejects a WhatsApp profile. Publishing stays on the eleven other networks.
The full parameter-by-parameter reference is on the WhatsApp platform page, and if you want an agent working the inbox rather than your own code, see how to reply to WhatsApp Business messages with an AI agent.
Postproxy
One API for every social platform
Publish to Instagram, X, LinkedIn, TikTok, YouTube and more with a single request. Free plan, no credit card required.