Validation guide
Audience: Developers integrating WhatsApp via 360dialog Status: Pre-GA. Use this guide to certify your integration before usernames reach GA. Companion page: [alpha] Usernames
Why this guide exists
There is no live "username-only" user you can create on demand — Meta enables username adoption per country, on its own schedule, and phone numbers still appear in every webhook until GA.
You don't need to. The breaking change is a deterministic contract, not new runtime behavior: at GA, certain phone fields are simply omitted from webhooks you already receive. You can validate every part of that contract today by combining:
Live production traffic — BSUIDs and BSUID-sending are already live, so most of the integration is testable against real data now.
Replayed fixtures — for the one thing you can't reproduce live (a webhook with the phone field absent), replay the golden payloads in this guide through your own staging pipeline.
Work through the stages in order. Each has a clear Pass criteria. At the end, complete the self-certification checklist (§10) and you're GA-ready.
Throughout, the test consumer is
RS.1502576394655843(BSUID) /310601600963(phone). Replace with your own values.
Before you start
You'll need:
A WhatsApp number connected via 360dialog (
https://waba-v2.360dialog.io,D360-API-KEYheader).A staging webhook endpoint you can POST test payloads to, isolated from production data.
At least one recent real inbound webhook captured from your number (it already contains a BSUID — see Stage 1).
A way to replay JSON to your webhook endpoint (e.g.
curl, Postman, or your test harness).
⚠️ Replay fixtures only into staging. The payloads below use example IDs and must not create real records in production.
Confirm you are capturing the BSUID (live)
The BSUID is already in every production webhook. Verify you persist it.
Steps
Send a message to your business number from any WhatsApp account.
Inspect the inbound webhook. Confirm these fields are present and stored:
contacts[0].user_idmessages[0].from_user_id
Confirm you store
user_idkeyed per business portfolio, not globally.
Pass criteria: the BSUID from a real webhook lands in your datastore, associated with the correct portfolio, on the same record as the phone number.
Send a message to a BSUID (early July)
Sending to a BSUID works in production today — no allowlist.
Steps
Take a
user_idfrom a recent webhook (the user's 24h window must be open, or use a template).Send using the
recipientfield (notto):
Confirm your code parses the BSUID-only send response shape:
Store the returned
wamid.
Pass criteria: the message sends; your parser reads messages[0].id and contacts[0].user_id (it does not expect wa_id); the wamid is persisted for status correlation.
Handle phone-withheld webhooks (fixtures — the breaking case)
This is what changes at GA. Replay each fixture below into your staging endpoint and confirm your pipeline routes on the BSUID and never crashes on a missing phone.
3a. Inbound message — BSUID only (no wa_id, no from)
wa_id, no from)Pass criteria: message is ingested and routed to the right conversation using from_user_id; no null-pointer/validation error from the missing from / wa_id; username is stored as a display string (no @) and not used as a key.
3b. Status webhook — BSUID only
Pass criteria: the status is matched to your sent message by wamid (id), not by recipient phone; recipient_user_id is used when recipient_id is absent.
3c. Failed status — no contacts array
contacts arrayPass criteria: your handler tolerates a failed status that has no contacts array; the error is logged against the correct message via wamid.
For comparison, keep a phone-present copy of 3a/3b on hand (same payloads with
wa_id/from/recipient_idadded back) and confirm both variants flow through the same code path. See §11.
Force the BSUID-only path in your own pipeline
Fixtures prove your webhook parser; this proves the rest of your system (routing, CRM lookup, replies) degrades gracefully when the phone is gone.
Pick one:
Strip-and-replay: take a copy of real production traffic and remove
wa_id,from, andrecipient_idbefore it reaches your ingestion.Feature flag: add a staging-only switch that nulls the phone on ingestion.
Then run a normal end-to-end flow (receive → identify → reply).
Pass criteria: the conversation still routes, the customer is still identified (by BSUID), and an outbound reply is sent via recipient — with no phone number anywhere in the flow.
Error-path drills
Trigger each deliberately and confirm graceful handling.
131062 — BSUID not supported
Send a one-tap / zero-tap / copy-code authentication template to a recipient (BSUID).
Error caught; you fall back to recovering the phone (Stage 6) before retrying auth.
131047 — window closed
Send a free-form (non-template) message to a BSUID whose 24h window is closed.
Error caught; you switch to a template instead of retrying free-form.
Failed w/o contacts
Replay fixture §3c.
No crash on missing contacts; error logged by wamid.
Stage the phone-recovery flow (REQUEST_CONTACT_INFO)
REQUEST_CONTACT_INFO)⚠️ Build, but don't depend on it yet. Meta now lists this as starting early July 2026.
Steps
Create the utility template (the
textfield is optional and the button label cannot be customized; if you include it, any value other thanShare Contact Infois rejected with subcode2388153):
Validate your handler for the on-tap response by replaying this fixture:
Pass criteria: you read the shared number from messages[0].contacts[0].phones[0], normalize on phones[0].wa_id (digits only, no +/spaces), and map it to the existing BSUID record. Handle both origin: "contact_request" (button tap, no vCard) and origin: "other" (manual share, vCard included).
Username & business username plumbing
User username (display only): confirm you store contacts[0].profile.username as a changeable display string and never use it as a key (see §3a).
Business username (your number's handle):
List suggestions:
GET /username_suggestionsClaim:
POST /usernamewith{ "username": "yourbrand" }Verify:
GET /username→{ "username": "...", "status": "..." }Subscribe each app to the
business_username_updatewebhook and confirm you receiveapproved/reserved/deletedevents.
Pass criteria: you can claim/verify a reserved handle and react to business_username_update. Remember: a business username does not hide your phone number.
Data-model & migration checks
Review (code review, not runtime) that:
The consumer phone number is nullable everywhere — schema, validation, UI.
A BSUID ↔ phone mapping table exists; phone is treated as enrichment, not identity.
BSUID is keyed per portfolio; you never compare or dedupe BSUIDs across clients.
Identity routing is by field name (
wa_id= phone,user_id= BSUID) — no generic E.164 validation on identity fields.You tolerate unknown/new fields (
username,parent_user_id,recipient_parent_user_id,from_user_id).If you're a multi-portfolio managed business, you've evaluated parent BSUID (
parent_user_id, format includesENT) enrollment with your 360dialog contact.
Pass criteria: all boxes hold under review.
Deploy early and watch real traffic
Because phone numbers remain in webhooks through the entire pre-GA window, you can validate in production with zero risk:
Deploy your BSUID-storing, phone-nullable code to production now.
Watch real traffic for several weeks — confirm BSUIDs are captured on every conversation and that sends correlate by
wamid.By GA, the only change is fewer phone fields — a path you already proved with the fixtures in §3.
Pass criteria: production shows BSUIDs persisted on 100% of recent conversations, with no parsing errors.
Self-certification checklist
Sign off each line before GA. Every item maps to a stage above.
Capture & send
Phone-withheld contract
Errors
Recovery & usernames
Data model
Production
Fixture appendix — phone-present variants
Keep these alongside the BSUID-only fixtures and confirm both flow through the same code path.
Inbound — phone present:
Status — phone present:
Need help?
Bespoke or high-complexity integration? Ask your 360dialog account manager about a deep-dive review session.
Found a discrepancy between this guide and live behavior? Report it — pre-GA behavior is still being confirmed with Meta engineering.
Last updated
Was this helpful?