2026-09-24

Provisioning a WhatsApp number in six steps

Giving each business its own WhatsApp number, its own inbox and an AI agent used to be a manual checklist across three dashboards. I turned it into two endpoints. This is what those endpoints actually do, because the sequence and the failure modes are where the lessons are.

The pieces: Twilio provides the number and the WhatsApp Business API, Chatwoot provides the inbox, conversations and human takeover, and Meta owns the WhatsApp Business Account and its approval.

Step 1: POST /provision-number

Authenticated with the business’s bearer token, with an optional area code. The service performs six operations in order, five external calls and then a save:

1. Twilio   create a subaccount              -> sub_sid, sub_auth_token
2. Twilio   buy a number on that subaccount  -> phone_number
3. Chatwoot POST /platform/api/v1/accounts        { name }           -> account_id
4. Chatwoot POST /platform/api/v1/agent_bots      { name, outgoing_url: <our webhook>, account_id }
                                                                     -> bot_id, bot_access_token
5. Chatwoot POST /api/v1/accounts/{id}/inboxes    { channel: { type: "api", medium: "whatsapp",
                                                     phone_number, twilio credentials } }
                                                                     -> inbox_id
6. Ours     save one row mapping all of it to the business

Two design choices stand out. A Twilio subaccount per business isolates credentials, usage and billing, and means a compromised token exposes one tenant. And a Chatwoot account per business means each business sees only its own conversations, with the bot registered per account so the webhook payload carries an account ID that resolves back to a tenant.

The row saved at the end holds the whole mapping: business, Chatwoot account, bot ID and token, inbox ID, the Twilio subaccount credentials and the phone number.

Step 2: the human part, Meta embedded signup

WhatsApp requires the business to authorise access to its own WhatsApp Business Account. That happens in the business’s browser via Meta’s embedded signup, which hands back a waba_id and a phone_number_id. The business then calls POST /connect-wa:

1. Twilio  create a Messaging Service on the subaccount
            (use_inbound_webhook_on_number = true)
2. Twilio  add the number as a WhatsApp sender on that service
3. Ours    update the mapping row with waba_id, phone_number_id and the sender ids

After this, a message to the number flows: customer WhatsApp, Twilio, Chatwoot, agent-bot webhook.

GET /chatwoot_link asks Chatwoot’s platform API for a time-limited SSO login URL for the business’s user, so they land in their inbox already signed in. There is no second password to manage and no credentials in an email.

An observation on failure modes

My reading of this flow, not something the docs call out: steps 1 to 5 create external resources in sequence and the mapping row is only written at step 6. If step 4 fails, you have a subaccount, a purchased number and a Chatwoot account that the database doesn’t know about. Numbers cost money and subaccounts are hard to find later, so a flow like this wants either compensation (delete what you created on failure) or an early “provisioning started” row that records each resource as it is created. The right shape is idempotent steps keyed by business, so a retry resumes rather than duplicates.

Takeaways

← All posts