A Dualhook onboarding session is a one-time co-branded WhatsApp Embedded Signup link for a single tenant (valid for 1 hour by default, configurable up to 24h). Your backend creates the session, your tenant clicks the resulting URL inside your app, and Dualhook posts a signed lifecycle event to your endpoint when onboarding completes. Each session round-trips your tenant id and free-form metadata.
What an onboarding session is
A session represents a single attempt by a single tenant to connect a WhatsApp Business number through your product. Sessions are scoped to your API key (Platform-tier subscription) and live for 1 hour after creation by default (override with expiresInSeconds, 300–86400). After expiry, the URL stops working and onboarding.failed fires with reason='expired'.
Create a session
POST https://dualhook.com/api/v1/onboarding/sessions
Authorization: Bearer dh_live_xxx
Content-Type: application/json
Idempotency-Key: <optional, recommended>
{
"tenantId": "cust_abc123",
"tenantName": "Acme Clinic",
"successRedirectUrl": "https://app.example.com/connected",
"failureRedirectUrl": "https://app.example.com/error",
"cancelRedirectUrl": "https://app.example.com/cancel",
"webhookOverrideUrl": "https://api.example.com/wh",
"webhookVerifyToken": "<your endpoint's hub.verify_token>",
"metadata": { "internal_id": "..." }
}
Returns:
{
"sessionId": "sess_xxxxxxxx",
"onboardingUrl": "https://dualhook.com/onboard/<token>",
"expiresAt": "2026-04-29T13:34:56Z",
"idempotencyExpiresAt": "2026-04-29T13:34:56Z"
}
- All redirect URLs must match a pattern in your Allowed Redirect URLs whitelist (configurable in the dashboard tab) — this prevents open-redirect abuse if your API key is ever stolen.
metadatais round-tripped on every onboarding event for that session.
Webhook verification preflight
Before creating the session, Dualhook sends a simulated Meta verification GET to your webhookOverrideUrl — ?hub.mode=subscribe&hub.verify_token=<your token>&hub.challenge=<random> — and requires HTTP 200 with the body echoing the challenge exactly. If your endpoint would fail Meta's real verification (403 from a WAF, 404, POST-only route, >5s response, JSON-wrapped challenge, …), session creation fails immediately with webhook_preflight_failed and a preflight.category telling you exactly what to fix — instead of your tenant completing the entire Embedded Signup flow and then hitting onboarding.failed.
Endpoint requirements (same as Meta's): a GET handler on the webhook path, unauthenticated, responding in under 5 seconds with the raw hub.challenge value as plain text — no WAF challenge pages, no JSON wrapper, no heavy work before responding.
To skip the preflight, pass "skipWebhookPreflight": true in the body. You need this if your endpoint allowlists Meta's IP ranges or user-agent: the preflight originates from Dualhook's infrastructure, not Meta's, so such an endpoint fails preflight while working fine in production.
Look up a session
GET /api/v1/onboarding/sessions/:id
Returns status, redirect URLs, and the resulting connectionId if completed.
Webhook configuration is WABA-scoped
Meta supports both WABA-level and phone-number-level Webhook Override (POST /{wabaId}/subscribed_apps and POST /{phone-number-id}/subscribed_apps). Dualhook Platform v1 uses the WABA-level path only — one override_callback_uri and verify_token per connected WABA. As a consequence, all phone numbers under one connected WABA share one webhook URL and verify token in Dualhook.
Re-onboarding the same tenant: latest config wins. When a session's tenant already has connections on the WABA being onboarded, the new webhookOverrideUrl + webhookVerifyToken simply replace the old ones — for every connection on that WABA. This makes retries, token rotation, and test→prod endpoint migrations work without any cleanup:
- Same phone, identical webhook config — the onboarding completes idempotently against the existing connection (no duplicate row, no error). The
onboarding.completedevent carriesalreadyConnected: true. - Same phone, changed webhook config — the existing connection is reconnected in place with the new config and a fresh access token. The event carries
webhookConfigReplaced: true. - New phone on an already-connected WABA, changed webhook config — the new connection is created and every sibling connection on the WABA adopts the new config once Meta accepts it. The event carries
webhookConfigReplaced: trueandupdatedSiblingConnectionIds.
Across tenants the conflict stays strict. If the WABA already has connections belonging to a different tenant of yours, the new config must exactly match the existing webhookOverrideUrl + webhookVerifyToken, or the session fails with waba_webhook_conflict — replacing a WABA-level override would silently reroute the other tenant's numbers. The error tells you whether the URL, the verify token, or both mismatched, and includes the existing URL on file. To deliberately change a WABA's webhook config outside onboarding, use PATCH /api/v1/wabas/:wabaId/webhook.
In practice the WABA-shared model is what most partners want: one endpoint per business, demux by phone_number_id from the payload (which you'd do anyway with Cloud API). If you need per-phone-number webhook routing, contact us — moving to per-phone overrides is on the v2 list.
Session expiry and idempotency
- Sessions are valid for 1 hour by default. Pass
expiresInSeconds(300–86400) to override — useful when you email or queue the onboarding URL instead of opening it immediately. Idempotency-Keyis optional but recommended; reusing it within 1 hour returns the same session, allowing safe retries. Different body with the same key returnsidempotency_key_collision.