Parity: Zaileys, Meta's Cloud API and meta-whatsapp-rs
Verified against main at b6fc893 (PR #20) on 2026-09-26; rows 17, 19 and 84–88 again when the bot framework (roadmap B1) landed, against its code; the service's cells of rows 5, 6, 9, 33, 84, 91, 112 and 113 again when its core was extracted (roadmap S1), against the core's code; rows 69, 91, 111 and 139 again with the ConversationStore port change (roadmap L5), against its code; rows 89 and 92 when paced broadcast (B2) landed, against its code; rows 119 and 139 again when the inbox began recording window events, thread ownership and identity links (roadmap L7), against its code; rows 66, 129–133 and 151 again with the phone number calls (roadmap L9), against their code, and row 156 added for what L9 left (L26); the service's cell of row 9 again with S2's security review, against its code; row 152 again with the parent BSUID accounts API (roadmap L10a), against its code; row 140 again with the Messaging Account a send bills (roadmap L16a), against its code and Meta's account model evolution pages of 2026-10-26; the service's cell of row 112 again when the port suites moved into the core (roadmap S3), against its code, and again when the service was composed from the bundle alone (roadmap S4), against its code and its tests; row 155 with app deep links on template URL buttons (roadmap L24), against its code and the service's test; rows 101 and 153 with template archiving and template groups (roadmap L23), against its code and Meta's pages of 2026-10-26 (§ Sources); row 156 with the number settings L9 left (roadmap L26), against its code and Meta's pages of 2026-10-27 (§ Sources); row 103 with the catalog and product reads (roadmap L17), against its code and Meta's Catalog API pages of 2026-10-27 (§ Sources); row 63 with the typed Flow JSON builder (roadmap L18), against its code and Meta's Flows pages of 2026-10-28; row 135 with call recording and transcription (roadmap L14), against its code and Meta's calling/call-recording and calling/call-transcription pages of 2026-10-31; rows 144 and 145 with Multi-Partner Solutions and migration intents (roadmap L12), against its code and Meta's solution-provider pages of 2026-11-01; rows 90, 127 and 146–148 with WABA administration (roadmap L10b), against its code and Meta's pages of 2026-11-01; row 139 again with the thread control API (roadmap L15), against its code and Meta's conversation-routing pages of 2026-11-01; rows 137, 138 and 154 with max price, reach estimates and welcome message sequences (roadmap L13), against its code and Meta's marketing and CTWA pages of 2026-11-01; rows 89 and 90 with durable scheduling (roadmap B3), against its code and Meta's templates/marketing-templates/per-user-limits page of 2026-11-01; row 48 with media conversion (roadmap L19), against its code and Meta's business-phone-numbers/media and messages/audio-messages pages of 2026-11-01, and again with stickers and image resizing (roadmap L19b), against its code and Meta's messages/sticker-messages and business-phone-numbers/media pages of 2026-11-01; row 91 with retention and auto-delete (roadmap B4), against its code; row 117 with llms.txt and the service's doctor (roadmap L22a), against their code and tests and Meta's webhook pages of 2026-11-01 (§ Sources); row 6 with the token-expiry signal (roadmap L11e), against its code and Meta's access-tokens and facebook-login/facebook-login-for-business pages of 2026-11-01; rows 69, 110 and 111 with the message and contact stores (roadmap L8), against its code and Meta's webhooks/reference/smb_app_state_sync page of 2026-11-01; row 139 again with the Routing Roles API (roadmap L15a), against its code and Meta's conversation-routing/routing-roles page of 2026-11-01; row 117 again with the docs MCP server (roadmap L22b), against its code and tests and the MCP specification of 2026-11-01 (§ Sources); row 122 with several WABAs in one signup (roadmap L11b), against its code and Meta's embedded-signup/implementation and solution-providers/manage-accounts pages of 2026-11-01; row 123 with pre-verified numbers (roadmap L11c), against its code and Meta's embedded-signup/pre-verified-numbers and pre-verified number reference pages of 2026-11-01. Every cell about us was checked against the code: a cell that says a thing is done names the symbol that does it, and what the service (meta-whatsapp-server) does is read from its routes, not from its design. The plan to close the gaps is roadmap.md; the Meta platform categories are in categories.md; the per-feature list is coverage.md.
Where we stand
42 of the 132 counted rows are done on both sides. Parity is not reached.
| 132 counted rows | done | partial | gap | n/a (that side does not carry it) |
|---|---|---|---|---|
| Library | 125 | 6 | 0 | 1 |
| Service | 42 | 22 | 67 | 1 |
- The table has 156 rows. 24 of them are not counted: they work only over WhatsApp Web ("n/a — unofficial protocol"). The counted rows' n/a column is a side that does not carry the capability at all: the library for row 115 (packaging), the service for row 63 (a Flow JSON builder).
- Each of the library's 6 partial or gap rows is cited by a roadmap item. Each of the service's 89 names, in its status, the roadmap items that bring it; by family (a row can name two): M2 8, M3 11, M4 2, M5 70, P 2 (payments).
- The largest gaps: the service routes for the modules the design once left "on demand" (M5), the inbox, live events and onboarding over HTTP (M2, M3), and the rest of the bot framework (section G: its commands, middleware, plugins, access lists and Markdown replies exist, B1, with subcommands and flags, B1b, paced broadcast, B2, rich replies, B1c, durable scheduling, B3, and retention and auto-delete, B4; over HTTP they wait for M5k).
What parity means
The owner's definition (2026-09-26):
- Cloud API only. Every capability of Meta's Cloud API is a row. A Zaileys capability that works only over WhatsApp Web is listed and marked "n/a — unofficial protocol"; meta-whatsapp-rs has no WhatsApp Web provider and will not get one.
- Zaileys' framework features are in scope, although Zaileys runs them only over WhatsApp Web: commands, middleware, plugins, broadcast with rate limiting, scheduled messages, auto-delete, message stores, rich markdown responses, multiple accounts and typed errors. We build them on Cloud API webhooks.
- Done means every row that is not n/a is done in the library, and in the service wherever the service is meant to carry it (roadmap.md § Done criteria).
- This is the comparison with Zaileys and Meta's Cloud API, not CONTRIBUTING.md's "docs and skills parity" (the docs and skills agreeing with the code).
How to read the table
- Status is
library / service, each one of done, partial, gap or n/a. A side is done when it carries every endpoint and field the capability's pages document for the Graph API version we target (v25.0), partial when it carries some, gap when it carries none, and n/a when that side does not carry the capability. A part another row covers counts in that row, and a part outside the Cloud API (Meta's ads APIs, say) is named and not counted. Something the service will carry counts as a gap (or partial) until it ships; the roadmap items that bring it are in parentheses. An n/a row says why instead. - The same grades hold in coverage.md, whose planned is a gap here and whose out of scope is an n/a, and in categories.md, which has no n/a (a category's n/a parts are named in its notes).
- Zaileys (Web) and Zaileys (Cloud): Yes, No, Partial, or
—when Zaileys does not mention the capability. The wording is ours. - Meta Cloud API: doc paths relative to
https://developers.facebook.com/documentation/business-messaging/whatsapp/(append.mdfor Markdown). (nm) marks one of the 5 pages Meta lists that the local mirror (.meta-docs/,just meta-docs) cannot fetch (flows/changelog,overview,pricing/prepaid-billing,webhooks/reference/messaging-handovers,webhooks/reference/standby: missing or gated at Meta); a claim resting on one rests on the reference pages or on our code's fixtures. Every other page cited is in the mirror. - Library and Service: code cited by module path and symbol, never by line.
client::ismeta_whatsapp_client::,webhooks::ismeta_whatsapp_webhooks::,core::ismeta_whatsapp_core::,adapters::ismeta_whatsapp_adapters::,typst::ismeta_whatsapp_typst::(the facademeta_whatsapp_rsre-exports each under the same name),inbox::ismeta_whatsapp_rs::inbox::,server::ismeta_whatsapp_server::, the service's binary (its process: configuration,serve, the command line),server_core::ismeta_whatsapp_server_core::, its framework-free core (the domain, the authorization order, event routing and polling, idempotency, the error model as data, and the ports its backends implement), and since roadmap S7 the crates the binary composes:server_http::ismeta_whatsapp_server_http::(listeners, authentication, the error renderer, Meta's webhook, media, operations),server_api_axum::ismeta_whatsapp_server_api_axum::(the/v1routes and their OpenAPI document),server_store_memory::andserver_store_postgres::the backends; an item the service re-exports from another of its crates is cited where it is declared (the binary re-exports the moved modules under their old paths). The bot framework's items (meta_whatsapp_bot::, re-exported asmeta_whatsapp_rs::bot) are cited by type (Command::alias) in a cell that namesmeta-whatsapp-bot. A type already qualified in a row is not qualified again in it. Routes are the service's/v1API; "the send union" is its message types (server_api_axum::api::messages::MessageType: text, the five media types, location, contacts, reaction, template, and interactivebutton,list,cta_url). - Row numbers are permanent: other documents cite them.
Sources
- Zaileys (zeative/zaileys, a Node library over two providers, Baileys for WhatsApp Web and Meta's Cloud API): its feature matrix and the feature pages in its navigation (getting started, core concepts, messaging, bots, chats, groups, data, and its Cloud pages), read on 2026-09-26 through summaries. Where its pages disagree (its providers page against the matrix, for locations and HTML apps; the plugins and middleware pages against the matrix and the Cloud overview), the matrix and the Cloud overview win here. Not read: the quickstart, installation, client reference, changelog and the recipes other than auto-reply. The Zaileys columns were not re-read for this revision.
- Meta:
.meta-docs/, crawled on 2026-09-24 and completed on 2026-09-26. The rows that rested on the pages the first crawl missed were checked again against those pages that day: their key facts (limits, fields, endpoints) and every claim that something is absent, not each sentence. Rows 101 and 153 were rewritten on 2026-10-26 againsttemplates/template-archivalandtemplates/template-groupsas Meta served them that day, not against the mirror: the first now shows the archive and unarchive endpoints, and the second, absent from Meta's page list at the crawl, is in it now. Row 156 was checked on 2026-10-27 againstreference/whatsapp-business-phone-number/settings-apiandreference/whatsapp-business-phone-number/whatsapp-business-account-phone-number-apias Meta served them that day: both still document its fields by schema only, without an example. - Meta's Catalog API, outside the WhatsApp docs and the mirror, for row 103: read on 2026-10-27 as Meta served it that day. From the Marketing API reference (
developers.facebook.com/documentation/ads-commerce/marketing-api/reference/, as Markdown):product-catalog,product-catalog/products,product-catalog/product_sets,product-setandproduct-set/products. From the Graph API reference (developers.facebook.com/docs/, as HTML; the new documentation serves no product item page):marketing-api/reference/product-item,graph-api/reference/whatsapp-business-account/product_catalogsandgraph-api/results(cursor pagination). - Meta's Flows pages, for row 63: read on 2026-10-28 as Meta served them that day, as Markdown, not against the mirror:
flows/guides/flowjson,flows/guides/components,flows/guides/media_upload,flows/guides/versioning,flows/changelogs,flows/reference/error-codes,flows/guides/flowsapiandflows/guides/flows-templates. The examples those pages show in an interactive preview are not in their Markdown and were not read. - Meta's webhook subscription pages, for row 117: read on 2026-11-01 as Meta served them that day, not against the mirror. As Markdown:
webhooks/overview(§ Fields, the field names),webhooks/create-webhook-endpoint(the objectwhatsapp_business_account, the app token) andsolution-providers/manage-webhooks(GET /{WABA_ID}/subscribed_appsand its example). As HTML, from the Graph API reference (developers.facebook.com/docs/, which serves no Markdown for them):graph-api/reference/app/subscriptions(object,callback_url,fieldsasstring[],active; reading needs the app token) andgraph-api/webhooks/subscriptions-edge(its example answer gives each field as{name, version}; the doctor reads both shapes). - The Model Context Protocol, for row 117's docs server, outside Meta's docs: the specification at
modelcontextprotocol.io, read on 2026-11-01 as Markdown. Revision 2026-07-28, the current one (specification/versioning):changelog,basic/index(_meta,resultType, the error codes),basic/versioning(no handshake, the dual-era rules),basic/transports/stdio,server/discover,server/tools(protocol errors againstisError) andserver/utilities/caching; and revision 2025-11-25'sbasic/lifecycle(theinitializehandshake). - Us: the code at b6fc893; for rows 17, 19 and 84–88, the bot framework's (B1).
The table
A. Connection, credentials, accounts
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 1 | Connection | QR-code login | Yes | No | none: the Cloud API authenticates with access tokens (access-tokens) | — | — | n/a — unofficial protocol |
| 2 | Connection | Pairing-code login | Yes | No | none | — | — | n/a — unofficial protocol |
| 3 | Connection | Saved session, logout, a budget of login attempts | Yes | No (the Cloud API is stateless HTTPS) | none | — | — | n/a — unofficial protocol |
| 4 | Connection | Token authentication; checking the token and the number at start | No | Yes (checks the token and the phone number id when it connects) | access-tokens, permissions.md | client::ClientBuilder::access_token; token inspection client::embedded_signup::EmbeddedSignup::debug_token; client::phone_numbers::PhoneNumber::get. No single "check my credentials" call | incomplete configuration refuses the start (server::config); attaching a WABA checks it with Meta: POST /v1/admin/tenants/{id}/wabas (server_core::admin::Admin::attach_waba) | done / done |
| 5 | Connection | Reconnect semantics on the Cloud API: back off when throttled, never replay a send that timed out | Yes (reconnects with growing delays) | Partial (waits after a rate limit) | throughput.md (error 130429), support/error-codes.md | client::RetryPolicy (jittered exponential backoff, Retry-After honoured, a send replayed only after a throttle or the account in maintenance, core::Error::may_resend); core::ErrorKind::is_retryable | may_have_been_sent on 502/504; Idempotency-Key replays an answer, never a send (server_core::idempotency::run, over HTTP server_http::idempotency), a key covering the operation and its canonical input, not the path (server_core::idempotency::Fingerprint, S6) | done / done |
| 6 | Connection | Token expiry and refresh | — | — | access-tokens (business tokens need no re-authentication; no refresh call is documented) | expiry recorded (client::embedded_signup::StoredBusinessToken::expires_at) and reported a configurable lead ahead, 7 days by default, from the record's clear metadata with nothing decrypted (client::embedded_signup::TokenVault::token_expiry, a TokenExpiry; L11e); nothing refreshes it, as Meta documents no refresh (OPEN_QUESTIONS #8, decided: re-onboard) | 409 reconnect_required (server_core::authz, over HTTP server_http::auth); the expiry surfaced before it lapses in M3e | done / partial (M3e) |
| 7 | Connection | Webhook endpoint: the verify-token handshake, X-Hub-Signature-256 | No | Yes (client.webhook(); an unsigned mode for development) | webhooks/overview.md, webhooks/create-webhook-endpoint.md | webhooks::verify::verify_subscription; webhooks::SignatureVerifier (several secrets, fail-closed, no unsigned mode); webhooks::WebhookHandler::deliver; the axum webhooks::router | GET and POST /webhooks/meta (server_http::api::webhooks::verify, receive, over the core's pipeline since S5e, server_core::inbound::Pipeline::deliver): a missing or malformed signature header refused before the body is read, the signature checked against every app secret before parsing, 3 MiB | done / done |
| 8 | Connection | Webhook deduplication | — | — | webhooks/overview.md (Meta retries a delivery) | webhooks::dedup::DedupGuard (a lease: pending, then done or released); an event the sink refuses permanently is dead-lettered and marked done instead of holding back its batch (webhooks::deadletter::DeadLetters, WebhookHandlerBuilder::dead_letters, roadmap L21a) | the library's lease, shared by every replica through the Postgres KvStore; no dead letters (its sink classifies no error as permanent) | done / done |
| 9 | Accounts | Several numbers and accounts, routed by phone_number_id | Yes (one client per session) | Yes (one client per phone number id; a router reads the body) | solution-providers/manage-accounts.md | client::Client::with_token; client::embedded_signup::TokenVault::get_by_phone_number; every event carries its business number | tenants, keys and WABA bindings (server_core::admin::Admin, under the routes of server_api_axum::api::admin); each webhook event routed to the tenant that owns its number or WABA (server_core::events::route, owner), a dated event only when that binding began by its second (a first onboarding keeps its first second; after a move from another tenant, a second before it, by the skew margin server_core::events::MoveRules); since S2b an undated event (an error, a history chunk, an undated group update) of a WABA that moved from another tenant is operator-only for 7 days and an hour after the move (server_core::events::UndatedAfterMove, OperatorOnly by default, CurrentHolder the behaviour before: design D36), from the record of the WABA's last unbinding (server_core::model::WabaTombstone), so a previous holder's that Meta redelivers no longer reach a new holder | done / done |
| 10 | Accounts | A live event stream for a UI | — | — | none (ours to build) | webhooks::sse; adapters::sink::BroadcastSink | polling: GET /v1/events (server_api_axum::api::events::list_events); SSE and webhooks-out in M2 | done / partial (M2b, M2c) |
B. Sending messages
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 11 | Messaging | Text, with WhatsApp formatting | Yes | Yes | messages/text-messages; reference/whatsapp-business-phone-number/message-api.md | client::messages::OutboundMessage::text | POST /v1/numbers/{pn}/messages (server_api_axum::api::messages::send_message, over server_core::messages::Messages::send), type text | done / done |
| 12 | Messaging | Link previews | — | — | link-previews, messages/text-messages | OutboundMessage::text_with_preview | text.preview_url | done / done |
| 13 | Messaging | Reply to (quote) a message | Yes | Yes | messages/contextual-replies | OutboundMessage::reply_to | reply_to | done / done |
| 14 | Messaging | @-mentions | Yes | No (dropped) | no mention object in reference/whatsapp-business-phone-number/message-api.md or in the groups guides (groups/groups-messaging) | — | — | n/a — unofficial protocol |
| 15 | Messaging | Disappearing messages | Yes | No | not supported: groups lists them among the types groups do not support, embedded-signup/onboarding-business-app-users says coexistence turns them off in one-to-one chats, and reference/whatsapp-business-phone-number/message-api.md has no such option | — | — | n/a — unofficial protocol |
| 16 | Messaging | Rich responses in Meta AI's block format | Yes (sent as a forwarded bot message) | No | undocumented format | — | — | n/a — unofficial protocol |
| 17 | Messaging | Rich markdown responses rendered as Cloud messages: formatting, long text split, images as media, suggestions as buttons or lists, product cards as carousels | Yes (through row 16) | No | building blocks only: text (at most 4096 characters, messages/text-messages), media, interactive (reference/whatsapp-business-phone-number/message-api.md) | formatting and the split, in meta-whatsapp-bot: Ctx::reply_markdown renders with a MarkdownRenderer (the default Renderer: bold, italic, strike, code, quotes, lists, links, tables as padded columns or header: value lines within Renderer::table_max_width and Renderer::table_max_growth) and sends messages of at most 4096 UTF-16 code units, cut between blocks; and since B1c Ctx::reply_rich sends a RichReply of Markdown and typed parts, in order, one message each: RichReply::image as an image message by link (messages/image-messages; the renderer's URL rule, then an absolute http(s) URL with a host and no user name or password), RichReply::suggestions as reply buttons up to 3 (messages/interactive-reply-buttons-messages) and a list from 4 to 10 (messages/interactive-list-messages), RichReply::products as a product carousel of 2 to 10 cards (catalogs/interactive-product-carousel-messages), each the client's own builder checked before the first send, the body of buttons, lists and carousels the text right before them, Suggestion::command sending a command's payload so the tap runs it, a failed send stopping the reply as an Error::Step. An image goes by link only, and Markdown's  stays text | gap (the bot API, M5k) | done / gap (M5k) |
| 18 | Messaging | HTML apps in the chat bubble | Partial (Android only, undocumented) | No | undocumented; the official in-chat UI is Flows (rows 60–63) | — | — | n/a — unofficial protocol |
| 19 | Messaging | Emoji reactions | Yes | Yes | messages/reaction-messages | client::messages::Messages::react; OutboundMessage::reaction; in a bot, Ctx::react (the received message) | type reaction | done / done |
| 20 | Messaging | Edit a sent message | Yes | No | no business-side edit in reference/whatsapp-business-phone-number/message-api.md; a user's edit arrives as an unsupported type (webhooks/reference/messages/edit.md) | inbound only: webhooks::fields::MessageContent::Edit | inbound only, in message_received events | n/a — unofficial protocol (sending an edit) |
| 21 | Messaging | Delete (revoke) a sent message | Yes | No | no business-side delete in reference/whatsapp-business-phone-number/message-api.md; an inbound revoke for coexistence (webhooks/reference/messages/revoke.md) | inbound only: webhooks::fields::MessageContent::Revoke; the inbox leaves a tombstone (core::store::ConversationStore::revoke) | inbound only, recorded by the service's inbox | n/a — unofficial protocol (sending a revoke) |
| 22 | Messaging | Pin a message | Yes | No | in groups only (groups/groups-messaging); an inbound pin is an unsupported type (webhooks/reference/messages/unsupported.md) | OutboundMessage::pin, unpin; client::groups::Groups::pin_message | not in the send union (server_api_axum::api::messages::MessageType) | done / gap (M5a) |
| 23 | Messaging | Forward a message | Yes | No | no forward call in reference/whatsapp-business-phone-number/message-api.md; a media id can be sent again | a media id can be sent again (OutboundMessage::image_id and the other *_id constructors) | a media id can be sent again | n/a — unofficial protocol |
| 24 | Messaging | Location | Yes | Yes | messages/location-messages | OutboundMessage::location | type location | done / done |
| 25 | Messaging | Contact cards | Yes | Yes | messages/contacts-messages (up to 257 cards) | OutboundMessage::contacts | type contacts | done / done |
| 26 | Messaging | Polls | Yes | No | an unsupported inbound type (webhooks/reference/messages/unsupported.md) | inbound only: MessageContent::Unsupported | inbound only | n/a — unofficial protocol |
| 27 | Messaging | Event invitations | Yes (groups) | No | none in the mirror | — | — | n/a — unofficial protocol |
| 28 | Messaging | Group invite card | Yes | No | an inbound group_invite is unsupported (webhooks/reference/messages/unsupported.md); an invite link goes out as a utility template from Meta's template library, or as text or a CTA URL (groups/reference) | the link: client::groups::Group::invite_link (row 79) | — | n/a — unofficial protocol (the card; the link is row 79) |
| 29 | Messaging | Ask a user for their phone number | Yes | No | "request contact info" for users who write by username (business-scoped-user-ids) | client::messages::Interactive::RequestContactInfo | not in the send union | done / gap (M5a) |
| 30 | Messaging | Post a status | Yes | No | no status endpoint anywhere in the mirror | — | — | n/a — unofficial protocol |
| 31 | Messaging | Mark as read | Yes | Yes | messages/mark-message-as-read | Messages::mark_read | POST /v1/numbers/{pn}/messages/{message_id}/read (server_api_axum::api::messages::mark_read, over server_core::messages::Messages::mark_read) | done / done |
| 32 | Messaging | Typing indicator | Yes | Partial (only with a read receipt, which is how the Cloud API works) | typing-indicators.md | Messages::mark_read_with_typing_indicator | typing_indicator on the read route | done / done |
| 33 | Messaging | Delivery statuses (sent, delivered, read, played, failed; pricing, conversation, errors) | Yes | Yes (adds the conversation id and the error) | webhooks/reference/messages/status.md, pricing.md | webhooks::WebhookEvent::StatusUpdated; webhooks::fields::MessageStatus (played included); webhooks::fields::Pricing | status_updated events on GET /v1/events (server_core::events::TENANT_EVENT_TYPES) | done / done |
| 34 | Messaging | Opaque callback data on a send | — | — | biz_opaque_callback_data, at most 512 characters (Meta's changelog), echoed in status webhooks (webhooks/reference/messages/status.md); the message reference's schema (reference/whatsapp-business-phone-number/message-api.md) does not list it | OutboundMessage::callback_data | callback_data | done / done |
| 35 | Messaging | Send to a group | Yes (groups) | No | reference/whatsapp-business-phone-number/message-api.md (recipient_type group) | core::recipient::Recipient::Group | a {"group_id"} recipient | done / done |
| 36 | Messaging | Send by BSUID or parent BSUID | Partial (LID addresses) | No | business-scoped-user-ids | core::recipient::Recipient::User | a {"user_id"} recipient | done / done |
| 37 | Messaging | Direct Send (beta: category, TTL, configuration) | — | — | direct-send, direct-send/api-reference | OutboundMessage::category, ttl_seconds, direct_send_template_name | refused today, 422 unsupported_message_type (design §4.3) | done / gap (M5a) |
| 38 | Messaging | Payment messages (order details, order status) | — | — | payments/payments-in/orderdetailstemplate.md, payments/payments-in/orderstatustemplate.md, payments/payments-br/orders.md | India typed (P1a): client::messages::Interactive::OrderDetails and client::messages::Interactive::OrderStatus (client::payments::OrderDetails, client::payments::OrderStatus: UPI intent links, payment links, payment gateways and the older payment_configuration, every documented rule checked before the request, amounts as client::payments::Amount); Brazil typed (P1b): BRL orders (client::payments::OrderDetailsParameters::brl, client::payments::OrderDetailsParameters::brl_simplified, the order optional, a reference_id of 60, payment_type br) paid by Pix, payment link, Boleto or one-click card (client::payments::PixDynamicCode, client::payments::OffsiteCardPay), and order_status with a payment update (client::payments::OrderStatus::brl, client::payments::PaymentUpdate; pending allowed), each currency checked by its own pages' rules; the order templates' order_details button and order_status component (P1c) only through client::messages::MessageContent::Raw | — (a Graph passthrough stays a non-goal: typed routes come with payments) | partial / gap (P2) |
C. Media
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 39 | Media | Image, video, document (by id or link; caption, filename) | Yes | Yes | messages/image-messages, messages/video-messages, messages/document-messages | client::messages::Image, Video, Document | types image, video, document by id or https link | done / done |
| 40 | Media | Stickers | Yes | Yes | messages/sticker-messages | client::messages::Sticker | type sticker | done / done |
| 41 | Media | Audio file | Yes | Yes | messages/audio-messages | client::messages::Audio | type audio | done / done |
| 42 | Media | Voice note | Yes | Partial (arrives as audio) | messages/audio-messages (voice messages, Ogg with Opus) | Audio::voice | audio.voice | done / done |
| 43 | Media | Round video notes, view-once, albums (GIF playback: row 154) | Yes | No, or sent as plain media | an inbound gif is unsupported (webhooks/reference/messages/unsupported.md); view-once is unsupported in groups and turned off for coexistence (groups, embedded-signup/onboarding-business-app-users); none of these in reference/whatsapp-business-phone-number/message-api.md; the nearest to an album is a media carousel (row 51) | — | — | n/a — unofficial protocol (a GIF as a template header is row 154) |
| 44 | Media | Upload, type and size checked | Yes (URL, file or buffer) | Yes | reference/whatsapp-business-phone-number/media-upload-api.md | client::media::Media::upload | POST /v1/numbers/{pn}/media (server_http::api::media::upload_media, over server_core::media::Media::upload, the checks server_core::media::Upload::new) | done / done |
| 45 | Media | Download received media | Yes | Yes (by key) | reference/media/media-api.md, reference/media/media-download-api.md | Media::download (SHA-256 checked; of media hosts, the token goes only to Graph and lookaside.fbsbx.com, and otherwise to one GET on api.facebook.com, roadmap L10a) | GET /v1/numbers/{pn}/media/{media_id} (server_http::api::media::download_media, over server_core::media::Media::download: whole, or the core's server_core::media::MediaStream, one chunk held back); whether Meta serves a received media id under the route's phone_number_id check is unverified (OPEN_QUESTIONS #43, decided: exempt the ids the service recorded as received, M2e) | done / partial (M2e) |
| 46 | Media | Delete media | — | — | reference/media/media-api.md | Media::delete | DELETE /v1/numbers/{pn}/media/{media_id} (server_http::api::media::delete_media, over server_core::media::Media::delete) | done / done |
| 47 | Media | Resumable upload (template header handles, profile pictures) | — | — | Graph's Resumable Upload API, documented outside the WhatsApp docs; the WhatsApp pages that rely on it: templates/template-media.md (header handles), reference/whatsapp-business-profile/whatsapp-business-profile-node-api.md (pictures) | Media::resumable_upload | not exposed | done / gap (M5c1) |
| 48 | Media | Converting media (voice to Opus, stickers from images, resizing) | Yes | Yes (runs locally: ffmpeg, sharp) | none (client side); accepted formats: business-phone-numbers/media (Ogg "OPUS codecs only", "mono input only"; images "8-bit, RGB or RGBA", JPEG and PNG, 5 MB), messages/audio-messages, messages/sticker-messages (WebP, 100 KB static, 500 KB animated; no dimensions) | partial: through the port core::media::MediaConverter, voice to Opus (core::media::TargetFormat::OggOpus: mono Ogg Opus, uploaded as audio/ogg; L19), a PNG or JPEG into a static sticker (core::media::TargetFormat::StaticSticker: a static WebP within 100 000 bytes, uploaded as image/webp) or under the image limit (core::media::TargetFormat::Jpeg, core::media::TargetFormat::Png: 8-bit, within 5 000 000 bytes; L19b), each of which client::media::validate_upload takes (client::media::Media::upload_converted), implemented by adapters::media::FfmpegConverter (feature ffmpeg, off by default: an ffmpeg the integrator installs, run with fixed arguments, stdin and stdout only, its input protocols and formats restricted (an image: PNG or JPEG, by its first bytes and by ffmpeg's own detection), bounded in input, pixels, output and time, an image brought under its limit by a fixed ladder of sizes and qualities; failures core::error::ConversionError); animated stickers (Meta's 500 KB WebP) are not made, a GIF or an animated PNG is refused (L19c) | gap: the image carries no ffmpeg (design §7, M4); configuring a converter is M5c1's | partial / gap (M5c1) |
| 154 | Media | A GIF (an animated mp4) as a template's media header | Yes (GIF playback, row 43) | No, or sent as plain media (row 43) | Marketing Messages API only: marketing-messages/features.md (an animated GIF header), templates/components.md (header format GIF: an mp4 of at most 3.5 MB); no page shows the send-time header parameter for a GIF (re-read for L13 on 2026-11-01: templates/components.md, templates/template-media.md, marketing-messages/features.md, marketing-messages/send-marketing-messages.md, the MM API and Messages API references) | the definition: client::templates::HeaderFormat::Gif; the send-time parameter is not typed, for want of a page showing it (client::templates::Parameter::Other would carry it verbatim; unverified); sent through client::marketing::Marketing::send | creating the template takes Meta's JSON (POST /v1/wabas/{waba_id}/templates); Marketing Messages API sends are not exposed (row 136) | partial / gap (M5h) |
D. Interactive messages and Flows
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 49 | Interactive | Reply buttons (at most 3 on the Cloud API), header, footer | Yes (up to 10) | Partial (3) | messages/interactive-reply-buttons-messages (up to three) | client::messages::ReplyButtons (1–3 checked) | interactive button | done / done |
| 50 | Interactive | A media header on buttons | Yes | No | reference/whatsapp-business-phone-number/message-api.md (header: text, image, video, document) | client::messages::Header | header text, image, video, document | done / done |
| 51 | Interactive | Carousels (interactive media carousel, product carousel, template carousels) | Yes | No | messages/interactive-media-carousel-messages (2–10 cards), catalogs/interactive-product-carousel-messages, templates/marketing-templates/media-card-carousel-templates.md | client::messages::MediaCarousel; client::messages::ProductCarousel; client::templates::TemplateMessage::carousel | template carousels pass through; interactive carousels are not in the send union | done / partial (M5a) |
| 52 | Interactive | URL button (CTA URL) | Yes | Partial (one button) | messages/interactive-cta-url-messages | client::messages::CtaUrl | interactive cta_url | done / done |
| 53 | Interactive | Copy-code and call buttons | Yes | No | copy code: templates/authentication-templates/copy-code-button-authentication-templates.md, templates/marketing-templates/coupon-templates.md; calls: calling/call-button-messages-deep-links | TemplateMessage::copy_code_button; client::templates::Button::phone_number, Button::voice_call; client::messages::Interactive::VoiceCall | template buttons pass through; interactive voice_call is not in the send union | done / partial (M5a) |
| 54 | Interactive | Lists (at most 10 rows) | Yes | Yes | messages/interactive-list-messages (10 sections, 10 rows in all) | client::messages::ListMessage | interactive list | done / done |
| 55 | Interactive | Button and list taps received | Yes | Yes | webhooks/reference/messages/interactive.md, webhooks/reference/messages/button.md | webhooks::fields::InteractiveReply::ButtonReply, ListReply; webhooks::fields::MessageContent::Button | in message_received events | done / done |
| 56 | Interactive | Location request | Yes (location button) | No | messages/location-request-messages | client::messages::Interactive::LocationRequest | not in the send union | done / gap (M5a) |
| 57 | Interactive | Address request | Yes (address button) | Yes (sendAddressRequest) | messages/address-messages | client::messages::Interactive::Address | not in the send union | done / gap (M5a) |
| 58 | Interactive | Reminder buttons, bottom sheets, countdown offers | Yes | No | the countdown offer is the limited-time-offer template (templates/marketing-templates/limited-time-offer-templates.md: an expiration timer on the offer code), row 99; no reminder buttons or bottom sheets | the countdown: TemplateMessage::limited_time_offer (row 99) | the template passes through | n/a — unofficial protocol (reminder buttons, bottom sheets; the countdown offer is row 99) |
| 59 | Interactive | Call permission request | — | — | calling/user-call-permissions; reference/whatsapp-business-phone-number/message-api.md (call_permission_request) | client::messages::CallPermissionRequest; replies as InteractiveReply::CallPermissionReply | not in the send union; replies arrive in message_received events | done / partial (M5a) |
| 60 | Flows | Send a Flow, receive its answer | No | Yes (flows.send, a flow-response event) | flows/guides/sendingaflow, flows/guides/flowswebhooks | OutboundMessage::flow; client::messages::FlowMessage; answers as InteractiveReply::NfmReply; webhooks::WebhookEvent::FlowUpdated | answers in message_received events, flow_updated events; sending a Flow is not in the send union | done / partial (M5a) |
| 61 | Flows | Manage Flows (create, upload the JSON, preview, publish, deprecate, delete, assets) | No | Partial (list only) | flows/guides/flowsapi | client::flows::Flows::create, list; client::flows::Flow::update, upload_flow_json, publish, deprecate, delete | not exposed | done / gap (M5d) |
| 62 | Flows | The data endpoint: decrypt requests, seal responses, check signatures, decrypt uploaded media; the business public key | — | — | flows/guides/implementingyourflowendpoint, flows/guides/whatsapp-business-encryption; reference/whatsapp-business-phone-number/business-encryption-api.md | client::flows::endpoint::FlowEndpointKey::decrypt_request; client::flows::endpoint::ResponseSealer; client::flows::BusinessEncryption::set_public_key | not exposed | done / gap (M5d) |
| 63 | Flows | A typed Flow JSON builder | — | — | flows/guides/flowjson, flows/guides/components, flows/guides/media_upload, flows/reference/error-codes | client::flows::flow_json::FlowJson::builder, FlowJson::validate; client::flows::flow_json::Component; client::flows::CreateFlow::flow_json_typed; client::flows::Flow::upload_flow_json_typed | n/a (callers send Flow JSON as JSON) | done / n/a |
E. Profile, chats, contacts, calls
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 64 | Profile | Own profile (on the Cloud API, the business profile: about, address, description, email, websites, vertical, picture) | Yes (name, about, picture) | Yes (business profile) | business-profiles; reference/whatsapp-business-phone-number/whatsapp-business-profile-api.md, reference/whatsapp-business-profile/whatsapp-business-profile-node-api.md | client::business_profile::BusinessProfile::get, update; the picture by upload handle (ProfileUpdate::profile_picture_handle) | GET and PATCH /v1/numbers/{pn}/profile (server_api_axum::api::numbers::get_profile, update_profile, over server_core::numbers::Numbers::profile, update_profile), without the picture | done / partial (M5c2) |
| 65 | Profile | Display name change | — | — | display-names | client::phone_numbers::PhoneNumber::request_display_name_change | not exposed | done / gap (M5c2) |
| 66 | Profile | Business username: adopt or change it, read it, reserved names, delete it; changes by webhook | — | — | business-scoped-user-ids (§ Business usernames: POST, GET and DELETE /{Phone-Number-ID}/username, username_suggestions); reference/whatsapp-business-phone-number/whatsapp-business-account-phone-number-api.md | client::phone_numbers::PhoneNumber::set_username (the format checked first, transfer_action typed), username, username_suggestions, delete_username; the webhook webhooks::WebhookEvent::BusinessUsernameUpdated | business_username_updated events; no setter | done / partial (M5c3) |
| 67 | Chats | Archive, pin, mute, star, clear chats | Yes | No | none | the inbox tracks unread only (inbox::Inbox::mark_read) | — | n/a — unofficial protocol |
| 68 | Contacts | Check that a number is on WhatsApp | Yes | No | none: the contacts check belonged to the retired On-Premises API (Meta's changelog) | — | — | n/a — unofficial protocol |
| 69 | Contacts | Save, rename, look up contacts | Yes | No | the coexistence contacts sync (webhooks/reference/smb_app_state_sync.md) | the sync requested by onboarding after a coexistence signup (row 124; L11d); typed webhooks::WebhookEvent::AppStateSynced; the port stores and pages them per number (core::store::ConversationStore::put_contact, remove_contact, contacts, core::store::StoredContact: L5), and inbox::InboxSink records every field of an add and a remove (under inbox::synced_contact_key, switch inbox::RecordingSwitches::contacts), read back with inbox::Inbox::contacts and contact (coverage row 21; L8) | app_state_synced events; the inbox records no synced contact (server_core::inbound::INBOX_RECORDING: the library's switches with inbox::RecordingSwitches::contacts off, decided 2026-11-01) until M3b turns recording on with the routes that read them | done / partial (M3b) |
| 70 | Contacts | Privacy settings | Yes | No | none for an account's privacy; the business number's search visibility is row 129 | — | — | n/a — unofficial protocol |
| 71 | Contacts | Block, unblock, list blocked users | Yes | Yes | block-users; reference/whatsapp-business-phone-number/block-api.md | client::block_users::BlockUsers::block, unblock, list (phone number or BSUID) | not exposed | done / gap (M5g) |
| 72 | Contacts | Presence (online, typing, recording) | Yes | No | typing only (row 32) | — | — | n/a — unofficial protocol |
| 73 | Contacts | LID and username lookups | Yes | No (null) | no lookup endpoint: identities arrive on webhooks, changes as user_id_update (business-scoped-user-ids) | the BSUID and username on every event; webhooks::WebhookEvent::UserIdChanged | user_id_changed events | done / done |
| 74 | Calls | Incoming calls (accept, reject, terminate) | Yes | No | calling/user-initiated-calls; reference/whatsapp-business-phone-number/calling-api.md | webhooks::WebhookEvent::CallUpdated; client::calling::Calling::pre_accept, accept, reject, terminate | call_updated and call_status_updated events; the actions are not exposed | done / partial (M5e) |
| 75 | Calls | Rejecting calls automatically, call restrictions | Yes (auto-reject with an allow list) | No | calling/call-settings | client::calling::CallingSettings (call hours, callback permission, voicemail, restrictions) | not exposed | done / gap (M5e) |
| 151 | Contacts | Meta's contact book (a user's phone number kept with their BSUID across the portfolio): delete an entry | — | — | business-scoped-user-ids (§ Contact book: DELETE /{Phone-Number-ID}/contact_book) | client::phone_numbers::PhoneNumber::delete_contact_book_entry: an erasure at Meta, exposed as an explicit call that nothing else in the library makes (a test scans the sources); malformed and parent BSUIDs are refused before any request | — | done / gap (M5c3) |
F. Groups and channels
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 76 | Groups | Create, read, list, update, set the picture, delete | Yes | No | groups; reference/whatsapp-business-phone-number/groups-management-api.md, reference/groups/groups-query-api.md | client::groups::Groups::create, list; client::groups::Group::info, update, set_picture, delete | not exposed (sends to a group work, row 35) | done / gap (M5f) |
| 77 | Groups | Add and remove members | Yes | No | reference/groups/groups-participants-api.md | Group::add_participants, remove_participants (adding kept with a warning, OPEN_QUESTIONS #23) | not exposed | done / gap (M5f) |
| 78 | Groups | Promote and demote admins, leave a group | Yes | No | no promote, demote or leave call in the groups reference or guides (groups/reference); a participant leaving arrives as a webhook | — | — | n/a — unofficial protocol |
| 79 | Groups | Invite links, join requests | Yes | No | reference/groups/groups-invite-link-api.md, reference/groups/groups-join-requests-api.md | Group::invite_link, reset_invite_link, delete_invite_link; join_requests, approve_join_requests, reject_join_requests | not exposed | done / gap (M5f) |
| 80 | Groups | Group events (joins, leaves, settings, lifecycle) | Yes | No | groups/webhooks; fields group_lifecycle_update, group_participants_update, group_settings_update, group_status_update | webhooks::WebhookEvent::GroupUpdated | group_updated events | done / done |
| 81 | Groups | Group analytics | — | — | analytics | client::analytics::Analytics::groups | not exposed | done / gap (M5f) |
| 82 | Groups | Communities | Yes | No | none anywhere in the mirror | — | — | n/a — unofficial protocol |
| 83 | Groups | Channels (newsletters) | Yes | No | none anywhere in the mirror | — | — | n/a — unofficial protocol |
G. Bots and automation (framework features)
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 84 | Bots | Typed message events | Yes | Partial (text, media, location, contacts, reactions, button and list taps) | webhooks/reference/messages.md and its pages, one per type | webhooks::WebhookEvent (34 variants, Unknown and Unparsed included); webhooks::fields::MessageContent; in a bot, listeners by message type or event kind: Listen::MessageType, Listen::Event (a kind not in WebhookEvent::KINDS fails the build) | every reviewed type on GET /v1/events (server_core::events::TENANT_EVENT_TYPES, 28 types); the standby, handover and click types in M2d (design D25) | done / partial (M2d) |
| 85 | Bots | Commands: prefixes, arguments and flags, aliases, subcommands, guards (group, private, admin), cooldowns, generated help, error events | Yes | No (parse by hand) | Meta shows a command menu, but dispatch is ours: business-phone-numbers/conversational-components; reference/whatsapp-business-account/conversational-automation-api.md | in meta-whatsapp-bot: prefixes (PrefixParser, BotBuilder::prefixes; names case-insensitive by default), aliases (Command::alias), arguments split on whitespace with quoted strings kept whole (Args::parse), image and video captions (BotBuilder::commands_from_captions), reply buttons, list rows and quick-reply buttons by payload (Command::payload), usage hints and metadata (Command::usage, Command::metadata), guards (Command::private_only, Command::group_only, Command::owner_only: Meta's group info lists participants by wa_id alone, with no role, so the bot's owners stand in for Zaileys' group admins), per-user cooldowns (Command::cooldown, KvCooldowns on KvStore), a generated help (BotBuilder::help_command, CategoryHelp), an unknown-command hook (BotBuilder::unknown_command), error events (ErrorHandler, BotBuilder::errors), Meta's command menu (Bot::sync_command_menu, over client::phone_numbers::PhoneNumber::configure_conversational_automation), subcommands one level deep with their own guards, usage, help line and cooldown (Command::subcommand, Invocation::subcommand; B1b), and flags beside the positional arguments for a command that declares one (Command::flag, Args::has_flag, Args::flag, Args::flags; --name or --name=value, -- ends them, a quoted one is an argument, an undeclared one refuses the command with its usage, Refusal::UnknownFlag, unless Command::pass_unknown_flags; a command that declares none reads every word as an argument) | gap (the bot API, M5k) | done / gap (M5k) |
| 86 | Bots | Middleware: ordered, next(), short-circuit, wraps the handler | Yes | No | none (ours to build) | Middleware in meta-whatsapp-bot, run in registration order (BotBuilder::middleware, Registrar::middleware) with Next::run: one that does not call it stops the event, and what follows the call runs after the handler (Logging times it); built in: Logging, MarkRead. Unlike Zaileys', it runs for every event, after the ban and the command match and before the command's guards | gap (M5k) | done / gap (M5k) |
| 87 | Bots | Plugins: setup and unload hooks; loaded from a folder with hot reload | Yes | No | none | compile-time plugins in meta-whatsapp-bot: Plugin::setup registers commands, middleware and listeners through a Registrar, Plugin::on_unload runs at Bot::unload, Plugin::category names the help section; added with BotBuilder::plugin. Loading from a folder and hot reload are not offered, by design (D28): dynamic loading of Rust code is neither idiomatic nor safe, so a plugin is a crate | gap (M5k) | done / gap (M5k) |
| 88 | Bots | Sender allow and deny lists (owners, banned users) | Yes | Yes | none (ours; row 71 is Meta's block list) | in meta-whatsapp-bot: an AccessPolicy (the default AccessList: owners and bans by BSUID, AccessList::owner, AccessList::ban, or by phone number, AccessList::owner_phone, AccessList::ban_phone), set with BotBuilder::access; a banned sender's message stops before the command match and the middleware (Refusal::Banned), and owners pass Command::owner_only | gap (M5k) | done / gap (M5k) |
| 89 | Bots | Broadcast with pacing (progress, retries) | Yes (5 a second by default) | No (throws) | limits to respect: throughput.md (80 messages a second per number by default), the pair limit (131056, support/error-codes.md), templates/marketing-templates/per-user-limits.md, messaging-limits | in meta-whatsapp-bot: Broadcast (one message, or one per recipient with BroadcastBuilder::compose), each send after a slot of the number's Pacer, a swappable RateLimiter (default TokenBucket: per number, in this process, evenly spaced at Rate::DEFAULT, 80 a second, other numbers at Rate::HIGHER_THROUGHPUT or any Rate::per_second with TokenBucket::rate_for; every one-second window of 10,000 sends checked under concurrency, a slow-down and its recovery); each person once by default (BroadcastBuilder::dedupe, SendOutcome::Duplicate); progress and cancel (BroadcastHandle::progress, BroadcastHandle::cancel, the slots waited for given back, RateLimiter::release); a report per recipient (BroadcastReport, SendOutcome, RecipientReport::last_error, BroadcastEnd), kept or streamed to a ReportSink as each settles (BroadcastBuilder::report_to); retries decided by a BroadcastPolicy on ErrorKind and the code (default Backoff: the pair limit defers only that recipient on Meta's 4^X schedule, throughput retries, a number in maintenance is retried every 20 s (Backoff::MAINTENANCE_RETRY), the per-user marketing limit is reported, number-wide kinds stop the run (Backoff::STOPS, the spam limit among them) and so do a template's own refusals when everyone gets the same message (Backoff::CONTENT_STOPS); every delay at most Backoff::max_delay), only when Error::may_resend holds (the client's rule too), and through the pacer only (BroadcastBuilder::client turns the client's replays off, Client::with_retry); throttling slows the number down (SlowDownRule, default ThrottlingErrors) and a number's rate changes while running (TokenBucket::set_rate), Meta's throughput upgrade included when asked (ThroughputUpgrades, BotBuilder::follow_throughput_upgrades: a THROUGHPUT_UPGRADE raises that number to Rate::HIGHER_THROUGHPUT through RateLimiter::raise_rate, never lowering it: in memory with a TokenBucket, for every replica with a KvRateLimiter; the number found by WABA and display number, NumberDirectory). One budget per process by default; every replica on one KvStore books from one with KvRateLimiter (opt-in, B2b: a record per number under wa.bot.pacer, PACER_NAMESPACE, booked by compare-and-swap with bounded attempts, failing closed when the store does, the replicas' clocks assumed synchronized); a run does not survive a restart, a job does (row 90, KvJobs); Meta's daily messaging limit is not counted | gap (M5k) | done / gap (M5k) |
| 90 | Bots | Scheduled messages: send at a time, cancel, survive restarts, retry | Yes | No (fails when due) | Meta's WABA campaign schedules, reference only (reference/whatsapp-business-account/schedules-api.md; its audience_id is explained nowhere in the mirror) | Meta's own campaign schedules are wrapped (client::waba::Waba::schedules, client::waba::Waba::create_schedule: a template to an audience at a Unix time, kept by Meta, so it survives our restarts; never replayed after a timeout; L10b), with no cancel or retry documented; and in meta-whatsapp-bot (B3; design D29), durable jobs, a typed store on KvStore (KvJobs: KvJobs::schedule at a time, KvJobs::cancel, KvJobs::rerun), one recipient or many (each person once), run by any number of JobRunners sharing the store (JobRunner::tick, JobRunner::run_until): each due job claimed by compare-and-swap under a lease that a crashed runner's successor waits out (JobRunnerBuilder::lease), sent through a paced Broadcast, each recipient marked before its send so a restart, a crash or a second runner sends nobody twice, and a send whose outcome is unknown is never resent (JobSummary::unknown); a pass that leaves recipients provably unsent is retried with backoff, then stopped (JobRunnerBuilder::retries, JobState::Stopped); a 131049 status webhook for a message a job sent marks that recipient for 24 hours, and a re-run skips them (KvJobs::observe, JobFeedback, JobSummary::limited). Schedules are one-off (no recurrence) | gap (M5k) | done / gap (M5k) |
| 91 | Bots | Auto-delete stored messages (by age, a cap per chat) | Yes | No ("not yet" on the Cloud API) | none (local data) | done: by age, core::store::ConversationStore::purge_before (one number or all) and apply_retention with a core::store::Retention set per store (adapters::store::MemoryConversationStore::with_retention, PostgresConversationStore::with_retention, SqliteConversationStore::with_retention, RedisConversationStore::with_retention; design D10); a cap per chat, core::store::ConversationStore::trim_conversations (each conversation's newest rows kept, the summary deleted when its latest message goes, an append at the same time never lost; B4); both applied by the bot framework once or on its Timer (RetentionPolicy::apply, RetentionSweeper::run, in meta-whatsapp-bot; B4); and erasure of a person on one number, core::store::ConversationStore::erase_all over core::store::ConversationStore::identities, their group messages redacted or deleted (core::store::ErasureMode; L5) | gap: the outbox purges after 7 days (server_core::events::DEFAULT_OUTBOX_RETENTION); the inbox keeps everything (retention per store: M2a; erasure of a customer: M2f; the bot's auto-delete over HTTP: M5k) | done / gap (M2a, M2f, M5k) |
| 92 | Bots | Throttling our own typing indicators and group operations | Yes | Yes, per the summary of its configuration page (doubtful for group operations, which Zaileys does not offer on the Cloud API; unverified) | none (ours) | in meta-whatsapp-bot: BotBuilder::pacer puts the bot's outbound in a PacedOutbound, so every read receipt and typing indicator (MarkRead), reply and refusal waits for a slot of its number's Pacer, the budget its broadcasts share, and a throttled one slows the number down and is retried through the pacer (PacedOutbound::retry; PacedOutbound over a ClientOutbound does the same outside a bot, given a client with RetryPolicy::NONE); a handler reaches the pacer with Ctx::pacer; the client's group operations go through PacedGroups (client.groups(number): create, list, pin, unpin) and PacedGroup (client.group(id): info, settings, picture, delete, invite link, join requests, participants), each after a slot of the number; the paging streams are not wrapped (page by page instead). Meta documents no rate for group operations: pacing them in the number's budget is a choice | gap (M5k) | done / gap (M5k) |
H. Templates, commerce, numbers
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 93 | Templates | Send approved templates (named or positional parameters, media headers, buttons) | No | Yes | templates/overview.md, templates/components.md; reference/whatsapp-business-phone-number/message-api.md | client::templates::TemplateMessage; OutboundMessage::template | type template | done / done |
| 94 | Templates | Create, list, get, delete | No | Yes | templates/template-management.md; reference/whatsapp-business-account/message-template-api.md | client::templates::Templates::create, list, get, delete_by_name, delete_by_id, delete_by_ids | /v1/wabas/{waba_id}/templates: list, get, create, delete (server_api_axum::api::templates, over server_core::templates::Templates) | done / done |
| 95 | Templates | Edit a template | No | No | reference/whatsapp-business-account/message-template-api.md (POST /{TEMPLATE_ID}) | Templates::edit | not exposed | done / gap (M5b) |
| 96 | Templates | Template library, migration, comparison, unpausing | — | — | templates/template-library.md, templates/template-migration.md, templates/template-comparison.md, templates/template-pausing.md | Templates::library, create_from_library, migrate_from, compare, unpause | not exposed | done / gap (M5b) |
| 97 | Templates | Review, quality, category and component events | No | Yes (status only) | webhooks/reference/message_template_status_update.md, webhooks/reference/message_template_quality_update.md, webhooks/reference/template_category_update.md, webhooks/reference/message_template_components_update.md | webhooks::WebhookEvent::TemplateStatusUpdated, TemplateQualityUpdated, TemplateCategoryUpdated, TemplateComponentsUpdated, TemplateCategoryMisuseDetected | the five template_* event types | done / done |
| 98 | Templates | Authentication templates (copy code, one-tap, zero-tap, previews, bulk upsert) and an OTP service | No | Partial (copy code sent by hand) | templates/authentication-templates/authentication-templates.md and its pages | client::authentication::Authentication::create, previews, upsert; client::authentication::OtpService::issue, verify | M3c (/v1/otp/*, POST /v1/wabas/{waba_id}/templates/authentication) | done / gap (M3c) |
| 99 | Templates | Marketing and utility template kinds: coupon, limited-time offer, media-card and product-card carousels, location, call permission, catalog, MPM, SPM, product header | — | — | templates/marketing-templates.md and its pages, templates/utility-templates/utility-templates.md and its pages, catalogs/product-card-carousel-template-messages | client::templates::TemplateComponent::limited_time_offer, carousel, header_location, header_product, call_permission_request; client::templates::Button::catalog, mpm, spm; client::templates::Parameter::coupon_code | create and send take Meta's JSON (unverified per kind: no service test creates each one, M5b); what the library cannot carry is refused (coverage row 33) and counted in its own row: payment buttons (rows 38, 141), optimization_spec (row 137); app_deep_link is carried (row 155); the pre-v21 one-tap fields are superseded on the Graph API version we target | done / partial (M5b) |
| 100 | Templates | Time to live, the tap-target title override | — | — | templates/time-to-live.md, templates/tap-target-url-title-override.md | client::templates::TemplateDefinition::message_send_ttl_seconds; TemplateMessage::tap_target | through Meta's JSON (unverified: no test covers these keys through the service) | done / partial (M5b) |
| 101 | Templates | Archiving and unarchiving templates | — | — | templates/template-archival.md (POST /{WABA_ID}/message_templates/archive and …/unarchive, up to 100 template ids each, answering with the ids done and a reason for each that failed; § Sources: read on 2026-10-26), templates/template-management.md (§ Archive and unarchive templates); archival and unarchival arrive as ARCHIVED and UNARCHIVED in webhooks/reference/message_template_status_update.md | client::templates::Templates::archive, unarchive (ArchiveResult, UnarchiveResult; 1 to 100 ids, each checked as digits before any request; not replayed on timeouts; sent to the Graph endpoint with its version, where the page prints https://api.facebook.com/ with none; Graph routes the edge, probed on 2026-10-26 with a template id that does not exist; a real archive not yet run live); the status events are typed (webhooks::WebhookEvent::TemplateStatusUpdated) | the status events only (template_status_updated) | done / gap (M5b) |
| 102 | Commerce | Commerce settings (cart, catalog visibility) | — | — | catalogs/set-commerce-settings; reference/whatsapp-business-phone-number/commerce-settings-api.md | client::commerce::Commerce::settings, update_settings, set_cart_enabled, set_catalog_visible | not exposed | done / gap (M5g) |
| 103 | Commerce | List connected catalogs and their products | Yes | Yes (commerce.catalogs(), commerce.products()) | catalogs/upload-inventory sends integrators to Meta's Catalog API, outside the WhatsApp docs; read there (§ Sources, 2026-10-27): GET /{WABA_ID}/product_catalogs, GET /{product_catalog_id} and its products and product_sets edges, GET /{product_set_id} and its products edge, GET /{product_item_id} | client::waba::Waba::product_catalogs (the WABA's connected catalog); client::commerce::Catalog::get, products, products_stream, product_sets, product_sets_stream; client::commerce::ProductSet::get, products, products_stream; client::commerce::Product::get (client::commerce::ListProducts, ListSetProducts, ListProductSets carry the cursors and the edges' documented filters; the streams follow paging.cursors.after until a page without next; every id in a path checked as digits before any request). Reads only: catalog writes, and connecting a catalog to a WABA, are not wrapped (roadmap L17) | not exposed | done / gap (M5g) |
| 104 | Commerce | Product messages (single, multi, catalog, product carousel) | Yes | Yes | catalogs/single-product-messages, catalogs/multi-product-messages, catalogs/catalog-messages | OutboundMessage::product, product_list, catalog | not in the send union | done / gap (M5a) |
| 105 | Commerce | Order events | No | Yes | webhooks/reference/messages/order.md | webhooks::fields::MessageContent::Order | in message_received events | done / done |
| 106 | Marketing | Click-to-chat QR codes and short links | No | Yes (create, list, delete) | qr-codes.md; reference/whatsapp-business-phone-number/whatsapp-business-qr-code-api.md, reference/whatsapp-business-phone-number/whatsapp-business-qr-code-management-api.md | client::qr_codes::QrCodes::create, update, get, list, delete | not exposed | done / gap (M5g) |
| 107 | Analytics | Messaging, conversation, pricing, template, call and group analytics | No | Yes (conversations, messages) | analytics | client::analytics::Analytics::messaging, conversation, pricing, calls, template, template_group, groups; enable_template_insights | not exposed | done / gap (M5g) |
| 108 | Numbers | Registration: request and verify a code, register, deregister, list numbers | No | Yes | reference/whatsapp-business-phone-number/phone-number-registration.md, reference/whatsapp-business-phone-number/phone-number-verification-request-code-api.md, reference/whatsapp-business-phone-number/verify-code-api.md, reference/whatsapp-business-phone-number/phone-number-deregister-api.md, reference/whatsapp-business-account/phone-number-management-api.md | client::phone_numbers::PhoneNumber::request_code, verify_code, register, deregister; client::waba::Waba::phone_numbers | GET /v1/numbers, GET /v1/numbers/{pn} (server_api_axum::api::numbers::list_numbers, get_number, over server_core::numbers::Numbers::details); registration in M3 | done / partial (M3d) |
| 109 | Numbers | Number health: quality rating, throughput, messaging limit tier | No | Yes (info()) | reference/whatsapp-business-phone-number/whatsapp-business-account-phone-number-api.md; throughput.md | client::phone_numbers::PhoneNumberInfo (quality_rating, throughput, messaging_limit_tier); webhooks::WebhookEvent::PhoneNumberQualityUpdated | GET /v1/numbers/{pn} reports the quality rating and throughput, not the messaging limit tier (server_core::numbers::NUMBER_FIELDS); phone_number_quality_updated events (limit changes included) | done / partial (M5c3) |
| 153 | Templates | Template groups: create, read, update, delete | — | — | templates/template-groups.md (POST /{WABA_ID}/template_groups; GET, POST and DELETE /{TEMPLATE_GROUP_ID}; § Sources: read on 2026-10-26); Meta's changelog (2025-02-14) also lists GET /{WABA_ID}/template_groups, which no page documents (its Graph API reference page shows no operation) | client::templates::Templates::create_group, get_group, update_group, delete_group (TemplateGroupCreate, TemplateGroupUpdate, TemplateGroup; a group id checked as digits, template ids as integers, names and descriptions against their limits, before any request; the POSTs not replayed on timeouts), and their analytics (Analytics::template_group); not wrapped, because no page documents them: the list of a WABA's groups, and the pages after the first of a group's templates | not exposed | partial / gap (M5b) |
| 155 | Templates | Android app deep links on marketing templates' URL buttons | — | — | marketing-messages/deep-links.md (a URL button's app_deep_link: the Android deep link, a fallback URL, the Meta app id) | client::templates::Button::Url (its app_deep_link), client::templates::AppDeepLink, client::templates::Button::url_with_app_deep_link; both strings checked non-empty before any request; the page's example round-trips | passed through to Meta unchanged when a template is created (POST /v1/wabas/{waba_id}/templates; a test sends the page's example) | done / done |
I. Data and production
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 110 | Storage | Auth store | Yes (plain JSON in files, SQLite, Postgres, Redis, Convex) | No (a token only) | none (ours) | client::embedded_signup::TokenVault (AES-256-GCM, key rotation) on core::store::KvStore: adapters::store::MemoryKvStore, PostgresKvStore, SqliteKvStore, RedisKvStore | Postgres and the vault; POST /v1/admin/vault/rotate (server_core::admin::Admin::rotate_vault) | done / done |
| 111 | Storage | Message store: backends, history, chat list, a single-message lookup | Yes (memory, SQLite, Postgres, Redis, Convex) | Yes | none (ours) | core::store::ConversationStore: adapters::store::MemoryConversationStore, PostgresConversationStore, SqliteConversationStore, RedisConversationStore (L8), each passing adapters::store::conversation_conformance; the lookup by message id, scoped to the business number (core::store::ConversationStore::message, L5); inbox::Inbox::conversations, history; no Convex adapter (a hosted database of Zaileys' JavaScript world; a ConversationStore of an integrator's runs the same suite) | the service records its tenants' inbox since M1c (server_core::inbound::Pipeline, the core's webhook pipeline since S5e); its read routes come in M2a | done / partial (M2a) |
| 112 | Storage | Pluggable stores with conformance tests | Yes (custom interfaces) | Yes | none (ours) | ports in meta-whatsapp-core; executable suites adapters::store::conformance, conversation_conformance | the core's ports (server_core::store::RecordStore, IdempotencyRecords, LeaderLock, Janitor, SchemaMigrator; server_core::outbox::Outbox), bundled per database as server_core::backend::Backend with the library's KvStore and ConversationStore, implemented in memory and on Postgres (server_store_memory::MemoryBackend, PgBackend), with the contracts any backend must meet since S2 (the outbox's server_core::outbox::RouteGuard checked with each insert, server_core::model::BindOutcome::NoSuchTenant, leased turns, typed contention, byte order); and since S3 their executable suites in core, which both backends pass (server_core::conformance::run, feature conformance, over a factory of empty Backends: records, outbox, the cross-port invariants with the RouteGuard cases, capabilities, races), so a backend of an integrator's runs the same suites; and since S4 the service is composed from the bundle alone (server_http::state::AppState::from_backend, server::serve::serve_with; housekeeping is server_core::housekeeping::run over it, and the command line opens it through a server::serve::BackendFactory), so a backend of an integrator's is served with no change to the server; and since S2b every backend records a WABA's last unbinding from another tenant (server_core::model::WabaTombstone, read into server_core::model::WabaBinding) and a bind generation in server_core::model::BindingEpoch, which the suites hold them to (records, invariants, capabilities, webhooks; the Postgres one by an additive migration) (design D26) | done / done |
| 113 | Errors | Typed errors with retry guidance | Yes | Yes | support/error-codes.md | core::Error, Error::in_step; core::ErrorKind, ErrorKind::ALL, is_retryable | the §5 error model as data on ErrorKind::as_str (server_core::error, over HTTP server_http::error), each code a constant generated from the §5.2 table, so a misspelled one does not compile (server_core::error::ErrorCode, S6) | done / done |
| 114 | Ops | Logging without secrets or personal data; metrics | Partial (a custom logger) | Partial | none (ours) | tracing; secrets kept out of Debug; webhook log redaction (webhooks::redact) | request logs with the key id, Prometheus /metrics (server_http::api::ops::metrics) | done / done |
| 115 | Ops | Runtimes and packaging | Yes (Node, Bun, Deno, Termux) | Yes | none | Rust with tokio; the crates are not published yet (publishing to crates.io is the owner's: roadmap § Owner touchpoints) | a binary today; the Docker image and the TypeScript client in M4 | n/a / gap (M4) |
| 116 | Ops | Server sizing guidance | Yes (for WhatsApp Web) | — | support/load-testing.md | — | — | n/a — unofficial protocol (sizing for a WhatsApp Web session) |
| 117 | Tooling | Agent skills, LLM-readable docs, a docs MCP server, a project doctor | Yes | Yes | webhooks/overview (§ Fields), webhooks/create-webhook-endpoint, solution-providers/manage-webhooks; outside the WhatsApp docs, the Graph API reference's app/subscriptions and webhooks/subscriptions-edge (§ Sources) | consumer skills under skills/ (npx skills add vaam-apps/meta-whatsapp-rs); docs/public/llms.txt, served at the docs site's root and checked by tests/llms_txt.rs; the docs MCP server, tools/docs-mcp (just docs-mcp: the tools list, search and read, a page or one section by its heading, over MCP's stdio transport, revision 2026-07-28 and, after initialize, 2025-11-25; a tool, not a crate of the library; L22b); the project doctor is the service's | the meta-whatsapp-rs-server* skills; meta-whatsapp-server doctor (server::doctor::Doctor: every configuration refusal by variable, the backend and its migration histories read-only, the app's whatsapp_business_account subscription and fields, each bound WABA's token through debug_token and the app's subscription to it, the Solution Partner's system token) | done / done |
| 118 | Documents | Invoices, receipts, vouchers rendered to PDF or PNG | — | — | none (ours) | typst::Renderer; typst::Template::invoice, receipt, voucher | POST /v1/numbers/{pn}/documents in M4 | done / gap (M4) |
| 119 | Inbox | CMS inbox: store events, check the 24-hour window before a reply | — | — | pricing.md (the customer service window) | inbox::InboxSink; inbox::Inbox::reply; core::store::CustomerServiceWindow; the window counts the calls that reopen it and the customer's standby messages (window events, inbox::Inbox::window: L7), and a reply is refused locally while another app owns the thread (inbox::Inbox::thread_owner), each check turned off by inbox::ReplyChecks | recording since M1c; the inbox routes and the window check over HTTP in M2a | done / partial (M2a) |
J. Cloud API capabilities Zaileys does not offer
| # | Area | Capability | Zaileys (Web) | Zaileys (Cloud) | Meta Cloud API | Library | Service | Status |
|---|---|---|---|---|---|---|---|---|
| 120 | Onboarding | Embedded Signup v4: launch options, session events, code exchange, token checks, the encrypted vault, an approval gate, resume | — | — | embedded-signup/overview, embedded-signup/implementation, access-tokens | client::embedded_signup::EmbeddedSignup, onboard_with_approval, resume; client::embedded_signup::LaunchOptions; client::embedded_signup::SignupSessions | M3 (/v1/signup/*) | done / gap (M3a) |
| 121 | Onboarding | Hosted Embedded Signup, app-only install | — | — | embedded-signup/hosted-es, embedded-signup/app-only-install, webhooks/reference/account_update | the app_only_install launch feature (client::embedded_signup::FeatureName::AppOnlyInstall, refused with coexistence); hosted Embedded Signup (L11a; OPEN_QUESTIONS #11): client::embedded_signup::HostedSignup (the PARTNER_ADDED webhook's waba_info.waba_id and owner_business_id) and client::embedded_signup::EmbeddedSignup::onboard_hosted, onboard_hosted_with_approval: the business token from system_user_access_tokens on the customer's portfolio with the appsecret_proof of your system token in a form body (client::waba::Business::system_user_access_token; a fetch_only read, step fetch_token), then the code flow's steps (the WABA's owner checked against the webhook's) and the same vault record, resumed by EmbeddedSignup::resume | — | done / gap (M3f) |
| 122 | Onboarding | Several WABAs in one signup | — | — | embedded-signup/overview, embedded-signup/implementation (the session info's waba_ids), solution-providers/manage-accounts (debug_token's granular_scopes) | opt-in (L11b; OPEN_QUESTIONS #5): client::embedded_signup::OnboardingRequest::every_granted_waba, off by default (then only the claimed WABA is onboarded, as by EmbeddedSignup::onboard), and client::embedded_signup::EmbeddedSignup::onboard_wabas, onboard_wabas_with_approval (the approval once per WABA, recorded per WABA for a Solution Partner): one code exchange and debug_token, a session naming a WABA the token was not granted refused, then each granted WABA verified, stored, subscribed and funded on its own, the claimed one with the request as it is, the others registering and syncing nothing and onboarded only when Meta reports the claimed WABA's owner as theirs, one the vault holds left alone and none replaced (stored only where absent); per-WABA outcomes in client::embedded_signup::WabasOnboarded (client::embedded_signup::WabaOutcome), a failure not failing the others, each resumed on its own by EmbeddedSignup::resume; hosted signup onboards one WABA per PARTNER_ADDED | inherits it (M3f) | done / gap (M3f) |
| 123 | Onboarding | Pre-verified numbers: pools, adding, codes, sharing, partners | — | — | embedded-signup/pre-verified-numbers; reference/business/whatsapp-business-pre-verified-phone-numbers-api.md, reference/business/whatsapp-business-pre-verified-phone-number-sharing-api.md, reference/business/add-phone-numbers-api.md, reference/whatsapp-business-pre-verified-phone-number/whatsapp-business-pre-verified-phone-number-api.md, reference/whatsapp-business-pre-verified-phone-number/request-verification-code-api.md, reference/whatsapp-business-pre-verified-phone-number/verify-code-api.md, reference/whatsapp-business-pre-verified-phone-number/whatsapp-business-pre-verified-phone-number-partners-api.md | the pool on a partner portfolio (L11c): client::waba::Business::add_pre_verified_number, pre_verified_numbers (filtered by number and code_verification_status, with a stream), share_pre_verified_number (Meta removed unsharing); each number through client::Client::pre_verified_phone_number (client::waba::PreVerifiedPhoneNumber::get, request_code, verify_code with the secret client::phone_numbers::VerificationCode, partners, delete), its id a new numeric id type of core, checked as digits (core::ids::is_numeric_id) when built and again before any request; offered by client::embedded_signup::LaunchOptions::pre_verified_phone_ids and added to a WABA by client::waba::NewPhoneNumber::preverified_id | — | done / gap (M3f) |
| 124 | Onboarding | Coexistence (WhatsApp Business app users): onboarding, contacts and history sync, echoes | — | — | embedded-signup/onboarding-business-app-users; webhooks/reference/history.md, webhooks/reference/smb_message_echoes.md, webhooks/reference/smb_app_state_sync.md | LaunchOptions::coexistence; client::phone_numbers::PhoneNumber::sync_smb_app_data, requested by onboarding itself by default (L11d; OPEN_QUESTIONS #7): steps sync_contacts and sync_history after a coexistence onboarding, turned off by client::embedded_signup::OnboardingRequest::coexistence_sync, Meta's answers in client::embedded_signup::Onboarded::coexistence_sync (client::embedded_signup::SyncOutcome), never posted twice for one onboarding (a record under client::embedded_signup::COEXISTENCE_NAMESPACE); webhooks::WebhookEvent::HistorySynced, MessageEchoed; recorded by the inbox (coverage row 21) | echoes and history recorded and delivered as events since M1c; onboarding and the automatic sync (D7) in M3b | done / partial (M3b) |
| 125 | Onboarding | In-App Signup (opt-in deep links, promo codes) | — | — | in-app-signup | client::signups::Signups::create, list; client::signups::deep_link | not exposed; accepting Meta's terms on a business's first signup comes in M5i as an explicit, audited operator action, off by default (OPEN_QUESTIONS #26, decided: the deployer's act) | done / gap (M5i) |
| 126 | WABA | Details, subscribed apps and the callback override, assigned users, client and owned WABAs, customer bases | — | Partial (lists numbers) | whatsapp-business-accounts.md, webhooks/override.md; reference/whatsapp-business-account/whatsapp-business-account-api.md, reference/whatsapp-business-account/subscribed-apps-api.md, reference/whatsapp-business-account/assigned-users-management-api.md, reference/business/client-whatsapp-business-accounts-api.md, reference/business/owned-whatsapp-business-accounts.md | client::waba::Waba; client::waba::Business | GET /v1/wabas, disconnect (server_api_axum::api::numbers::list_wabas, disconnect_waba, over server_core::numbers::Waba::disconnect); admin attach and unbind | done / partial (M5c4) |
| 127 | WABA | Create a WABA, its activities, system users and their tokens for client businesses, a WABA's solutions | — | — | reference/business/whatsapp-business-accounts-api.md (POST), solution-providers/partner-initiated-waba-creation.md, reference/whatsapp-business-account/whatsapp-business-account-activities-api.md, reference/whatsapp-business-account/whatsapp-business-account-solutions-list-api.md, solution-providers/manage-system-users.md, system_user_access_tokens (marketing-messages/onboard-business-customers.md, embedded-signup/hosted-es) | client::waba::Business::create_whatsapp_business_account (NewWaba: every field of the creation request but the deprecated On-Behalf-Of on_behalf_of_business_id; ISO 4217 currency checked locally; never replayed after a timeout); partner-initiated creation is a Meta Business Suite flow, with no API call to wrap (partner-initiated-waba-creation); client::waba::Waba::activities and client::waba::Waba::solutions (ListWabaRecords, paged, with streams); client::waba::Business::system_users (on the business portfolio: manage-system-users prints a WABA id, its prose and the Business node's reference a business id); client::waba::Business::system_user_access_token (SystemUserTokenRequest, every parameter of facebook-login-for-business § Get tokens; the appsecret_proof of the token the request carries (the partner's) computed from the app secret and sent in a form body; the answer's token an AccessToken, no error quoting the body; a generation never replayed, a fetch_only read replayed as a read) (L10b) | — | done / gap (M5c4) |
| 128 | Numbers | Two-step PIN, data localization, identity-key check, per-number webhook override | — | — | business-phone-numbers/two-step-verification, no-storage.md, local-storage, identity-change; reference/whatsapp-business-phone-number/settings-api.md | PhoneNumber::set_two_step_pin, enable_local_storage, set_identity_key_check, set_webhook_override | the PIN in M3d; the rest not exposed (M5c3) | done / gap (M3d, M5c3) |
| 129 | Numbers | Search visibility, security notifications, notifying users of a number change | — | — | reference/whatsapp-business-phone-number/whatsapp-business-account-phone-number-api.md (search_visibility, show_security_notifications, notify_user_change_number) | client::phone_numbers::PhoneNumber::set_search_visibility, set_security_notifications, set_notify_user_change_number: fields of the number's own POST (whatsapp_business_api_data for the last two), one per call; the page has no example, so the bodies follow its schema | — | done / gap (M5c3) |
| 130 | Numbers | Official Business Account: request and status | — | — | official-business-accounts.md; reference/whatsapp-business-phone-number/whatsapp-business-account-official-business-account-status-api.md | client::phone_numbers::PhoneNumber::official_business_account (the guide's official_business_account field; the reference's edge of the same name has no example), request_official_business_account (an application to Meta, explicit, never automatic; never replayed after a timeout, only when Meta refused it before processing it, Error::may_resend); the flag PhoneNumberInfo::is_official_business_account. The reference says the endpoint also withdraws an application but documents no field for it: not offered | — | done / gap (M5c3) |
| 131 | Numbers | Business compliance information (India) | — | — | reference/whatsapp-business-phone-number/business-compliance-information-api.md | client::phone_numbers::PhoneNumber::business_compliance_info, set_business_compliance_info (the page's validation rules checked first; it has no example, so the types follow its schemas) | — | done / gap (M5c3) |
| 132 | Numbers | Health status of a number, a WABA or a business | — | — | support/health-status.md | one type, client::common::HealthStatus (entities, can_send_message, can_receive_call_sip, errors, additional info): client::phone_numbers::PhoneNumber::health_status, client::waba::Waba::health_status, and the health_status of client::templates::TemplateInfo. Meta documents the field on numbers, WABAs and templates only; a business portfolio's status is the BUSINESS entity each of them returns (HealthStatus::entity) | — | done / gap (M5c3) |
| 133 | Bots | Conversational components (welcome message, ice breakers, commands); bot details | — | — | business-phone-numbers/conversational-components; reference/whatsapp-business-account/conversational-automation-api.md, reference/whatsapp-business-bot/bot-details-api.md | PhoneNumber::conversational_automation, configure_conversational_automation; bot details client::phone_numbers::WabaBot::get (Client::waba_bot; the page has no example, and no mirrored page says where a bot id comes from) | not exposed | done / gap (M5c3) |
| 134 | Calling | Calling settings (hours, SIP, voicemail, icons), permissions, business-initiated calls | — | — | calling, calling/call-settings, calling/business-initiated-calls; reference/whatsapp-business-phone-number/calling-api.md | client::calling::Calling::settings, update_settings, permissions, connect; client::calling::CallingSettings (signalling only: WebRTC media is the integrator's stack) | not exposed | done / gap (M5e) |
| 135 | Calling | Call recording and transcription | — | — | calling/call-recording, calling/call-transcription | the per-call recording and transcription objects on connect and accept: client::calling::ConnectCall::recording, client::calling::ConnectCall::transcription, client::calling::Calling::accept_with with client::calling::AcceptOptions (client::calling::CallRecording, client::calling::CallTranscription: off unless set; purpose and announcement_language required with ENABLED, purpose at most 250 characters, checked locally; L14); the webhooks are typed (call_recording_available, call_transcription_available in webhooks::fields) and the files download through Media::download by the media id they carry; the downloaded transcript document is not typed | the call events only | done / gap (M5e) |
| 136 | Marketing | Marketing Messages API: send (product policy, activity sharing, bid multiplier), onboarding, the Cloud API marketing switch, partner onboarding to MM Lite | — | — | marketing-messages/overview.md, marketing-messages/send-marketing-messages.md, marketing-messages/onboarding.md; reference/whatsapp-business-phone-number/marketing-messages-api-for-whatsapp.md, reference/business/whatsapp-business-partner-onboarding-to-mm-lite-api.md | client::marketing::Marketing::send; client::marketing::MarketingAccount::onboarding_status, set_cloud_api_marketing_disabled; client::marketing::MarketingBusiness::request_onboarding | not exposed | done / gap (M5h) |
| 137 | Marketing | Max price (agreement, partner allow list, duplicating a template at another price), reach estimates, click tracking (metrics and conversion measurement are Meta's ads APIs: not part of this row) | — | — | marketing-messages/pricing/enroll-max-price.md, marketing-messages/pricing/duplicate-templates.md, marketing-messages/pricing.md (GET /{WABA_ID}/reachestimate), marketing-messages/changelog.md (2026-10-01: partners no longer allowlist clients), marketing-messages/view-metrics.md, marketing-messages/measure-conversion.md, marketing-messages/track-click-events.md | the agreement: client::marketing::MarketingBusiness::sign_max_price_agreement, which signs Meta's beta agreement on the business's behalf, a legal act: its own explicit call, which no other call makes, and signing is the deployer's (OPEN_QUESTIONS #45); its state, client::marketing::MarketingBusiness::max_price_agreement; duplicating a template, client::marketing::MarketingAccount::duplicate_template; reach estimates, client::marketing::MarketingAccount::reach_estimate; a template's max price, client::marketing::MarketingTemplate::set_max_price (L13). The partner allow list was withdrawn by Meta with the open beta (2026-10-01): a partner's agreement covers its shared and managed client WABAs, so there is nothing to wrap. Click tracking opt-out: client::analytics::Analytics::set_button_click_tracking; clicks as webhooks::WebhookEvent::UserActionReported. The metrics and conversion measurement come from Meta's ads APIs (insights on ad objects, events for ads), outside the Cloud API | clicks are operator-only until M2d (D25); max price in M5h, its agreement an explicit, audited operator action | done / gap (M2d, M5h) |
| 138 | Marketing | Click-to-WhatsApp: the referral on inbound messages, automatic events, welcome message sequences | — | — | ctwa/welcome-message-sequences; embedded-signup/automatic-events-api | webhooks::fields::Referral; webhooks::WebhookEvent::AutomaticEventDetected; the sequences (/{WABA_ID}/welcome_message_sequences): client::marketing::WelcomeSequences (create, update, list, get, delete; L13; coverage row 31); naming a sequence in an ad creative is Meta's Marketing API, and reporting automatic events to Meta's Conversions API (optional) is outside the Cloud API: neither is part of this row | the referral in message_received events, automatic_event_detected events | done / partial (M5h) |
| 139 | Routing | Conversation routing: thread control (pass, take, release), standby, conversation context | — | — | conversation-routing/thread-control, conversation-routing/overview, conversation-routing/routing-roles; webhooks/reference/messaging-handovers (nm), webhooks/reference/standby (nm) | typed webhooks webhooks::WebhookEvent::ThreadControlChanged, StandbyObserved; the port stores ownership and window events (core::store::ConversationStore::set_thread_owner, record_window_event: L5); the inbox records ownership from the handovers and standby copies and standby messages as window events, derives this app and idle (inbox::Inbox::thread_owner), records this app's release, pass and take (inbox::Inbox::record_release, inbox::Inbox::record_thread_owner) and refuses a reply locally while another app owns the thread (core::ErrorKind::ThreadOwnedElsewhere; OPEN_QUESTIONS #44: L7), without storing conversation_context; the thread control API: pass, take and release (client::thread_control::ThreadControl::pass, take, release; L15), each never replayed, which the inbox does not call itself (the caller records its own call); the Routing Roles API, the roles the business holds on a number and its standby grant (client::thread_control::ThreadControl::routing_roles, answering client::thread_control::RoutingRoles; L15a) | the two types are operator-only until M2d (D25); the API in M5l | done / gap (M2d, M5l) |
| 140 | Accounts | Account model evolution: Messaging Accounts and WhatsApp accounts (WAAC); general availability from H2 2026 | — | — | account-model-evolution, account-model-evolution/messaging; reference/whatsapp-account-number/whatsapp-account-number-api.md | partial (coverage row 30): the Messaging Account a send bills, client::messages::OutboundMessage::messaging_account_id (L16a; a WABA id names its Messaging Account, so the WABA calls reach it unchanged); not the WAAC and Messaging Account endpoints, the WAAC id or the whatsapp_account webhook, which Meta publishes in its phase 2, H1 2027 (L16b) | — | partial / gap (M5c4, M5c5) |
| 141 | Payments | Payments in India (UPI, payment links, order templates, onboarding) and Brazil (Pix, Boleto, payment links, one-click) | — | — | payments/payments-in/overview.md, payments/payments-br/overview.md and their pages | India (P1a): the order messages (row 38), the payment lookup and refunds (client::payments::Payments::lookup, client::payments::Payments::refund: never replayed after a timeout) and onboarding (client::payments::PaymentConfigurations: list, get, create, regenerate_oauth_link, delete; the OAuth link a client::common::SecretUrl); webhooks::WebhookEvent::PaymentConfigurationUpdated. Brazil (P1b): Pix (copy and paste, or Pix with redirect), payment links, Boleto and one-click payments in the order messages (row 38). Not yet: the payment status webhook of type payment and the payment_method reply, still marked out of scope in the webhooks' conformance manifest, and the templates' payment buttons (P1c); the data endpoint of POST /{WABA_ID}/payment_configuration/{CONFIGURATION_NAME} (for checkout button templates, its answer undocumented) and the alpha third-party validation | payment_configuration_updated events only | partial / partial (P2) |
| 142 | Partners | Credit lines: share, attach, verify, revoke | — | — | solution-providers/share-and-revoke-credit-lines.md; reference/whatsapp-business-account/extended-credits-api.md | client::credit_lines::CreditLines::list, share_and_attach, revoke, allocation_status | M3a (Solution Partner mode) | done / gap (M3a) |
| 143 | Partners | Partner-led business verification | — | — | solution-providers/partner-led-business-verification.md | client::business_verification::BusinessVerification::submit, submissions, status | not exposed | done / gap (M5j) |
| 144 | Partners | Multi-Partner Solutions: create, accept or reject, deactivation, the solution token, an app's solutions and client businesses | — | — | solution-providers/multi-partner-solutions.md, solution-providers/multi-partner-solution-embedded-creation.md; reference/whatsapp-business-solution/solution-details-api.md and the pages beside it, reference/application/application-solutions-api.md, reference/application/application-connected-client-businesses.md | client::partners::solutions::PartnerSolutions::create, accept, reject, send_deactivation_request, accept_deactivation_request, reject_deactivation_request, get, partners, app_solutions, connected_client_businesses (each list with its …_stream), and the client's business token through a solution, PartnerSolutions::access_token (an AccessToken, withheld from decode errors); webhooks::WebhookEvent::PartnerSolutionUpdated; LaunchOptions::solution_id | partner_solution_updated is operator-only (no WABA to route it by) | done / gap (M5j) |
| 145 | Partners | Moving numbers and WABAs between partners or solutions | — | — | solution-providers/support/migrating-phone-numbers-among-solution-partners-programmatically.md and the other migration pages; reference/whatsapp-business-account-migration-intent/migration-intent-details-api.md | the number migration flag client::waba::NewPhoneNumber::migrate_phone_number with the code and register calls; migration intents, client::partners::migration::MigrationIntents::create (set_solution_migration_intent) and get (MigrationIntentInfo::is_accepted); the templates of a moved WABA, Templates::migrate_from (row 96); the rest of the migration pages are Meta Business Suite and Embedded Signup steps, not calls | — | done / gap (M5j) |
| 146 | Partners | A user's assigned WABAs | — | — | reference/user/assigned-whatsapp-business-accounts-api.md | client::waba::BusinessUser::assigned_whatsapp_business_accounts (ListAssignedWabas: fields, limit, cursors; a stream) (L10b) | — | done / gap (M5j) |
| 147 | Billing | Billing currency or payment method migration | — | — | pricing/change-billing-currency.md (set_payment_method_migration_intent) | client::waba::Waba::set_payment_method_migration_intent (PaymentMethodMigrationIntent: the currency checked locally, an optional Line of Credit), client::waba::PaymentMethodMigration::status, client::waba::PaymentMethodMigration::resume_migration: the intent and the resume are explicit calls no other call of the library makes, neither replayed after a timeout (L10b) | — | done / gap (M5j) |
| 148 | History | Message history events (the delivery events of one history entry) | — | — | reference/message-history/whatsapp-business-message-history-events-api.md (where its id comes from is not in the mirror) | client::waba::MessageHistory::events (ListMessageHistoryEvents: delivery_status, fields, limit, cursors; a stream); the history id is the caller's, Meta documents no call that gives it (L10b) | — | done / gap (M5c4) |
| 149 | Policy | Account, policy and security events (alerts, violations, reviews, capability, name, quality, security) | — | — | webhooks/reference/account_alerts.md, webhooks/reference/account_update.md, webhooks/reference/account_review_update.md, webhooks/reference/business_capability_update.md, webhooks/reference/phone_number_name_update.md, webhooks/reference/security.md; policy-enforcement.md | webhooks::WebhookEvent::AccountAlert, AccountReviewUpdated, AccountUpdated, AccountSettingsUpdated, BusinessCapabilityUpdated, PhoneNumberNameUpdated, SecurityUpdated | all seven are tenant event types | done / done |
| 150 | Policy | Marketing opt-out (user preferences) | — | — | webhooks/reference/user_preferences.md | webhooks::WebhookEvent::UserPreferenceChanged; core::ErrorKind::MarketingOptedOut (131050) | user_preference_changed events; 409 marketing_opted_out | done / done |
| 152 | Accounts | Parent BSUID accounts: the portfolios that share parent BSUIDs | — | — | business-scoped-user-ids (§ Get parent BSUID account, served from api.facebook.com) | client::waba::Business::parent_bsuid_account (a ParentBsuidAccount): the one request the client's credential rules (client::GraphRequest) let take a token to api.facebook.com, GET /{digits}/parent-bsuid-accounts over https on the default port, with no version segment; every other path, method, scheme or port on that host is refused before any request (roadmap L10a) | — | done / gap (M5c4) |
| 156 | Numbers | Payload encryption, connection status and webhook URL on a number | — | — | reference/whatsapp-business-phone-number/settings-api.md (payload_encryption: status, client_encryption_key, client_encryption_key_fingerprint, cloud_encryption_key); reference/whatsapp-business-phone-number/whatsapp-business-account-phone-number-api.md (connection_status, webhook_url) | client::phone_numbers::PhoneNumber::enable_payload_encryption (status and the key, a client::phone_numbers::ClientEncryptionKey checked to be base64 and not an obvious private key, whose Debug shows only its length), disable_payload_encryption, and the answer of settings (client::phone_numbers::PayloadEncryption: status, optional so that an answer without it still parses, client_encryption_key_fingerprint, cloud_encryption_key, a client::phone_numbers::CloudEncryptionKey shown as a length in Debug; decode errors withhold the body); set_connection_status (client::phone_numbers::ConnectionStatus) and set_webhook_url (an absolute https URI, no user info or fragment, sent as given), fields of the number's own POST, one per call. The four setters are not replayed after a timeout. Neither page has an example: the bodies follow the schemas, and the module docs name what the pages leave out (roadmap L26) | — | done / gap (M5c3) |
Plan to parity
roadmap.md cites every partial and gap row from a PR-sized item, each naming its crate, what it comes after and its decisive test:
- The service's modular split (S1–S18 and U1–U4, design D26): a framework-free core with ports, CrateStack as the default API and store once the owner accepts design D20 (a), the axum API (permanent, the swap target) and the sqlx store kept, MongoDB later. Row 112, and the ground every service milestone below builds on.
- The bot framework, a new library crate
meta-whatsapp-bot: commands, middleware, compile-time plugins and markdown replies (B1, done; rows 17, 85–88), subcommands and flags (B1b, done; row 85), rich replies beyond text (B1c, done; row 17), paced broadcast (B2, done; rows 89, 92; D29), durable scheduling (B3, done; row 90; D29), and retention and auto-delete after theConversationStoreport change (B4, after L5, done; row 91). - Library gap batches (L4–L26), by crate and topic: every library partial or gap row not in the bot framework or payments.
- Service milestones: M2a–M2f (inbox, SSE, webhooks-out, D25, erasure), M3a–M3f (Embedded Signup, OTP, coexistence), M4 (packaging), M5a–M5l (routes for the modules parity requires, the bot and broadcast APIs included).
- Payments (India, Brazil; P1a–P1c, P2): in scope, last; India's messages, lookup, refunds and onboarding landed (P1a), and Brazil's messages (P1b).
How to keep this true
- A pull request that changes a capability updates its row here in the same PR, with the symbol that now does it, and its category in categories.md and its row in coverage.md when their status moves. A row says done only when the code shows it; when in doubt it says unverified.
- A new capability gets the next free row number, in the section it belongs to (numbers never move), and the counts at the top are recounted.
- When a page marked (nm) reaches the mirror, read it: drop the mark, and correct the row if the page says otherwise.
- The PR description says what happened to this table, as it does for the docs and skills (CONTRIBUTING.md § Docs and skills parity).