Onboarding Links

Onboarding links let API-only customers onboard their end-users' WhatsApp numbers without building any frontend. You create a link, send the hosted URL to your end-user, and they complete Meta's Embedded Signup in the browser. After completion, we redirect back to your app and fire a webhook event.

Inspired by Stripe Connect and Plaid Hosted Link.


Flow

1 Your Server Our API
POST /v1/onboarding-links with {name, externalId, redirectUrl, webhookUrl?, webhookSecret?, webhookEvents?}
Our API Your Server
Returns {id, hostedUrl, status: "pending", ...}
2 Your Server End-User
Send the hostedUrl to your end-user (email, in-app link, etc.)
3 End-User Our API
Opens hostedUrl in the browser — serves the hosted signup page
4 End-User Meta
Clicks "Connect WhatsApp" — FB.login() popup opens for Embedded Signup
Meta End-User
Returns OAuth code after user completes signup
5 End-User Our API
POST /signup/:id/callback with the OAuth code
6 Our API
Processes signup: exchanges code for tokens, sets up WABA & phone number
7 Our API Your Server
Fires phone_number.connected webhook event
8 Our API End-User
Redirects to redirectUrl?onboarding_link_id=x&external_id=y&status=success
9 Your Server Our API
GET /v1/onboarding-links/:id — fetch the completed result with phoneNumberId, phoneNumber, wabaId

Create an Onboarding Link

curl -X POST https://api.example.com/v1/onboarding-links \
  -H "Authorization: Bearer sk_org_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corp",
    "externalId": "customer-123",
    "redirectUrl": "https://myapp.com/whatsapp/callback",
    "webhookUrl": "https://myapp.com/webhooks/whatsapp",
    "webhookSecret": "whsec_my_secret_key_123",
    "webhookEvents": ["message.received", "message.sent"]
  }'
Field Type Required Description
name string Yes Display name shown in the signup page
externalId string No Your internal identifier for this link
redirectUrl string No URL to redirect to after signup completes
webhookUrl string No URL for an outgoing webhook auto-registered when the phone connects
webhookSecret string No HMAC signing secret for the webhook (16-256 chars)
webhookEvents string[] No Event types for the webhook (required if webhookUrl is set)

When webhookUrl is provided, an outgoing webhook is automatically created and scoped to the new WABA once the phone number connects. This saves a separate POST /v1/outgoing-webhooks call.

Response (201):

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Acme Corp",
    "status": "pending",
    "externalId": "customer-123",
    "redirectUrl": "https://myapp.com/whatsapp/callback",
    "hostedUrl": "https://api.example.com/signup/550e8400-...",
    "phoneNumberId": null,
    "phoneNumber": null,
    "wabaId": null,
    "isPhoneRegistered": null,
    "webhookUrl": "https://myapp.com/webhooks/whatsapp",
    "webhookSecret": "whsec_my_secret_key_123",
    "webhookEvents": ["message.received", "message.sent"],
    "createdAt": "2024-01-15T10:30:00.000Z",
    "completedAt": null
  }
}

Send the hostedUrl to your end-user (email, in-app link, etc.). They open it in a browser to start the signup flow.


Redirect After Completion

If you provided a redirectUrl, the end-user is redirected after signup:

On success:

https://myapp.com/whatsapp/callback?onboarding_link_id=x&external_id=customer-123&status=success

On error:

https://myapp.com/whatsapp/callback?onboarding_link_id=x&external_id=customer-123&status=error&error=brief+message

If no redirectUrl is set, the hosted page shows a success/error message inline.


Get Onboarding Link (Enriched)

After the redirect (or webhook), fetch the full result:

curl https://api.example.com/v1/onboarding-links/{id} \
  -H "Authorization: Bearer sk_org_..."

Response when completed:

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Acme Corp",
    "status": "completed",
    "externalId": "customer-123",
    "redirectUrl": "https://myapp.com/whatsapp/callback",
    "hostedUrl": "https://api.example.com/signup/550e8400-...",
    "phoneNumberId": "660e8400-e29b-41d4-a716-446655440001",
    "phoneNumber": "+5491155551234",
    "wabaId": "770e8400-e29b-41d4-a716-446655440002",
    "isPhoneRegistered": true,
    "webhookUrl": "https://api.example.com/webhook/phone/660e8400-...",
    "createdAt": "2024-01-15T10:30:00.000Z",
    "completedAt": "2024-01-15T10:35:00.000Z"
  }
}

Use the phoneNumberId from this response to send messages.

Before sending production traffic, call GET /v1/phones/{phoneNumberId}/health and check the relevant field in data.capabilities. A phone can be connected and even registered, but still not be fully ready if Meta reports health or subscription issues.

Example:

curl https://api.example.com/v1/phones/{phoneNumberId}/health \
  -H "Authorization: Bearer sk_org_..."

For inbound-first use cases, treat data.capabilities.canReceiveMessages: true as the platform's current "safe to use" signal.


Webhook Event: phone_number.connected

For server-to-server reliability, subscribe to the phone_number.connected event via Outgoing Webhooks:

{
  "event": "phone_number.connected",
  "timestamp": "2024-01-15T10:35:00.000Z",
  "organizationId": "org_uuid",
  "data": {
    "onboardingLinkId": "link_uuid",
    "externalId": "customer-123",
    "phoneNumberId": "phone_uuid",
    "phoneNumber": "+5491155551234",
    "wabaId": "waba_uuid",
    "metaWabaId": "meta_waba_id",
    "isPhoneRegistered": true
  }
}

List Onboarding Links

curl "https://api.example.com/v1/onboarding-links?page=1&limit=50" \
  -H "Authorization: Bearer sk_org_..."

Delete an Onboarding Link

Only pending links can be deleted.

curl -X DELETE https://api.example.com/v1/onboarding-links/{id} \
  -H "Authorization: Bearer sk_org_..."