diff --git a/skills/waba-embedded-signup/SKILL.md b/skills/waba-embedded-signup/SKILL.md index 09dc218..af0e94b 100644 --- a/skills/waba-embedded-signup/SKILL.md +++ b/skills/waba-embedded-signup/SKILL.md @@ -8,7 +8,7 @@ Verified against Sent sources: - https://docs.sent.dm/start/quickstart/dashboard-walkthrough - https://docs.sent.dm/start/quickstart/channel-setup - https://docs.sent.dm/reference/api -- Sent v3 OpenAPI: /v3/profiles, /v3/profiles/{profileId}, /v3/profiles/{profileId}/complete, /v3/webhooks, /v3/webhooks/{id}/test, /v3/webhooks/{id}/events, /v3/webhooks/{id}/rotate-secret +- Sent v3 OpenAPI: /v3/profiles, /v3/profiles/{profileId}, /v3/profiles/{profileId}/complete, /v3/webhooks, /v3/webhooks/{id}/test, /v3/webhooks/{id}/rotate-secret Review notes: - Sent docs verify Sender Profiles and WhatsApp configuration status in the dashboard, but the extracted Sent docs/API did not expose a public Sent-specific Embedded Signup endpoint. @@ -86,7 +86,7 @@ Use `/v3/profiles/{profileId}/complete` when prerequisites are ready and API com ### 6. Verify webhook readiness -Use Sent webhook endpoints to confirm event delivery. Verify the webhook exists, the relevant event types are available, and a test event reaches the customer endpoint. Inspect `/v3/webhooks/{id}/events` when customer logs and Sent state disagree. +Use Sent webhook endpoints to confirm event delivery. Verify the webhook exists, the relevant event types are available, and a test event reaches the customer endpoint via `POST /v3/webhooks/{id}/test`. Rotate webhook secrets only when needed and coordinate deployment, because secret rotation invalidates the old secret immediately. @@ -164,7 +164,7 @@ See the top-level `references/sent-glossary.md` for shared Sent terminology. ## Unverified claims to confirm or remove -- A public Sent Embedded Signup endpoint was not verified in the extracted Sent docs/API. -- Required Meta app type, Tech Provider/Solution Partner status, granular scopes, and Graph endpoint sequence are external Meta claims, not Sent API facts in this pass. +- Sent does not expose a public Embedded Signup endpoint; WhatsApp connection is dashboard-only via Channels → WhatsApp (confirmed against Sent v3 docs snapshot, 2026-05-19; the Channels page is explicitly listed as "dashboard config; not directly in v3 API"). +- Required Meta app type, Tech Provider/Solution Partner status, granular scopes, and Graph endpoint sequence are external Meta claims, not Sent API facts. - Mandatory direct phone-number registration or WABA subscription by the customer app depends on integration path and was not verified as a universal Sent requirement. -- Exact Sent webhook payload fields for WhatsApp sender events were not verified; use account event types and observed payloads. +- Sent's webhook envelope is confirmed as `{field, sub_type, timestamp, payload}` with sub-types of the form `.` (e.g., `message.delivered`). WhatsApp-specific sub-types are not enumerated in the snapshot — discover them empirically against your account. diff --git a/skills/waba-embedded-signup/references/waba-embedded-signup-spec.md b/skills/waba-embedded-signup/references/waba-embedded-signup-spec.md index 0923dbd..a9a04fe 100644 --- a/skills/waba-embedded-signup/references/waba-embedded-signup-spec.md +++ b/skills/waba-embedded-signup/references/waba-embedded-signup-spec.md @@ -1,244 +1,91 @@ -# WABA Embedded Signup — Implementation Reference - -Supporting reference for `waba-embedded-signup`. Captures the exact field names, example payloads, and Graph API endpoints used in a current (v23.0) integration. Authoritative source: [Embedded Signup](https://developers.facebook.com/docs/whatsapp/embedded-signup). - -> Meta bumps the Graph API version regularly. The examples below pin v23.0 because it's a current stable as of mid-2026 and gives a comfortable margin before its sunset window. Bump as Meta releases newer stable versions — the field names below have been stable since v20.0. - -## Prerequisites Checklist - -- [ ] Tech Provider or Solution Partner approval from Meta -- [ ] Meta App with products: **WhatsApp**, **Facebook Login for Business** -- [ ] FBL Configuration created in App Dashboard with the WhatsApp Embedded Signup feature selected -- [ ] `config_id` captured from the FBL config (string, treat as semi-public) -- [ ] Redirect URIs allowlisted in the FBL config for every environment -- [ ] App secret stored server-side; never expose to the browser -- [ ] System User in your Meta Business with admin role on the app -- [ ] Webhook URL configured under the WhatsApp product with a verify token -- [ ] App signing secret stored server-side, used to verify `X-Hub-Signature-256` - -## Frontend — Launch the Dialog + Listen for `WA_EMBEDDED_SIGNUP` - -The current SDK flow returns `waba_id` and `phone_number_id` via a `postMessage` event in addition to the OAuth code. Install the listener **before** `FB.login()` or you'll miss it. - -```html - - - - -``` + -**Required `extras`:** `feature: 'whatsapp_embedded_signup'`. Without it the dialog runs the legacy FBL flow and you'll get back a user token without WABA scopes, and no `WA_EMBEDDED_SIGNUP` event. +# WABA Embedded Signup — Implementation Reference -`extras.version: 3` opts into the current signup UI. Older `config_id`s may not honor it; fall back to `debug_token` extraction in that case. +Supporting reference for `waba-embedded-signup`. The Sent v3 docs snapshot (2026-05-19) confirms that **Sent does not expose a public Embedded Signup API endpoint**. The customer-facing surface for connecting WhatsApp to Sent is the **dashboard's Channels → WhatsApp tab**, which is explicitly listed in Sent's "Dashboard pages → API endpoints map" as `(dashboard config; not directly in v3 API)`. The dashboard internally initiates Meta's Facebook Login for Business / Embedded Signup flow on the tenant's behalf. -**Optional `extras`:** `setup` for pre-fill (business name, email). Useful for existing tenants who already have data on file. +What this means for an integrator: -## Backend — Step 5: Exchange Code for Token +- **You do not call a Sent endpoint to start Embedded Signup.** You direct the tenant to their Sent dashboard. +- The Meta-side authentication, token exchange, WABA discovery, phone-number registration, app subscription, and app review state are owned by **Meta** and abstracted by the Sent dashboard. They are not surfaced as Sent API operations. +- After dashboard completion, the WhatsApp wiring is bound to the tenant's Sender Profile and routable via Sent's normal v3 API (`POST /v3/messages`, etc.). -```http -GET https://graph.facebook.com/v23.0/oauth/access_token - ?client_id={APP_ID} - &client_secret={APP_SECRET} - &redirect_uri={ALLOWLISTED_URI} - &code={CODE_FROM_FRONTEND} -``` - -Response: -```json -{ - "access_token": "EAAB...", - "token_type": "bearer", - "expires_in": 5184000 -} -``` +Anything below this line is **external Meta documentation context** — included only so an operator debugging a stuck dashboard flow knows what is happening behind the scenes. Authoritative source: Meta — [Embedded Signup](https://developers.facebook.com/docs/whatsapp/embedded-signup), [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api), [Facebook Login for Business](https://developers.facebook.com/docs/facebook-login/facebook-login-for-business). Meta bumps the Graph API version regularly — always check the live Meta docs for the current version, scope names, and field names. -This is a user token. For production, exchange to a System User token (long-lived) before persisting. +## Sent-side surface (what the API does and doesn't expose) -## Backend — Step 5b: `debug_token` (fallback only) - -If the `WA_EMBEDDED_SIGNUP` event never fired (older `config_id`, blocked listener, non-default flow), you can still recover the IDs by introspecting the token: +| Concern | Where it lives | +|---|---| +| Start Embedded Signup | Dashboard → Channels → WhatsApp → "Connect" (no public Sent API) | +| WABA / phone-number binding | Dashboard (not in v3 API) | +| Mark profile setup complete | `POST /v3/profiles/{id}/complete` (idempotent, sensitive endpoint — 10/min, burst 5) | +| Profile status after binding | `GET /v3/profiles/{id}` → `status` ∈ `incomplete` \| `pending_review` \| `approved` \| `rejected` | +| Webhook config | `POST /v3/webhooks`, `PUT /v3/webhooks/{id}`, `POST /v3/webhooks/{id}/test`, `POST /v3/webhooks/{id}/rotate-secret` (sensitive — 10/min, burst 5) | +| Auth header | `x-api-key: ` — single header, account-scoped. No `x-sender-id` in v3. | -```http -GET https://graph.facebook.com/debug_token - ?input_token={USER_TOKEN} - &access_token={APP_ID}|{APP_SECRET} -``` +## Customer-facing dashboard flow (what the tenant sees) -Response (abbreviated): -```json -{ - "data": { - "app_id": "...", - "type": "USER", - "expires_at": 1700000000, - "is_valid": true, - "scopes": [ - "whatsapp_business_management", - "whatsapp_business_messaging", - "business_management" - ], - "granular_scopes": [ - { "scope": "whatsapp_business_management", "target_ids": ["WABA_ID_1"] }, - { "scope": "whatsapp_business_messaging", "target_ids": ["WABA_ID_1"] } - ], - "user_id": "..." - } -} -``` +This mirrors the live flow on the dashboard's Channels page; it is what a tenant should be guided through, not an API sequence: -`granular_scopes` is the authoritative source of which WABA(s) the user granted access to. Cross-check it; don't trust the token alone. +1. Dashboard → **Channels** → **WhatsApp** tab → click **Connect**. +2. Meta consent popup opens (Facebook Login for Business surface, initiated by Sent). +3. Tenant selects (or creates) a **WABA** under their Meta Business Portfolio. +4. Tenant grants Sent permission to **manage WhatsApp messages and templates** on that WABA. +5. Tenant adds a **Meta payment method** (separate from Sent billing — Meta charges per-conversation independently). +6. Dashboard reflects channel setup completion; the WhatsApp wiring is bound to the tenant's Sender Profile. +7. API credentials (the `x-api-key`) can be copied from the post-setup screen or retrieved later from the dashboard's API Keys page. -To enumerate owned WABAs: -```http -GET https://graph.facebook.com/v23.0/{business_id}/owned_whatsapp_business_accounts -``` +The runbook (`waba-onboarding-runbook.md`) walks through this end-to-end with failure modes and recovery steps. -## Backend — Phone Numbers +## Webhook envelope (Sent-confirmed) -```http -GET https://graph.facebook.com/v23.0/{WABA_ID}?fields=id,name,owner_business_info -``` +After WhatsApp is connected, Sent emits webhooks for that profile's messages using the universal envelope: -```http -GET https://graph.facebook.com/v23.0/{WABA_ID}/phone_numbers -``` - -Phone-numbers response: ```json { - "data": [ - { - "id": "PHONE_NUMBER_ID", - "display_phone_number": "+1 555-555-0123", - "verified_name": "Sent Demo", - "code_verification_status": "VERIFIED", - "quality_rating": "GREEN" - } - ] -} -``` - -## Backend — Step 6: Register the Phone Number - -```http -POST https://graph.facebook.com/v23.0/{PHONE_NUMBER_ID}/register -Content-Type: application/json -Authorization: Bearer {SYSTEM_USER_TOKEN} - -{ - "messaging_product": "whatsapp", - "pin": "123456" + "field": "message", + "sub_type": "message.delivered", + "timestamp": "2026-01-15T10:35:00+00:00", + "payload": { + "account_id": "", + "message_id": "", + "message_status": "DELIVERED", + "channel": "whatsapp", + "inbound_number": "+1...", + "outbound_number": "+1...", + "template_id": "" + } } ``` -The `pin` is a 6-digit numeric PIN. Generate one per phone number, store it encrypted on the WhatsApp sender record, and reuse for any future re-registration. - -Response: `{ "success": true }`. - -## Backend — Step 7: Subscribe Your App to the WABA - -```http -POST https://graph.facebook.com/v23.0/{WABA_ID}/subscribed_apps -Authorization: Bearer {SYSTEM_USER_TOKEN} -``` - -Response: `{ "success": true }`. +Sub-types follow `.` (`message.queued`, `message.routed`, `message.sent`, `message.delivered`, `message.failed`, and on WhatsApp/RCS only, `message.read`). -## Backend — Step 8: Verify by Reading Back +WhatsApp-specific sub-types beyond the universal `message.*` family (e.g., template approval/rejection notifications) are not enumerated in the v3 snapshot. To discover what your account currently subscribes to: -```http -GET https://graph.facebook.com/v23.0/{WABA_ID}/subscribed_apps -``` +1. List configured webhooks: `GET /v3/webhooks`. +2. Inspect a single webhook's `event_types` and `event_filters` fields. +3. Subscribe broadly to the `message` parent type and observe what arrives in production — fold the observed sub-types into your routing. -Response should include your app in `data[]`. If not, webhooks will never fire for this WABA. +## Webhook signature verification -## Webhook Verification (one-time, for the URL itself) +The webhook model (verified) exposes `signing_secret` as a per-webhook field; the exact HMAC algorithm and header name are not specified in the snapshot. Rotate via `POST /v3/webhooks/{id}/rotate-secret` — the old secret is invalidated immediately, so coordinate with the receiver before rotating. -Meta calls your webhook URL with a verification request: -``` -GET /webhook?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=12345 -``` -Respond with the challenge value as the body when `verify_token` matches. +## Meta-side context (for operators only — link, do not reimplement) -## Webhook Signature Verification (every request) +When a dashboard tenant is stuck and you need to know what the dashboard is doing on their behalf, the underlying Meta flow looks like this — read Meta's docs for current details: -Header: -``` -X-Hub-Signature-256: sha256= -``` +- Meta app type, Tech Provider / Solution Partner status, granular scopes, Graph version, redirect URI allowlisting → [Embedded Signup docs](https://developers.facebook.com/docs/whatsapp/embedded-signup). +- OAuth code → System User token exchange → [Facebook Login for Business](https://developers.facebook.com/docs/facebook-login/facebook-login-for-business). +- WABA / phone-number lookup, phone-number registration with PIN, app subscription to WABA → [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api). +- App review state, business verification, payment method, quality rating → Meta Business Suite UI. -Compute `HMAC-SHA256(request.raw_body, APP_SECRET)` and compare in constant time. **Reject the request if the signature doesn't match.** This is the only thing preventing spoofed webhooks. +If a tenant is genuinely operating their own Meta app (not using the Sent-managed dashboard flow), they own all of the above and should be referred to Meta's docs directly. Sent's API does not replace that. -## State Transitions for the WhatsApp Sender +## What is not in the v3 docs snapshot -After each Graph API call, persist the WhatsApp sender's state on its Sender Profile: +- The exact shape of the request body for `POST /v3/profiles/{id}/complete` for WhatsApp wiring (the snapshot confirms the endpoint exists and is sensitive; the per-channel payload is not published). +- The webhook signature algorithm / header used to verify Sent → receiver deliveries. +- The mapping shape between Sender Profile and the WABA / phone-number IDs the dashboard binds to it. +- WhatsApp-specific webhook sub-types (e.g., template lifecycle events). -| Step | New state | -|---|---| -| `WA_EMBEDDED_SIGNUP FINISH` received, code received | `connecting` | -| Token exchanged + IDs resolved | `connected` | -| Phone number registered | `registered` | -| App subscribed to WABA + verified read-back | `active` | -| Any of the above fails | `failed` (with `error_code` and `error_message`) | - -Never declare `active` until Step 8 read-back succeeds. Otherwise the tenant sees "Connected!" but no webhooks arrive. - -## Common Error Responses (and what to do) - -| HTTP / code | Body excerpt | Action | -|---|---|---| -| 400 / `code: 100` | `Invalid OAuth access token` | Token expired or revoked; ask tenant to reconnect | -| 400 / `code: 190` | `OAuthException` | Same as 100 | -| 403 / `code: 200` | `Permissions error` | Granular scope missing; re-launch with the right `config_id` | -| 500 | Empty body | Meta transient; retry with exponential backoff | -| 4XX on `/register` | `code: 133005` or `133006` | PIN wrong, or phone number not eligible — surface to tenant | +Treat each of these as "discover via your account" rather than "code to a spec". diff --git a/skills/waba-embedded-signup/references/waba-onboarding-runbook.md b/skills/waba-embedded-signup/references/waba-onboarding-runbook.md index 5aa1204..63d6e7a 100644 --- a/skills/waba-embedded-signup/references/waba-onboarding-runbook.md +++ b/skills/waba-embedded-signup/references/waba-onboarding-runbook.md @@ -1,142 +1,122 @@ -# WABA Onboarding Runbook + -Operator-facing companion to `waba-embedded-signup`. Walks the flow in time order and tells you, for each step, what success looks like, the failure modes you should expect, and how to recover **without** restarting the whole signup. +# WABA Onboarding Runbook — Dashboard Flow -Use this when shepherding a real tenant through Embedded Signup on Sent, or when a stuck WhatsApp Sender Profile lands on your desk and you need to pinpoint where it broke. +Operator-facing companion to `waba-embedded-signup`. Walks the **Sent dashboard** WhatsApp connection flow in time order and tells you, for each step, what success looks like, the failure modes you should expect, and how to recover **without** restarting the whole signup. -## 0. Pre-flight (do once per Meta app, not per tenant) +The v3 snapshot confirms there is **no public Sent Embedded Signup API endpoint** — the Channels page in the dashboard is the surface, and it initiates Meta's Facebook Login for Business flow internally. This runbook reflects that reality; for the broader skill workflow and integration-path decision, see `waba-embedded-signup` SKILL.md. -Verify all of these in [Meta Business Suite](https://business.facebook.com) and the App Dashboard before *any* tenant sees the button: +## 0. Pre-flight (per-tenant gates) -- [ ] **Tech Provider status approved.** Business Settings → Requests → Tech Provider tab shows "Approved". If it shows "Pending" or "More info needed", finish that first — signup will technically run without it, but you cannot bill or manage tenant WABAs on their behalf. -- [ ] **App is Live** (not "In Development") in App Dashboard → App Review. In Development apps only signal-test with admin/dev/tester accounts. -- [ ] **Required products added:** WhatsApp + Facebook Login for Business. -- [ ] **Permissions approved in App Review:** `whatsapp_business_management`, `whatsapp_business_messaging`, `business_management`. -- [ ] **System User created** in Business Settings → Users → System Users. Role: Admin. Generate a never-expiring token scoped to the app and store in your secrets manager. -- [ ] **FBL Configuration** with the WhatsApp Embedded Signup feature selected, `config_id` captured. -- [ ] **Redirect URIs allowlisted** for every environment that launches the dialog. -- [ ] **Webhook URL + verify token** configured on the WhatsApp product. URL must respond to Meta's GET verification handshake before save will succeed. +Before the "Continue Channel Setup" button is meaningful for a tenant, two account-level gates must be true: -**Success looks like:** A green Tech Provider badge, app status "Live", a System User token that introspects via `debug_token` with all three WhatsApp scopes. +- [ ] **KYC approved.** Per the verified onboarding state machine, the account must have reached `KYC_COMPLETED` (state 5+). Before that, the v3 API returns `AUTH_006` and the dashboard blocks the Channels page. Compliance form fields (business identity, use cases, opt-in evidence) come from the dashboard's KYC + compliance pages. +- [ ] **Meta Business Portfolio ready.** The tenant must already have (or create during the flow) a Meta Business Portfolio under which a WABA will be selected or created. Sent does not provision this on the tenant's behalf. -**Common failure:** Tech Provider in "More info needed" because the legal entity uploaded doesn't match what's on your Meta Business. Re-upload with the exact registered business name and EIN — Meta matches strings, not entities. +If KYC is still in `KYC_STARTED`, `WHITELISTED`, `ONBOARDING_STARTED`, or `KYC_RESUBMISSION_REQUESTED`, finish that first. The dashboard's onboarding checklist surfaces the next required step. -**Recovery without restarting:** None — pre-flight blocks everything. If you discover a gap mid-tenant-flow, pause signup at the UI level and finish pre-flight before re-launching. +## 1. Click "Continue Channel Setup" in the dashboard -## 1. Launch the Embedded Signup dialog (frontend) +After KYC, the dashboard surfaces a **Continue Channel Setup** entry that lands on the **Channels** page. -The tenant clicks "Connect WhatsApp". Your code calls `FB.login()` with the `config_id` and `extras: { feature: 'whatsapp_embedded_signup', version: 3 }`. - -**Success looks like:** The Meta dialog opens to "Continue as " and lists the assets the tenant will grant. The `message` listener (installed *before* `FB.login`) fires with `type: 'WA_EMBEDDED_SIGNUP'` and `event: 'FINISH'` containing `{ phone_number_id, waba_id, business_id }`. +**Success looks like:** The Channels page loads and shows a **WhatsApp** tab with a **Connect** action. **Common failure modes:** -- Dialog opens then closes immediately → `redirect_uri` not allowlisted or `config_id` belongs to a different app. -- No `WA_EMBEDDED_SIGNUP` event ever fires → listener installed *after* `FB.login` (race), or the `config_id` predates the embedded-signup feature. -- Tenant chose "Create a new WhatsApp Business Account" inside the dialog and got stuck on business verification → not your bug; surface a clear "finish business verification in Meta Business Suite" link and resume later. +- Button is missing / disabled → account state hasn't reached `KYC_COMPLETED`. Finish KYC first. +- API returns `AUTH_007` against `/v3/messages` for a tenant who thinks they're set up → they're at `KYC_COMPLETED` or `MESSAGE_COMPLIANCE_COMPLETED` but haven't completed channel setup. They need to land on this page. -**Recovery without restarting:** Capture the auth code regardless. If `sessionInfo` is missing, fall back to Step 5b (`debug_token`) — you can still recover `waba_id` from `granular_scopes` + `/owned_whatsapp_business_accounts`. +**Recovery without restarting:** Re-check `GET /v3/me` or the dashboard's onboarding indicator. Channel setup itself has no API; route the tenant back to the dashboard. -## 2. Backend OAuth code → token exchange +## 2. Select the phone number -`POST /signup/whatsapp/callback` arrives with `{ code, session }`. Exchange the code at `GET /oauth/access_token` to receive a short-lived user token, then **immediately** swap it for a System User token so it survives the tenant changing their password. +In the Channels → WhatsApp flow, the tenant selects the phone number that will be used for the WABA's first sender. Sent docs note that **this selection is not easy to change later** — once a phone number is bound to a Sender Profile, swapping it requires Meta-side migration plus a dashboard re-bind. -**Success looks like:** `debug_token` on the resulting token shows `is_valid: true`, `expires_at: 0` (never), and `granular_scopes` containing the WABA the tenant granted. +**Success looks like:** The phone number is captured and the flow advances to Meta login. The number should be an E.164 line the tenant controls, not currently registered to another WABA they care about. **Common failure modes:** -- `OAuthException 100` → wrong `client_secret`, or `redirect_uri` doesn't *exactly* match what was used to mint the code. -- `expires_in: 5184000` instead of never-expiring → you forgot the System User swap and persisted the user token. -- `granular_scopes` empty → tenant unchecked WhatsApp permissions in the dialog. +- Tenant picks a number that's already on a WABA they intend to keep separate → after Meta consent they'll discover the number is "in use elsewhere" and have to detach in WhatsApp Manager. +- Tenant picks a personal line they later want back for WhatsApp Consumer → that's a one-way door; warn upfront. + +**Recovery without restarting:** Within the same flow you can usually back out and pick a different number. After completion, switching numbers requires Meta-side migration and a fresh dashboard binding. -**Recovery without restarting:** Token-exchange failures are recoverable in place — fix secrets / redirect URI and replay the same `code` (codes are valid for 1 hour and one use). If the code is already consumed, re-launch the dialog from Step 1; the tenant only re-confirms permissions. +## 3. Log in with Facebook/Meta and grant Sent permission -## 3. Phone number registration with Meta +The dashboard launches Meta's Embedded Signup popup (Facebook Login for Business). The tenant: -`POST /{phone_number_id}/register` with `{ messaging_product: 'whatsapp', pin }`. The PIN is a 6-digit code you generate; persist it encrypted on the WhatsApp sender record so re-registration is possible later. +- Logs in with their Meta account that admins the Business Portfolio. +- Selects (or creates) the **WABA** to bind. +- Grants Sent permission to **manage messages and templates** on that WABA. -**Success looks like:** `{ "success": true }`. A follow-up `GET /{phone_number_id}` shows `code_verification_status: VERIFIED`. +**Success looks like:** The popup closes with success; the dashboard reflects the connected WABA name. **Common failure modes:** -- `133005` (PIN mismatch) → previously registered with a different PIN. Reset in WhatsApp Manager → Phone Numbers → ⋮ → Two-step verification, then retry. -- `133006` (number not eligible) → number is on hold (quality drop) or owned by a different WABA. Check WhatsApp Manager. -- Silent 200 with no register response → wrong endpoint version. Confirm you are on the same Graph API version as the rest of the flow. +- Popup closes immediately → ad-blocker or popup-blocker. Disable for the Sent dashboard origin. +- Tenant chose "Create a new WhatsApp Business Account" inside the dialog and got stuck on business verification → not a Sent issue; tenant must finish verification in Meta Business Suite, then return. +- Tenant unchecked WhatsApp permissions in the consent screen → the binding will fail or be unusable. Re-launch Connect and accept all required permissions. -**Recovery without restarting:** All register errors are step-local. Fix the PIN / number eligibility and re-POST `/register`. Do *not* re-launch the dialog. +**Recovery without restarting:** Re-launch Connect from the dashboard. The tenant only re-confirms permissions; previously-captured fields (like the chosen phone number) typically persist. -## 4. WABA subscription +## 4. Add Meta payment method -`POST /{waba_id}/subscribed_apps`. This subscribes **your app** to **this WABA's** webhook firehose. App-level webhook URL config is necessary but not sufficient — without per-WABA subscription, no events flow. +Meta charges per-conversation for WhatsApp Business messaging, separately from Sent's billing. The tenant must add a payment method to the WABA in WhatsApp Manager / Meta Business Suite. -**Success looks like:** `{ "success": true }`, then `GET /{waba_id}/subscribed_apps` returns `data[]` containing your `{ whatsapp_business_api_data: { id: } }`. +**Success looks like:** Payment method status is "Active" in WhatsApp Manager. Sent's dashboard may surface a "Meta payment required" warning until this is true. **Common failure modes:** -- 200 but read-back is empty → token used for the POST didn't have `whatsapp_business_management` on this WABA. Use the System User token, not a user token. -- 403 on POST → app not approved for WhatsApp permissions in App Review. - -**Recovery without restarting:** Always do the read-back. If subscription is missing, fix the token / app review and re-POST. Never mark the sender `active` based only on the POST response. +- Card declines → tenant retries with a different card in Meta Business Suite. +- Tenant conflates this with Sent billing → clarify: Sent bills Sent fees; Meta bills WhatsApp conversation fees directly to the WABA. -## 5. Mapping into a Sent Sender Profile +**Recovery without restarting:** Meta payment is set on the WABA, independent of the Sent flow — the tenant can complete this without re-doing steps 1–3. -Persist the new WhatsApp half on the tenant's [Sender Profile](https://docs.sent.dm) by calling Sent's profile-completion endpoint: +## 5. Confirm channel setup completion in dashboard -``` -POST /v3/profiles/{profile_id}/complete -{ - "channel": "whatsapp", - "waba_id": "...", - "phone_number_id": "...", - "business_id": "...", - "access_token_ref": "vault://wa/tokens/", - "pin_ref": "vault://wa/pins/" -} -``` +After the WABA binding and Meta payment are in place, the dashboard reflects channel setup as complete. Internally, the account state should advance to `MESSAGE_COMPLIANCE_COMPLETED` and then to activated. The API surface that signals "I am done" is `POST /v3/profiles/{id}/complete` (idempotent, sensitive — 10/min, burst 5). Inspect `GET /v3/profiles/{id}` and look for `status` ∈ `pending_review` → `approved`. -Store tokens and PINs by *reference* (vault path), never by value, in the Sender Profile record. - -**Success looks like:** Sender Profile `channels.whatsapp.state` transitions to `active`. The Sent dashboard shows the verified business name and `display_phone_number`. +**Success looks like:** Profile `status` reaches `approved`; the dashboard shows the WhatsApp channel as connected. **Common failure modes:** -- Profile already had a WhatsApp half on a different WABA → reject the completion request; force tenant to either detach the old WABA or create a new profile. -- Token-ref persisted but no `access_token` actually written to the vault → broken vault wiring on your side. The profile will show "Connected" and silently fail on first send. - -**Recovery without restarting:** Profile completion is idempotent on `(profile_id, channel)` — re-POST with corrected fields. +- Profile stays `pending_review` → Sent-side review is still running. Surface the status to the tenant; do not retry `complete` in a loop (rate-limited). +- Profile lands at `rejected` → KYC or compliance evidence was insufficient; the dashboard explains the reason. Fix in KYC + re-run. +- API returns `AUTH_005` against sends → the account state is at step 6 waiting for final Sent-side activation. No tenant action needed; wait. -## 6. Webhook subscription verification +**Recovery without restarting:** `POST /v3/profiles/{id}/complete` is idempotent — calling again with the same input is safe. Do not delete and re-create the profile to "reset" status. -Send a synthetic event through the loop to prove the wiring works end-to-end: - -1. Mark the new WhatsApp number as your own (admin-side) and from a personal WhatsApp, message it. -2. Expect Sent to receive a `messages` webhook from Meta and re-emit it on the tenant's MDR stream. - -**Success looks like:** A Sent MDR with `channel: whatsapp`, `direction: inbound`, and a `wamid` that matches what your personal device sees in the chat. - -**Common failure modes:** -- No webhook → Step 4 (per-WABA subscription) wasn't actually verified. -- Webhook arrives at Meta-side but not at Sent → app-level webhook URL is wrong, or the signature check is rejecting requests. Check Sent's webhook-ingest logs. -- Webhook arrives at Sent but not on the tenant's MDR → Sender Profile mapping in Step 5 didn't actually persist `phone_number_id` (routing key). +## 6. Copy API credentials -**Recovery without restarting:** All are repair-in-place. Re-verify subscription read-back; check the app-level webhook URL; re-POST `/complete` with the correct `phone_number_id`. +Once `status = approved`, API credentials are available: -## 7. First test send +- On the post-setup screen, or +- Anytime from the dashboard's **API Keys** page (the snapshot lists this as `(dashboard-only; not in v3 API spec)` — there is no API to mint or list keys). -`POST` an approved template (the simplest utility template you have) to the phone number you control. +Auth in v3 is a single header: `x-api-key: `. There is no `x-sender-id` in v3 — that's v2 legacy. The key is account-scoped. -**Success looks like:** `200` with a `messages[0].id` (the outbound `wamid`); within seconds, a `sent` → `delivered` → `read` sequence on the MDR stream. +**Success looks like:** A test request to `GET /v3/me` with the key returns 200. **Common failure modes:** -- `131047` (re-engagement required) → expected; means you're outside the 24-hour window and used a non-template message. Use a template. -- `132000` (template not approved) → tenant hasn't authored or had a template approved yet. Direct them to the template builder. -- Outbound succeeds, no MDR follow-up → Step 6 (webhook end-to-end) wasn't actually verified. +- `AUTH_001` (401, missing header) → header name wrong; must be `x-api-key`. +- `AUTH_002` (401, invalid key) → key was rotated or copied with whitespace. +- `AUTH_007` (403, no channel configured) → key is valid but the account is at `KYC_COMPLETED` / `MESSAGE_COMPLIANCE_COMPLETED` without a finished channel. Re-check step 5. +- `AUTH_005` (403, pending final activation) → wait for Sent activation; not a credential problem. -**Recovery without restarting:** Send errors are tenant-content issues, not signup issues. The Sender Profile is `active` — leave it that way and surface a clear next-step ("approve a template") in the tenant UI. +**Recovery without restarting:** Re-copy the key from the dashboard. Treat the key as a secret — never log it. Use the sandbox mode (`"sandbox": true` in mutation request bodies) for integration tests so you don't burn budget. ## Stuck-state triage cheat-sheet | Symptom in production | Step | First thing to check | |---|---|---| -| "Connect WhatsApp" does nothing | 1 | Browser console for FBL init errors; `config_id` matches app | -| Dialog closes immediately | 1 | Redirect URI allowlist for *this* environment | -| Backend got code, missing `waba_id` | 1–2 | Listener race; fall back to `debug_token` | -| Token works, register fails 133005 | 3 | PIN was set elsewhere; reset in WhatsApp Manager | -| Subscribed but no webhooks | 4–6 | Read back `/subscribed_apps`; verify app-level webhook URL | -| Sender Profile `active` but no MDR | 6 | `phone_number_id` mapping in Sent | -| Test send 132000 | 7 | Not a signup bug; tenant needs an approved template | +| "Continue Channel Setup" missing | 0–1 | Account state — finish KYC first | +| Channels page rejects the chosen number | 2 | Number already on another WABA | +| Meta popup closes immediately | 3 | Popup/ad-blocker on dashboard origin | +| Popup completes but dashboard shows "not connected" | 3 | Tenant unchecked permissions; re-launch Connect | +| Dashboard shows "Meta payment required" | 4 | Add payment in WhatsApp Manager | +| Profile stuck `pending_review` | 5 | Sent-side review; do not re-POST `complete` in a loop | +| API send returns `AUTH_007` | 5 | Channel setup not actually complete | +| API send returns `AUTH_005` | 5 | Final Sent activation pending; no action | +| `x-api-key` returns `AUTH_002` | 6 | Re-copy from dashboard; check whitespace | + +## What this runbook deliberately does not cover + +- Customer apps that own their **own** Meta App and run Embedded Signup themselves (rather than using the Sent-managed dashboard flow). That path is owned by Meta — see `waba-embedded-signup-spec.md` and Meta's [Embedded Signup docs](https://developers.facebook.com/docs/whatsapp/embedded-signup). +- Template authoring and submission — see `sent-skills:waba-template-author`. +- Multi-tenant Sender Profile design — see `sent-skills:sender-profile-architect`. +- Post-connection delivery debugging — see `sent-skills:messaging-performance-analyzer`. diff --git a/skills/waba-embedded-signup/references/whatsapp-sender-profile-mapping.md b/skills/waba-embedded-signup/references/whatsapp-sender-profile-mapping.md index bb19986..fb37b5b 100644 --- a/skills/waba-embedded-signup/references/whatsapp-sender-profile-mapping.md +++ b/skills/waba-embedded-signup/references/whatsapp-sender-profile-mapping.md @@ -1,19 +1,38 @@ + + # WhatsApp ↔ Sent Sender Profile Mapping How Meta-side entities created during Embedded Signup map onto Sent's Sender Profile model. Read this before deciding how many profiles to create per tenant, or when debugging why a webhook landed on the wrong profile. +For the broader multi-channel architecture (one profile owns SMS + WhatsApp + RCS halves; how to split tenants across profiles), see `sent-skills:sender-profile-architect`. + ## The entities **Meta side:** -- **Business Manager** — the tenant's legal/operational umbrella in Meta Business Suite. +- **Business Manager / Business Portfolio** — the tenant's legal/operational umbrella in Meta Business Suite. - **WABA (WhatsApp Business Account)** — owns templates and phone numbers; the unit Meta bills. - **Phone Number** — a single E.164 number registered for Cloud API on a WABA. -- **System User** — the long-lived identity that holds the access token used to call Graph API on behalf of the tenant. +- **System User** — long-lived identity holding the access token used to call Graph API on behalf of the tenant. + +**Sent side (v3 — schema verified against snapshot):** -**Sent side:** -- **Sender Profile** — the tenant's unified sending identity across SMS / WhatsApp / RCS. Owns one API key. (See top-level `references/sent-glossary.md`.) -- **Channel config** — the per-channel block on a profile. The WhatsApp channel config holds `waba_id`, `phone_number_id`, vault-refs for the access token and registration PIN. -- **Webhook routing** — Sent uses `phone_number_id` from inbound Meta payloads to route events back to the right profile's MDR stream. +A Sender Profile is: + +| Field | Type | Notes | +|---|---|---| +| `id` | UUID | The Sent-side primary key. | +| `name` | string | Display name. | +| `icon` | string \| null | URL. | +| `description` | string \| null | | +| `short_name` | string \| null | | +| `role` | `admin` \| `billing` \| `developer` \| null | Caller's role on this profile. | +| `status` | `incomplete` \| `pending_review` \| `approved` \| `rejected` \| null | Setup status. | +| `created_at` | ISO8601 | | +| `settings` | object | `{default_channel, webhook_url, timezone, language}` | + +There is **no public `channels.whatsapp` sub-resource** on the Profile in the v3 docs snapshot. Per-channel WhatsApp wiring (WABA ID, phone-number ID) is performed via the dashboard Channels page, which is explicitly listed as "dashboard config; not directly in v3 API". Treat WABA and phone-number IDs as external provider identifiers that the dashboard binds to the profile; do not invent v3 endpoints to mutate that binding. + +Auth in v3 is a single header — `x-api-key: ` — at the account level. `x-sender-id` is **v2 legacy** and is exposed per profile in the dashboard for routing, not as a v3 API auth requirement. ## ASCII map @@ -22,85 +41,65 @@ Tenant │ ├── Business Manager (1) │ │ - │ ├── WABA #A ────────────────────────► Sender Profile P1 - │ │ ├── Phone +1 555 0100 ◄────────┤ channels.whatsapp - │ │ ├── Phone +1 555 0101 ◄──┐ │ { waba_id: A, - │ │ └── Templates │ │ phone_number_id: ...100, - │ │ │ │ token_ref, pin_ref } + │ ├── WABA #A ────────────────────────► Sender Profile P1 (id, status=approved) + │ │ ├── Phone +1 555 0100 ◄────────┤ (dashboard-bound) + │ │ ├── Phone +1 555 0101 ◄──┐ │ + │ │ └── Templates │ │ │ │ │ │ │ │ └────► Sender Profile P2 - │ │ │ channels.whatsapp - │ │ │ { waba_id: A, - │ │ │ phone_number_id: ...101 } + │ │ │ (different phone, same WABA) │ │ │ └── WABA #B ────────────────────────► Sender Profile P3 - │ └── Phone +44 20 7946 0000 ◄────┤ channels.whatsapp - │ │ { waba_id: B, - │ │ phone_number_id: ... } + │ └── Phone +44 20 7946 0000 ◄────┤ │ └── System User (1) ──► token held in vault, referenced by all of P1/P2/P3 ``` -## Cardinality rules +## Cardinality rules (operational, not enforced by v3 API) | Relationship | Cardinality | Notes | |---|---|---| -| Business Manager → WABA | 1 : N | A tenant may operate multiple WABAs (e.g. per region or brand) under one Business Manager. | +| Business Manager → WABA | 1 : N | A tenant may operate multiple WABAs (per region or brand). | | WABA → Phone Number | 1 : N | Up to 25 per WABA per Meta's current limits. | -| Phone Number → Sender Profile | 1 : 1 | **Hard rule.** Each phone number routes to exactly one profile. Sharing a number across profiles breaks webhook routing. | -| WABA → Sender Profile | 1 : N | Multiple profiles may reference the same WABA, each pinning a different `phone_number_id`. Useful when one tenant runs separate brands off the same WABA. | +| Phone Number → Sender Profile | 1 : 1 | **Hard rule.** Each phone number routes to exactly one profile; sharing breaks inbound routing. | +| WABA → Sender Profile | 1 : N | Multiple profiles may bind to the same WABA, each pinning a different phone number. | | System User → WABA | 1 : N | One System User token can hold scopes for many WABAs. | -| Sender Profile → WhatsApp channel | 0 : 1 | A profile has at most one WhatsApp half. SMS / RCS halves are independent. | +| Sender Profile → WhatsApp wiring | 0 : 1 | A profile has at most one WhatsApp binding. SMS / RCS bindings are independent. | ## What `POST /v3/profiles/{id}/complete` actually does -Profile completion is the Sent-side commit that takes a profile from `draft` to `active` for a given channel. For WhatsApp: +`POST /v3/profiles/{id}/complete` is confirmed in the v3 snapshot as the profile-completion endpoint. It supports `Idempotency-Key` and is classified as a sensitive endpoint (10 req/min, burst 5). It transitions the profile out of `incomplete` once prerequisites are met. -1. Validates that `(waba_id, phone_number_id)` is not already claimed by another profile in this tenant. -2. Persists `phone_number_id` into the profile's routing index — this is what lets Sent's webhook ingest find the right profile when Meta delivers an inbound event. -3. Stores `token_ref` and `pin_ref` as opaque vault references; the values themselves never enter the profile record. -4. Subscribes Sent's app to the WABA (idempotent — safe to re-call). -5. Returns the profile with `channels.whatsapp.state = active`. +The exact request/response shape for the completion call (which fields must be present, what gets persisted) is **not enumerated in the v3 snapshot**. Treat the completion call as a commit: prerequisites (KYC + channel config done via the dashboard) must already be true; the endpoint signals "I am ready". Check the live OpenAPI at [docs.sent.dm](https://docs.sent.dm) before wiring a tenant-facing integration. -Idempotent on `(profile_id, channel)`: calling again with corrected fields updates in place. See [docs.sent.dm](https://docs.sent.dm) for the live request/response schema. +## Routing inbound events back to a profile -## How webhook events flow back +Sent's webhook envelope (verified) is: -``` -Meta Cloud API ──signed POST──► Sent webhook ingest - │ - │ verify X-Hub-Signature-256 - │ extract entry[].changes[].value.metadata.phone_number_id - │ - ▼ - Phone-number-id → Sender Profile lookup - │ - ▼ - Tenant MDR stream + webhook to tenant URL +```json +{ + "field": "message", + "sub_type": "message.delivered", + "timestamp": "2026-01-15T10:35:00+00:00", + "payload": { "account_id": "...", "message_id": "...", "channel": "whatsapp", "inbound_number": "+1...", "outbound_number": "+1...", "template_id": "..." } +} ``` -The routing key is `phone_number_id`, not `waba_id`. This is why the "Phone Number → Sender Profile" relation must stay 1:1 — there's nowhere else in the payload to disambiguate. +For WhatsApp inbound, the payload carries `account_id`, `message_id`, and the inbound/outbound E.164 numbers. WhatsApp-specific sub-types beyond the generic `message.*` family (e.g., template-status events) are not enumerated in the snapshot — discover them empirically against your account by subscribing broadly and observing what arrives. ## Detaching a WABA without losing message history -You will eventually need to remove a WABA from a profile (tenant churns, swaps providers, etc.) **without** wiping the historical MDRs for compliance. +There is no v3 API endpoint documented for detaching a WhatsApp binding from a profile. The Channels page in the dashboard is the surface. Operationally: -1. Set `channels.whatsapp.state = detaching` on the profile (blocks new sends). -2. Wait for in-flight messages to settle (delivery webhooks drain within 24h for normal traffic, 48h for slower carriers). -3. `DELETE /v3/profiles/{id}/channels/whatsapp` — soft-deletes the channel config, keeps MDR history searchable by `wamid` and `phone_number_id`. -4. Unsubscribe Sent's app from the WABA: `DELETE /v23.0/{waba_id}/subscribed_apps` (Graph API). -5. Optionally archive the System User token if no other profile references it. +1. Stop sending on the profile. +2. Wait for in-flight deliveries to settle (delivery webhooks drain within ~24h for normal traffic, longer for slower carriers). +3. Use the dashboard Channels page to remove the WhatsApp binding. +4. On the Meta side, unsubscribe your Tech Provider app from the WABA via Graph API if you held the subscription directly. -The MDR history stays queryable; only the *send* path is removed. Re-attaching later goes through the normal Embedded Signup flow and produces a fresh channel config. +Historical MDRs remain queryable by `message_id` — message history is not deleted when the binding is removed. ## Migrating a phone number between WABAs -Tenants sometimes need to move a phone number from one WABA to another (consolidating, separating brands, or switching from a self-served WABA to a Tech-Provider-managed one). Meta supports this; Sent treats it as a profile re-mapping. - -1. **In Meta:** initiate "Migrate phone number" in WhatsApp Manager on the *destination* WABA, target the source WABA + number. Meta sends an OTP to the registered number to confirm. -2. **Post-migration:** the *same* `phone_number_id` now belongs to the new WABA. The number, display name, quality rating, and existing `wamid` history all survive. Templates do **not** transfer — they live on the WABA. -3. **In Sent:** update the affected profile's channel config: `PATCH /v3/profiles/{id}/channels/whatsapp { waba_id: }`. The `phone_number_id` is unchanged, so webhook routing is unaffected. -4. **Subscribe Sent's app to the new WABA** (`POST /{new_waba_id}/subscribed_apps`) — the old subscription does not follow the number. -5. **Re-author or re-clone any templates** the tenant used; their old approvals do not migrate. +Meta supports moving a phone number between WABAs and the phone-number ID is stable across the move. On Sent's side, the dashboard Channels page is the supported surface to re-bind. Since the v3 docs do not publish the channel-config mutation endpoint, do not encode a `PATCH /v3/profiles/{id}/channels/whatsapp` call in client integrations — operate via the dashboard until the API is published. -The `phone_number_id` stability is what makes this clean for Sent — webhook routing and MDR history are keyed on it, so message history pre- and post-migration sits in one searchable timeline. +Templates are WABA-scoped and do **not** transfer with the phone number — re-author or re-import on the new WABA.