Troubleshooting
The push did not arrive, what a case code (K7P-2QX) means, a user changed or lost their phone, the QR does not link, the webhook does not come: the causes and what to do.
The push didn't arrive
Go through these in order. Most of the time the first one is enough.
- Open Touchque Authenticator. When the app connects, it gets every request that is still pending and not expired, so the request shows up in the app even without the notification. A request lives 30 seconds; after that, send a new one.
- Notification permission. Notifications must be on for Touchque Authenticator in the phone's settings; Focus or Do Not Disturb can hide them.
- Internet. A push needs the phone online. Without internet, use offline sign: Offline sign.
- Was the request sent at all? Look at the
login.requestanswer.404: the user has no linked phone, so no push went out.403 blocked: the risk engine refused it (look up the case code).429 rate_limited: the limit of 5 pushes per user in 10 minutes is reached.423 frozen: 2FA is frozen after many rejections. The dashboard's Logs page shows every request and how it ended. - The right phone? The push goes to the phone the user linked. If the app was reinstalled or the phone restored from a backup, its keys are gone (
403 device_keys_lost): the user links the phone again.
Case codes (K7P-2QX): what they are and how to look one up
Every login.request answer and every refusal (403 blocked, 403 device_keys_lost, 423 frozen, 429 rate_limited) carries a caseCode: 6 characters such as K7P-2QX. It names the decision Touchque made for that request. The SDKs put it on the error too (err.caseCode).
- Show the code on your refusal screen: "Tell your support team:
K7P-2QX". - Your team opens Logs in the Touchque dashboard and types the code into the Look up a case field at the top.
- The decision opens: the reasons, each check that ran, the rule that matched and what would have let the request through.
Number-matching and passkey answers also carry plain reasons ([{ code, text }]) you can show to the user as they are.
New phone or lost phone
The user has a new phone and still has the old one
Nothing to do on your side. On the old phone, the user opens Touchque Authenticator and starts the transfer ("move your accounts to the new phone"), scans the transfer QR with the new phone and types the number the new phone shows. The selected accounts move, signed by the old phone, and are removed from it. The next push goes to the new phone.
The phone is lost, broken or stolen
Offer these in order, from the safest:
- A recovery code. One of the 10 codes you showed at enrolment approves a pending sign-in request. Critical actions and sign-ins that need a passkey or the phone cannot be approved with a recovery code. After signing in, the user links their new phone.
- A passkey. If the user registered a passkey on your site, they can sign in with it: Passkeys.
- Moving to a new phone with colleagues vouching (Never Locked Out). After the password step,
recovery.startgives you a QR that the new phone scans. At least two colleagues the user named approve on their own phones, a cooling-off period passes (the old phone and the vouchers can stop it meanwhile) and the account moves to the new phone. Critical actions stay closed for the next 24 hours. - A reset by you. The last resort. Check strongly who the user is, your own way (fraudsters aim at exactly this step), then reset the secret: the old phone can no longer approve, you get a new QR and new recovery codes, and a frozen 2FA opens again.
// The user chose "I can't reach my phone" and typed a recovery code;
// pending.requestId is the sign-in request you sent in step 3.
await tq.login.approveWithRecoveryCode({ requestId: pending.requestId, code: typedCode });tq.login.approve_with_recovery_code(pending['request_id'], typed_code)$tq->login->approveWithRecoveryCode($pending['request_id'], $typedCode);_, err := tq.Login.ApproveWithRecoveryCode(pending.RequestID, typedCode)await tq.Login.ApproveWithRecoveryCodeAsync(pendingRequestId, typedCode);tq.login.approve_with_recovery_code(session[:pending_2fa]["request_id"], typed_code)tq.login().approveWithRecoveryCode(pendingRequestId, typedCode);// After the password step, the user chose "Move me to my new phone":
const r = await tq.recovery.start({ externalUsername: user.email, clientIp: req.ip, displayName: user.name });
showQr(r.bindUri); // Touchque Authenticator on the new phone scans it
// While the user waits:
const now = await tq.recovery.status(r.recoveryId);
// AWAITING_DEVICE → VOUCHING → COOLING → COMPLETED (or VETOED, EXPIRED, FAILED)// Only after you have checked who the user is, your own way:
const { qrCodeDataUrl, recoveryCodes, expiresAt } = await tq.auth.resetSecret({ externalUsername: user.email });
// The old phone can no longer approve. Show the new QR and the new recovery codes.reset = tq.auth.reset_secret(user.email) # reset['qrCodeDataUrl'], reset['recoveryCodes']$reset = $tq->auth->resetSecret($user->email); // $reset['qrCodeDataUrl'], $reset['recoveryCodes']reset, err := tq.Auth.ResetSecret(user.Email) // reset.QRCodeDataURL, reset.RecoveryCodesvar reset = await tq.Auth.ResetSecretAsync(user.Email); // reset.QrCodeDataUrl, reset.RecoveryCodesreset = tq.auth.reset_secret(user.email) # reset["qrCodeDataUrl"], reset["recoveryCodes"]JsonNode reset = tq.auth().resetSecret(user.getEmail()); // reset.path("qrCodeDataUrl"), reset.path("recoveryCodes")The QR code doesn't link the phone
- A secret is valid for 5 minutes. An expired QR does not work (
410): create a new secret. - A secret works once (
409). If a phone is already linked, reset first instead of creating another secret. - Scan the QR from inside Touchque Authenticator, not with the phone's camera app.
- Check the link with
getUser: once linked it hasused: trueand adeviceId.
The webhook doesn't arrive or its signature fails
- The Callback URL must be set under Developers → Webhooks and be HTTPS (localhost is fine for local tests).
- Answer 2xx within 5 seconds; otherwise Touchque retries a few times. Do the heavy work after answering.
- Verify the signature over the raw body: do not parse the JSON and write it again. In Express, use
express.rawon that route. - The signature is made with this integration's API secret; the Signing secret under Developers → Webhooks is the same value. If you rotated the key, update the secret on your server too.
- The same event can arrive twice: de-duplicate on
jtiorrequestId. If the webhook never comes, you can carry on with status checks (login.status).
API calls answer 401 or 403
401: something is missing or stale; the answer'serrornames it. Nox-api-keyheader, orSIGNATURE_REQUIRED,TIMESTAMP_REQUIRED,TIMESTAMP_EXPIRED(x-timestampis more than 5 minutes off the server's clock: sync it with NTP),NONCE_REQUIRED,NONCE_INVALID(the nonce was used before or is shorter than 16 characters). A 401 never means a wrong signature.403 SIGNATURE_INVALID: the signature did not match. Use the right secret (the new one if you rotated the key); the signed path is exactly the one you call, with its query string; the body is byte for byte the one you send; GET requests are signed with an empty body. The SDKs do all of this for you. See Authentication.403for the key itself:Invalid API key(no such key),Credential has been revoked,credential_expired(rotate it in the dashboard),credential_org_access_revoked(the person who made the key is no longer in the organization),insufficient_scope(the key has no scope for this area).403 IP not allowed: if you use an IP allowlist, your server's outgoing address must be on it. Other codes: Error codes.
Still stuck? Write to support@touchque.com with the case code or the requestId.