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
POST /v1/onboarding-links with {name, externalId, metadata?, redirectUrl, webhookUrl?, webhookSecret?, webhookEvents?}{id, hostedUrl, status: "pending", ...}hostedUrl to your end-user (email, in-app link, etc.)hostedUrl in the browser — serves the hosted signup pageFB.login() popup opens for Embedded SignupPOST /signup/:id/callback with the OAuth codephone_number.connected webhook eventredirectUrl?onboarding_link_id=x&external_id=y&status=successGET /v1/onboarding-links/:id — fetch the completed result with phoneNumberId, phoneNumber, wabaId, and metadataCreate 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",
"metadata": {
"customerId": "customer-123",
"locationCode": "store-01",
"crmAccountId": "crm_789"
},
"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 |
| metadata | object | No | Structured customer metadata copied to the connected phone |
| 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.
Use metadata for non-secret structured identifiers that help your integration recognize the
connected phone later. The metadata is stored locally on the onboarding link, copied to the phone
when signup completes, and returned by secured GET endpoints. It is not sent to Meta and is not
included in the public redirect URL. See Onboarding Metadata
for the full contract and verification path.
Response (201):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corp",
"status": "pending",
"externalId": "customer-123",
"metadata": {
"customerId": "customer-123",
"locationCode": "store-01",
"crmAccountId": "crm_789"
},
"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",
"metadata": {
"customerId": "customer-123",
"locationCode": "store-01",
"crmAccountId": "crm_789"
},
"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. The same metadata is also returned
by GET /v1/phones/{phoneNumberId} and WABA listing/detail endpoints.
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.
Inspect Connected Business Metadata
Use the completed wabaId from the onboarding link response to inspect the Meta account that owns the connected phone:
curl https://api.example.com/v1/wabas/{wabaId}/connection \
-H "Authorization: Bearer sk_org_..."
The response includes the linked phones plus a connectedAccount object with the Business Portfolio, WABA, Pages, datasets, catalogs, Instagram account IDs, and marketing messages onboarding status returned by Meta or captured during Embedded Signup.
If you already call GET /v1/phones/{phoneNumberId} with an organization API key, that phone
detail response also includes the same connectedAccount object.
{
"data": {
"waba": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"metaWabaId": "123456789012345",
"name": "Acme WhatsApp Business Account",
"currency": "USD",
"ownerBusiness": {
"id": "456789012345678",
"name": "Acme Business Portfolio"
},
"datasetId": "901234567890123",
"pageId": "789012345678901"
},
"connectedAccount": {
"source": "embedded_signup",
"business": {
"id": "456789012345678",
"name": "Acme Business Portfolio"
},
"waba": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"metaWabaId": "123456789012345",
"name": "Acme WhatsApp Business Account",
"currency": "USD"
},
"pageIds": ["789012345678901"],
"selectedPageId": "789012345678901",
"pages": [
{
"id": "789012345678901",
"name": "Acme Facebook Page",
"isFromOnboarding": true,
"isSelectedForCapi": true
}
],
"datasetIds": ["901234567890123"],
"selectedDatasetId": "901234567890123",
"catalogIds": [],
"instagramAccountIds": [],
"marketingMessagesOnboardingStatus": { "status": "COMPLETED" },
"graphLookupErrors": []
},
"phones": []
}
}
connectedAccount.graphLookupErrors is non-fatal. If a live Graph lookup fails, stored IDs captured during onboarding are still returned when available.
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_..."