X-Hub-Signature-256. Simulate endpoints act as WhatsApp users and as WhatsApp itself, so an app gets the inbound messages, read receipts, and failures it has to handle. Meta ships no official Node SDK, so the tests drive the emulator over raw HTTP; any HTTP client or community SDK works with its Graph base URL changed.
At a glance
Start
--service. When it is the only enabled service, it listens on http://localhost:4000.
Default fixtures
With no seed config the service starts with:- App
1000000000000001with app secretbaseline-whatsapp-app-secret, which signs webhooks. No callback URL, so no webhooks until one is set - System user access token
baseline-whatsapp-token, allowed on every business account - WhatsApp Business Account
102290129340398(“Baseline Business”), with the app subscribed - Phone number id
106540352242922, display number+1 555-078-3881, verified name “Baseline” - Approved
en_UStemplateshello_world(Meta’s sample, no variables) andorder_update(Hi {{1}}, your order {{2}} has shipped.with the quick replies “Track order” and “Talk to us”)
URL mapping
Graph versions
v21.0 through v26.0 are served. Older versions are answered as v21.0 and say so in a facebook-api-version response header, as Graph does after a version retires; versions that do not exist yet get the unknown path error.
Auth
Authorization: Bearer <token>, oraccess_tokenin the query string or the body- Tokens seeded under
access_tokensare system users. Withbusiness_accountslisted, a token sees only those accounts and their numbers, templates, and media; anything else answers as if it did not exist (code100, subcode33) - Any other token the CLI accepts acts as a system user on every account
- The app token
{app-id}|{app-secret}is needed for/{app-id}/subscriptions; a user token there gets code15 - No token: HTTP 400 code
104. An unknown token: HTTP 401 code190with aWWW-Authenticateheader
Errors
Errors use Graph’s body:
The emulator never rate limits. Inject throttling errors such as
130429 or 131056 with fault injection to test retries.
Messages
POST /{phone-number-id}/messages with messaging_product: "whatsapp", recipient_type: "individual" (optional), and to:
context.message_id makes a reply; biz_opaque_callback_data comes back on the message’s status webhooks. The answer is { messaging_product, contacts: [{ input, wa_id }], messages: [{ id }] }, and template sends add message_status: "accepted". Sending to the business’s own number is 131021.
Templates are matched by name and language. A missing pair is HTTP 404 code 132001; a paused template 132015; a disabled one 132016; wrong parameter counts 132000; a parameter that breaks the template’s rules 132012 or 132018. Named templates (parameter_format: NAMED) need parameter_name on each body parameter. Header media, URL button suffixes, and quick reply payloads are checked against the template’s components.
Mark an inbound message read, optionally with a typing indicator that lasts until the next reply:
Customer service window
A user’s message opens a 24 hour window between that user and the business number. Inside it any message type may be sent. Outside it, a free form message is still accepted with HTTP 200, and then its status webhook arrives asfailed with error 131047 (“Re-engagement message”), which is how WhatsApp reports it. Templates can be sent at any time. A contact seeded with undeliverable: true fails with 131026.
Every other outbound message gets a sent and then a delivered status webhook, in that order. Read, played, and failed statuses come from the simulate endpoint. Status payloads follow the subscription’s webhook version: from v24.0 (the default) pricing comes only with delivered and there is no conversation object; earlier versions get conversation and pricing on sent and delivered. Service messages, and utility templates inside an open window, are priced as free_customer_service.
Templates
GET /{waba-id}/message_templates— List templates: filters byname,status,category,language, andcontent, selects withfields, and pages withlimit,after, andbeforePOST /{waba-id}/message_templates— Create template: withname,language,category(MARKETING,UTILITY,AUTHENTICATION),components, andparameter_format. Names are lower case letters, digits, and underscores; variables must be sequential (positional) or named consistently, and may not start or end the body; button and header limits follow WhatsApp’s. Failures use code100with Meta’s subcodes, such as2388024for a name and language that already exist,2388042for an invalid name,2388299for a variable at the start or end, and2388019past 250 templates. The answer is{ id, status, category }DELETE /{waba-id}/message_templates?name=— Delete template: deletes every language of a template, or one withhsm_idGET /{template-id}andPOST /{template-id}— Get or edit template:POSTeditscomponentsorcategory. Approved, rejected, and paused templates can be edited (otherwise subcode2388039), and edits go back through review
template_review: approve (the default) answers APPROVED at once, pending leaves them PENDING until the simulate endpoint decides, and reject rejects them. Every change sends a message_template_status_update webhook.
Media
POST /{phone-number-id}/media— Upload media: multipart form withfile,type, andmessaging_product. WhatsApp’s supported types and size limits apply (images 5 MB, video and audio 16 MB, documents 100 MB, stickers 500 KB); others fail with131053. The answer is{ id }GET /{media-id}— Get media URL: returns{ url, mime_type, sha256, file_size, id, messaging_product }. The URL works for five minutes and needs the sameAuthorizationheader, as on WhatsAppDELETE /{media-id}— Delete media.GETandDELETEboth take an optionalphone_number_idthat must own the media- Uploads expire after 30 days
id; fetch it the same way.
Phone numbers and accounts
GET /{phone-number-id}andGET /{waba-id}/phone_numbers— Get phone numbers:display_phone_number,verified_name,quality_rating,code_verification_status,platform_type,throughput,messaging_limit_tier, and more withfieldsGET /{waba-id}— Get business account: name, currency, timezone, template namespace, and review statusGET/POST/DELETE /{waba-id}/subscribed_appssubscribes the app to the account’s webhooks
Webhooks
The app’s subscription comes fromapp.webhook in the seed config or from POST /{app-id}/subscriptions with the app token and object=whatsapp_business_account, callback_url, verify_token, and fields. GET /{app-id}/subscriptions lists it and DELETE removes it.
Setting a subscription runs Meta’s verification handshake:
2200 and Meta’s message, and nothing is saved. A callback set in seed config is verified before the first delivery, and POST /_whatsapp/webhooks/verify runs the handshake on demand and returns the outcome.
Deliveries are POSTs of:
X-Hub-Signature-256: sha256=<HMAC-SHA256 of the raw body keyed by the app secret> and the older X-Hub-Signature: sha1=...:
messages, message_template_status_update, and the others Meta lists) and accounts with the app in subscribed_apps get deliveries. They go out one at a time in the order they happened, so sent always arrives before delivered. A non 2xx answer or a timeout is retried after 1, 2, 4, 8, 16, 32, and 60 seconds, a compressed form of Meta’s schedule. The inspector shows every attempt.
Simulate
These endpoints need no token. Each returns what it did plus the webhook deliveries it caused.phone_number_id picks the business number and defaults to the first seeded one.
Seed config
- IDs are optional; generated ones are 16 digit Graph ids. Write numeric ids as strings
- Seeded templates are checked like created ones and default to
APPROVED; setstatusto seed others subscribed: falseleaves an account without the app insubscribed_apps, so it gets no webhookswindow_open: trueopens the contact’s window with every seeded number- Any number can receive messages;
contactsonly names users and marks undeliverable ones
Inspector
GET / shows conversations per business number with each thread’s messages, statuses, and window state, the templates with their components and status, uploaded media, webhook deliveries with every attempt, and the app, tokens, accounts, and numbers.
Limits
- One app per service, and no unversioned Graph paths
- No groups, usernames, per account or per number override callback URLs, business profiles, phone number registration, two step verification, or display name changes
- Media links in outbound messages are stored as sent, not downloaded
- Flow and product messages are accepted without checking a catalog or flow; the Marketing Messages API is not modeled
- No rate limits or messaging tier limits
Conformance
See CONFORMANCE.md for every provider.