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, metadata?, 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, and metadata

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",
    "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_..."