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.
Why bother
Section titled “Why bother”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_idquery param to surface customer-specific UI (“Acme Logistics is now connected ✓”). - Read the
account_idquery 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.
Setting them
Section titled “Setting them”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',})curl -sS https://api.kirimdev.com/v1/customers/cus_xxx/setup_links \ -H 'Authorization: Bearer kdv_live_xxx' \ -H 'Content-Type: application/json' \ -d '{ "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.
Patching an active link
Section titled “Patching an active link”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.
Validation
Section titled “Validation”Both fields go through the same guard at create time and patch time:
| Rule | Detail |
|---|---|
| Parseable URL | Must be an absolute URL with a scheme. |
| HTTPS only | http:// 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 chars | SMS / email-friendly. |
| No private hosts | RFC 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_…" }}Query param contract
Section titled “Query param contract”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).
Success
Section titled “Success”https://yourapp.com/whatsapp/connected ?customer_id=cus_335T08RM0EAKN9DTE6RD5RWP7B &account_id=internal_account_id_xxx &status=success| Param | What it is |
|---|---|
customer_id | The public cus_… id. Match against your own database. |
account_id | The 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. |
status | Always success on this URL. |
Failure
Section titled “Failure”https://yourapp.com/whatsapp/connect-failed ?customer_id=cus_335T08RM0EAKN9DTE6RD5RWP7B &status=failed &reason=signup_cancelled| Param | What it is |
|---|---|
customer_id | The public cus_… id, when we know it. |
status | Always failed on this URL. |
reason | Stable code naming the failure. See reason codes below. |
Failure reason codes
Section titled “Failure reason codes”reason | When it fires |
|---|---|
signup_cancelled | The tenant closed the Meta Embedded Signup popup without completing. |
token_exchange_failed | Meta rejected the OAuth code. Often expired or already used. |
token_info_failed | The follow-up debug_token call failed. |
no_waba_found | The granted scope didn’t include a WhatsApp Business Account. |
phone_lookup_failed | GET /{waba}/phone_numbers failed. |
no_phone_found | The WABA has no phone numbers attached yet. |
account_limit_reached | Your org has hit its WhatsApp account quota. |
embedded_signup_disabled | Your Kirimdev deployment is missing Facebook app config. |
link_already_consumed | A parallel callback raced and won. |
account_create_failed | Internal: 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.
What the tenant sees
Section titled “What the tenant sees”The Kirimdev onboarding page handles the bounce client-side:
- After a successful
callback, it flashes “Connected! Returning you to yourapp.com…” for 1.5 seconds, thenwindow.location.replace(success_url). - After a failure with
failure_redirect_urlset, it flashes the friendly reason (“You cancelled the Meta authorisation popup…”) for 2.5 seconds, then redirects. - 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 redirectedCommon UX patterns
Section titled “Common UX patterns”Pattern 1: separate success/failure pages
Section titled “Pattern 1: separate success/failure pages”{ 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.
Pattern 2: one page, branch on status
Section titled “Pattern 2: one page, branch on status”{ 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.
Pattern 4: pass your own tracking params
Section titled “Pattern 4: pass your own tracking params”{ 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.
Localhost development
Section titled “Localhost development”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.
See also
Section titled “See also”- Setup links — where you configure the redirect URLs.
- Onboarding flow — when in the flow the redirect happens.
- Errors — the standard error envelope shape.