Wonder

Sign in with Wonder

Authenticate end-users with the Wonder consumer app via QR code or app-to-app deep link, then exchange the result for a session on your site.

"Sign in with Wonder" lets merchants and partners authenticate end-users with their existing Wonder consumer account. Two flows are supported:

  1. QR code (desktop / cross-device) β€” your web app renders a short-link QR code. The user scans it with the Wonder app to approve sign-in. Your server polls for completion.
  2. App-to-app deep link (mobile) β€” on a phone that has the Wonder app installed, your web or native app opens a wonder://authorization?... URL. Wonder returns an authorization code to a callback you control, which you exchange for the user profile server-side.

Both flows end the same way: your server receives a verified user identity (email, Wonder user ID, display name) and creates / signs in the matching account on your platform.

[CLARIFY: confirm the canonical product name used in marketing β€” "Sign in with Wonder" vs "Sign in with Wonder ID" vs "Wonder Login".]


When to use which flow

FlowBest forUser has Wonder app on…
QR codeDesktop web, kiosks, terminals, secondary-screen sign-ina separate phone
App-to-appMobile web, native iOS/Android partner appsthe same device

You typically implement both and let the client pick based on device capability (e.g. isMobile() && hasWonderInstalled()).


Prerequisites

Before either flow works, your tenant must be onboarded by Wonder. Wonder will provision:

  • bindo_client_id β€” sent as x-client-id on every Bindo call.
  • app_key β€” sent as x-app-key on gateway calls.
  • app_slug β€” sent as x-app-slug on gateway calls.
  • app_bearer β€” long-lived JWT used as Authorization: Bearer … on the gateway.
  • app_to_app_client_id + app_to_app_secret β€” used to sign app-to-app deep links via HMAC-SHA256.
  • app_scheme β€” the Wonder URL scheme for the target environment (wonder, wonder-alpha, wonder-stg).

Environment hosts:

EnvBindo hostGateway hostApp scheme
Productionhttps://main.bindo.cohttps://gateway.wonder.appwonder
Alphahttps://main-alpha.bindo.cohttps://gateway-alpha.wonder.appwonder-alpha
Staginghttps://main-stg.bindo.cohttps://gateway-stg.wonder.appwonder-stg

On Alpha, all requests must also include x-internal: TRUE.

Store these credentials server-side only. Never ship app_bearer, app_to_app_secret, or the bearer JWT to the browser.


Flow 1 β€” QR code sign-in

Sequence

sequenceDiagram participant U as User (phone) participant B as Browser participant S as Your server participant Bindo as Bindo (main.bindo.co) participant GW as Wonder Gateway B->>S: POST /sign-in/wonder/qr/start S->>Bindo: POST /user/b2c/qr_code Bindo-->>S: { uuid } S->>GW: POST /api/short-chain/qrcode-links<br/>{ link_type: "Scan Code Login", data: { uuid, client_id } } GW-->>S: { shortChain.sUrl } S-->>B: { uuid, qr_url } B->>B: Render qr_url as a QR image U->>U: Open Wonder app, scan QR loop every 2s B->>S: POST /sign-in/wonder/qr/poll { uuid } S->>Bindo: GET /user/b2c/qr_code/info?id={uuid} Bindo-->>S: { is_scan, access_token, is_expired, is_cancel } end S->>Bindo: GET /user/me (with x-user-access-token) Bindo-->>S: { email, name, user_id } S->>S: Find or create local user, mint session S-->>B: { status: "completed", token_hash } B->>B: Verify magic-link / set session cookie β†’ redirect

Step 1 β€” Start a QR session (server)

Call Bindo to mint a session UUID, then ask the gateway to wrap it in a short link. The short link is what you encode as a QR code.

// Server function (TanStack Start / Node / any backend) const baseHeaders = { accept: "application/json, text/plain, */*", "x-client-id": cfg.bindo_client_id, "x-i18n-lang": "en-US", "x-request-id": crypto.randomUUID(), ...(isAlpha ? { "x-internal": "TRUE" } : {}), }; // 1. Mint a UUID const r1 = await fetch(`${bindoBase}/user/b2c/qr_code`, { method: "POST", headers: { ...baseHeaders, "content-length": "0" }, }); const { uuid } = (await r1.json()).data ?? (await r1.json()); // 2. Wrap as a short link const r2 = await fetch(`${gatewayBase}/api/short-chain/qrcode-links`, { method: "POST", headers: { ...baseHeaders, authorization: `Bearer ${cfg.app_bearer}`, "content-type": "application/json", "x-app-key": cfg.app_key, "x-app-slug": cfg.app_slug, }, body: JSON.stringify({ type: "Normal", link_type: "Scan Code Login", data: { uuid, client_id: cfg.bindo_client_id }, }), }); const j2 = await r2.json(); const qr_url = j2.data?.shortChain?.sUrl; return { uuid, qr_url };

