Skip to content
Concepts

Redirect URLs

When you generate a setup link you can optionally supply two URLs:

  • success_redirect_url — where the tenant lands after they complete Meta Embedded Signup.
  • failure_redirect_url — where they land if anything goes wrong (link expired, signup cancelled, Meta token exchange failed, etc.).

This is the same pattern Stripe Connect, Calendly, and GitHub Apps use to hand control back to the operator’s own UI.

Without redirect URLs the tenant ends up on a generic Kirimdev success page. With them, you can:

  • Catch the user back inside your own app, your styling, your navigation.
  • Read the customer_id query param to surface customer-specific UI (“Acme Logistics is now connected ✓”).
  • Read the account_id query param to pre-select the new WhatsApp number in your operator’s dashboard.
  • Trigger your own onboarding follow-ups — a welcome modal, a setup checklist, a CSAT survey.
await kirim.customers.createSetupLink(customer.id, {
expires_in_hours: 168,
success_redirect_url: 'https://yourapp.com/whatsapp/connected',
failure_redirect_url: 'https://yourapp.com/whatsapp/connect-failed',
})

Both fields are optional and independent. You can set just success, just failure, both, or neither.

If you typo’d a URL, patch the link without rotating the token:

await kirim.customers.updateSetupLink(customer.id, link.id, {
success_redirect_url: 'https://yourapp.com/whatsapp/connected-v2',
failure_redirect_url: null, // clears the redirect
})

Pass null to clear a field. Patching is only allowed while status: 'active' — a consumed/expired/revoked link returns 409 conflict.

Both fields go through the same guard at create time and patch time:

RuleDetail
Parseable URLMust be an absolute URL with a scheme.
HTTPS onlyhttp:// is rejected except for localhost / 127.0.0.1 / *.localhost in development.
No fragment#anchor is rejected because we append query params and a fragment would silently swallow them in some browsers.
Length ≤ 2048 charsSMS / email-friendly.
No private hostsRFC 1918, loopback, link-local, multicast, .local, .internal, and DNS-rebinding patterns (*.nip.io, *.sslip.io with private-IP labels) are rejected unless WEBHOOK_ALLOW_PRIVATE_HOSTS=true is set (dev only).

The check runs server-side at every create/patch, so URL strings in the DB are pre-validated. A failing URL returns 400 invalid_field_value with the field name in error.param:

{
"error": {
"type": "invalid_request_error",
"code": "invalid_field_value",
"message": "must use https:// (http:// is only allowed for localhost dev)",
"param": "success_redirect_url",
"request_id": "req_…"
}
}

When we redirect, we append our params to whatever you already have on the URL — we don’t clobber. Existing fragments are stripped (we rejected them at create time, so this is a defence-in-depth no-op).

https://yourapp.com/whatsapp/connected
?customer_id=cus_335T08RM0EAKN9DTE6RD5RWP7B
&account_id=internal_account_id_xxx
&status=success
ParamWhat it is
customer_idThe public cus_… id. Match against your own database.
account_idThe internal WhatsApp account id (NOT the Meta phone_number_id). Use it only to disambiguate; for sending, look up the phone_number_id from the customer.onboarded webhook or GET /v1/accounts.
statusAlways success on this URL.
https://yourapp.com/whatsapp/connect-failed
?customer_id=cus_335T08RM0EAKN9DTE6RD5RWP7B
&status=failed
&reason=signup_cancelled
ParamWhat it is
customer_idThe public cus_… id, when we know it.
statusAlways failed on this URL.
reasonStable code naming the failure. See reason codes below.
reasonWhen it fires
signup_cancelledThe tenant closed the Meta Embedded Signup popup without completing.
token_exchange_failedMeta rejected the OAuth code. Often expired or already used.
token_info_failedThe follow-up debug_token call failed.
no_waba_foundThe granted scope didn’t include a WhatsApp Business Account.
phone_lookup_failedGET /{waba}/phone_numbers failed.
no_phone_foundThe WABA has no phone numbers attached yet.
account_limit_reachedYour org has hit its WhatsApp account quota.
embedded_signup_disabledYour Kirimdev deployment is missing Facebook app config.
link_already_consumedA parallel callback raced and won.
account_create_failedInternal: the WhatsApp account insert returned no rows.

reason values are stable strings. New ones can be added; existing ones won’t be renamed. Branch defensively in your handler.

The Kirimdev onboarding page handles the bounce client-side:

  1. After a successful callback, it flashes “Connected! Returning you to yourapp.com…” for 1.5 seconds, then window.location.replace(success_url).
  2. After a failure with failure_redirect_url set, it flashes the friendly reason (“You cancelled the Meta authorisation popup…”) for 2.5 seconds, then redirects.
  3. If no redirect URL is configured, it shows the inline status indefinitely and tells the tenant “you can close this tab”.

Both flash screens include an <a> fallback for the redirect URL in case window.location.replace is blocked (some webviews):

Returning you to yourapp.com… Click here if you're not redirected
{
success_redirect_url: 'https://yourapp.com/onboarding/success',
failure_redirect_url: 'https://yourapp.com/onboarding/failure',
}

Easiest. Each page reads customer_id (and reason on failure) and renders independently.

{
success_redirect_url: 'https://yourapp.com/onboarding/result',
failure_redirect_url: 'https://yourapp.com/onboarding/result',
}

Same component handles both. Branch in your React/Vue/etc:

function OnboardingResult() {
const { status, reason, customer_id } = useQueryParams()
if (status === 'success') return <Success customerId={customer_id} />
return <Failure customerId={customer_id} reason={reason} />
}

Pattern 3: redirect into the customer’s tenant page

Section titled “Pattern 3: redirect into the customer’s tenant page”
{
success_redirect_url: `https://yourapp.com/tenants/${customer.id}/whatsapp`,
}

Skip the intermediate result page entirely. The tenant lands directly on the surface that uses the new WhatsApp account.

{
success_redirect_url: `https://yourapp.com/onboarded?campaign=q3_signup_drive&internal_ref=${customer.email}`,
}

We preserve your existing query string and append our own. You’ll see all of them on the redirected URL.

http://localhost:3000/onboarded is accepted even though it’s http. Set WEBHOOK_ALLOW_PRIVATE_HOSTS=true in your local .env if you need to use other private IPs (192.168.x.x for a phone on the same LAN, for instance). Never set this in production.

  • Setup links — where you configure the redirect URLs.
  • Onboarding flow — when in the flow the redirect happens.
  • Errors — the standard error envelope shape.