Troubleshooting

Top failure modes, in the order we see them. Include the X-Request-Id response header when contacting support.

Origin mismatch (popup mode)

Symptom: popup opens, login completes, but your page never receives the result; console may show a postMessage origin error, or /authorize renders an error page.

Cause: the page origin isn't in the site's allowed origins. Origins are matched exactly — https://www.example.com and https://example.com are different origins, as is any port difference. Okia validates the origin before issuing a code and posts the result only to that exact origin.

Fix: add the exact origin (scheme + host + port) in Site → Settings. Check for a www/apex mismatch first — it's the usual culprit.

redirect_uri mismatch

Symptom: error page on /authorize (redirect_uri_mismatch) instead of the login sheet. By design Okia never redirects to an unregistered URI, so the failure is loud. The refused URL is never shown on that page — it is listed under the site's Settings tab in the dashboard, with a button to add it.

Cause: the redirect_uri param doesn't byte-match a registered redirect URI. No wildcards, no path prefixes, trailing slash counts.

Fix: copy the exact callback URL your integration prints (the WordPress plugin and dashboard both show it) into Site → Settings. Also confirm you send the same redirect_uri to /oauth/token as you did to /authorize — a mismatch there fails the exchange with invalid_grant.

Popup blocked

Symptom: nothing happens on click, or the browser shows a blocked-popup indicator.

Cause: the popup wasn't opened from a direct user gesture, or the browser blocks popups regardless (common on iOS).

Fix: usually nothing — the SDK detects the blocked popup and automatically falls back to a full-page redirect. If you call pw.login() yourself, call it synchronously inside the click handler, not after an await. Redirect is the documented baseline; treat popups as progressive enhancement and make sure your redirect callback works.

OTP not arriving

Check in this order:

Check Detail
Resend cooldown Resend waits 30s, then 60/120/300s. The sheet shows the countdown — the user may just be early
Per-phone send caps 3 per 10 min, 8/hour, 15/24h. A user retrying in a loop hits these; the API still returns 202 {"status":"sent"} (enumeration safety), so "sent" does not prove delivery
Per-site caps 500/hour, 3000/24h per site — fixed, not adjustable. A test-mode site is also capped at 25 users: crossing 20 records site.quota_warning, which appears in Dashboard → Activity and in the alerts sent to your organization's contact. It is not a webhook event
Sender instance health Dashboard → Activity shows delivery status per send; an unhealthy WhatsApp sender instance is failed over automatically, but delivery can lag
User's WhatsApp The number must have WhatsApp installed and reachable — the "Open WhatsApp" chip on the sheet deep-links to the chat with the number that sent the code (until the sender is known it opens WhatsApp itself)
Delivery check The sheet re-checks the send about 6 s after "sent". If the message could not go out — for any reason — it says so and offers resend at once, without saying why

Also: 10 failed verifications per hour locks the phone for 1 hour, a code expires after 5 minutes, and 5 wrong attempts burn it. "Code didn't work" after a long delay usually means expiry — resend.

Clock skew breaks JWT validation

Symptom: intermittent exp/iat/nbf validation failures on freshly issued tokens, often only on one server.

Cause: your server's clock is off. Tokens live 15 minutes, so even ±1–2 minutes of skew produces "expired on arrival" or "issued in the future" errors.

Fix: run NTP (chrony/systemd-timesyncd) and configure a small leeway (30–60s) in your JWT library — e.g. clockTolerance in jose. Don't crank leeway past a couple of minutes; fix the clock instead.

token_use confusion

Symptom: "invalid audience" when verifying, or — worse — logins succeed but user data looks wrong.

Cause: the two JWTs are easy to swap. The access token has aud: "https://auth.okia.io" and token_use: "access"; the ID token has aud: <your client_id> and token_use: "id".

Fix:

You are doing Verify against Require
Creating a session from a login ID token aud = client_id, token_use = "id", plus nonce
Authorizing an API call to /userinfo Access token send it as the Bearer token; never mint sessions from it

If your verifier accepts a token without checking token_use, fix that today — accepting an access token as an ID token is a security bug, not a compatibility feature. @okia/react's verifyOkiaToken checks iss, aud, exp, and token_use for you.

Still stuck

  • Dashboard → Activity: every auth attempt and OTP send with per-step status.
  • Dashboard → Webhooks → Deliveries: response codes and bodies for each attempt, with replay.
  • Your org's contact email (Settings): a webhook switched off, a failed or lost domain verification, a test site near its cap, or a rotated secret still in use arrives there within minutes; a demotion or a disabled webhook also reaches every owner on WhatsApp.
  • Support: include your client_id, the X-Request-Id, and a timestamp — never a user's phone number.
Troubleshooting · Okia docs