[CLARIFY: response shape for /user/b2c/qr_code and /api/short-chain/qrcode-links varies across environments (sometimes wrapped in data, sometimes flat, sometimes result). Please publish the canonical envelope so SDK consumers don't have to defensively try every shape.]

Step 2 β€” Render the QR code (browser)

import QRCode from "qrcode"; const { uuid, qr_url } = await startWonderQrSession(); const dataUrl = await QRCode.toDataURL(qr_url, { width: 256, margin: 1 }); setQr(dataUrl); beginPolling(uuid);

Step 3 β€” Poll for completion (server)

Poll every 2 seconds. Stop on cancelled, expired, or completed.

const r = await fetch(`${bindoBase}/user/b2c/qr_code/info?id=${uuid}`, { headers }); const { data: d } = await r.json(); if (d.is_cancel) return { status: "cancelled" }; if (d.is_expired) return { status: "expired" }; if (!d.is_scan || !d.access_token) return { status: "pending" }; // Approved β†’ fetch profile using the returned access token const me = await fetch(`${bindoBase}/user/me`, { headers: { ...headers, "x-user-access-token": d.access_token }, }).then((r) => r.json()); const email = me.email ?? me.data?.email; // β†’ find-or-create local user, mint your own session

[CLARIFY: the profile endpoint we currently hit is /user/me, but several deployments only respond on /user/profile, /user/info, /user/b2c/me, or /user/b2c/profile. Please confirm the single canonical endpoint partners should call, and the exact JSON shape of the response (email, name, user_id locations).]

[CLARIFY: polling cadence and the official expired timeout. We currently use 2s polling and assume Bindo expires the UUID after ~5 minutes. Please confirm.]

Step 4 β€” Issue a local session

Map the verified Wonder email to a local account (create on first sign-in), then issue your normal session β€” magic-link verification, signed cookie, JWT, etc. Wonder does not manage your platform's session; it only attests identity.


Use when the user is already on a mobile device with the Wonder app installed. You build a signed wonder://authorization?... URL, redirect the browser to it, Wonder approves the user, and Wonder calls back to your callback URL with an authorization code you exchange server-side.

Sequence

sequenceDiagram participant B as Browser (mobile) participant S as Your server participant W as Wonder app participant GW as Wonder Gateway B->>S: POST /sign-in/wonder/applink/start { callback } S->>S: requestTime = now()<br/>nonce = uuid()<br/>signature = HMAC_SHA256(secret, `${clientId}|${requestTime}|${nonce}`) S-->>B: { url: "wonder://authorization?client_id=…&request_time=…&nonce=…&signature=…&callback=…" } B->>W: window.location = url W->>W: User approves W->>B: Redirect to {callback}?code=… B->>S: POST /sign-in/wonder/applink/exchange { code } S->>GW: POST /api/authorization/access_token (signed) GW-->>S: { access_token } S->>GW: GET /user/me (with x-user-access-token) GW-->>S: { email, name, user_id } S-->>B: { token_hash } β†’ set session
async function hmacSha256Hex(key: string, message: string) { const enc = new TextEncoder(); const k = await crypto.subtle.importKey("raw", enc.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); const sig = await crypto.subtle.sign("HMAC", k, enc.encode(message)); return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join(""); } const requestTime = Date.now(); const nonce = crypto.randomUUID(); const signature = await hmacSha256Hex( cfg.app_to_app_secret, `${cfg.app_to_app_client_id}|${requestTime}|${nonce}`, ); const url = `${cfg.app_scheme}://authorization` + `?client_id=${encodeURIComponent(cfg.app_to_app_client_id)}` + `&request_time=${requestTime}` + `&nonce=${encodeURIComponent(nonce)}` + `&signature=${signature}` + `&callback=${encodeURIComponent(callback)}`; return { url };
const { url } = await startWonderAppLink({ callback: `${origin}/auth/wonder/callback` }); window.location.assign(url);

If the Wonder app isn't installed, the deep link will silently fail on most platforms. Fall back to the QR flow (rendered in a modal) after a short timeout.

[CLARIFY: is there an officially supported way to detect "Wonder app not installed" on iOS/Android so we can auto-fall back to the QR flow without the 2-3 second flicker?]

Step 3 β€” Receive the callback and exchange the code

Wonder redirects the browser to {callback}?code=.... Your callback route forwards the code to your server, which exchanges it for an access token, then loads the profile.

// Server-side exchange const requestTime = Date.now(); const nonce = crypto.randomUUID(); const signature = await hmacSha256Hex( cfg.app_to_app_secret, `${cfg.app_to_app_client_id}|${requestTime}|${nonce}`, ); const r = await fetch(`${gatewayBase}/api/authorization/access_token`, { method: "POST", headers: { "content-type": "application/json", accept: "application/json", "x-client-id": cfg.bindo_client_id, "x-app-key": cfg.app_key, "x-app-slug": cfg.app_slug, authorization: `Bearer ${cfg.app_bearer}`, ...(isAlpha ? { "x-internal": "TRUE" } : {}), }, body: JSON.stringify({ client_id: cfg.app_to_app_client_id, code, authorization_code: code, grant_type: "authorization_code", request_time: requestTime, nonce, signature, }), }); const { access_token } = (await r.json()).data ?? (await r.json()); const me = await fetch(`${bindoBase}/user/me`, { headers: { ...baseHeaders, "x-user-access-token": access_token }, }).then((r) => r.json());

[CLARIFY: the canonical exchange endpoint. Our current integration tries seven different paths across three hosts (/api/authorization/access_token, /api/oauth/access_token, /api/authorization/token, /authorization/access_token, /oauth/access_token, /user/b2c/authorization/access_token, /user/b2c/app_to_app/access_token) because behavior differs between Production, Alpha, and Staging. Please publish one canonical path per environment.]

[CLARIFY: the exact field name returned for the user token β€” we've seen access_token, data.access_token, result.access_token, user_access_token, and data.user_access_token. Please standardise.]

[CLARIFY: signature algorithm details. We currently sign ${clientId}|${requestTime}|${nonce} with HMAC-SHA256 and submit hex-lowercase. Please confirm (a) field separator, (b) field ordering, (c) hash algorithm, (d) encoding (hex vs base64), and (e) whether the body must also be included in the signature.]


Reference: HTTP headers

Every request you make to Bindo or the gateway should carry:

HeaderValueRequired on
x-client-idbindo_client_idAll requests
x-i18n-langen-US (or user locale)All requests
x-request-idFresh UUID per requestAll requests
x-app-keyapp_keyGateway requests
x-app-slugapp_slugGateway requests
authorizationBearer ${app_bearer}Gateway requests
x-internalTRUEAlpha environment only
x-user-access-tokenToken from QR/exchangeProfile lookup only

Storing the Wonder identity

Once you have a verified email and wonder_user_id, persist them on your user record:

alter table public.profiles add column wonder_user_id text, add column signup_source text; create unique index profiles_wonder_user_id_unique on public.profiles (wonder_user_id) where wonder_user_id is not null;

Subsequent sign-ins should match by wonder_user_id first, then fall back to email, so users who change their Wonder email don't get a duplicate account.


Testing

  • Use the Alpha environment with x-internal: TRUE for end-to-end tests; it accepts test accounts that don't bill or send notifications.
  • The QR qr_url is a normal HTTPS short link β€” you can open it in a regular browser to inspect what the Wonder app will see.
  • Use a fresh uuid for every sign-in attempt; reused UUIDs are rejected as expired.

[CLARIFY: are there public test Wonder accounts (email + scan-only) that partners can use during integration without standing up a real Wonder consumer profile?]


Security checklist

  • Keep app_bearer, app_to_app_secret, and the bearer JWT server-side. Never expose them to the browser.
  • Verify the signature on the deep-link callback before trusting the code.
  • Rate-limit /qr/start and /applink/start per IP β€” both create gateway resources.
  • After exchanging a code or completing a QR session, invalidate the corresponding uuid / code on your side so it cannot be replayed.
  • Issue your own session (cookie, JWT, magic-link) rather than relying on the Wonder access token for ongoing authorization β€” Wonder only attests the user's identity at one point in time.