Documentation

Add Touchque 2FA to your own app (instead of Google Authenticator)

Let your customers approve sign-ins with the Touchque Authenticator app instead of typing Google Authenticator's 6-digit codes. Your password step stays as it is; the second step becomes an approval on the phone. This guide covers the whole path: API key, SDK install, linking the phone, the sign-in request, the result (webhook or status check) and short code.

In short: 1) Create an API key in the dashboard. 2) Install the SDK. 3) The user scans the QR on your settings page with Touchque Authenticator, once. 4) On every sign-in, after the password, call login.request: a push goes to the phone. 5) Get the result by webhook or with login.status, then open the session.

What changes for your users

TopicGoogle AuthenticatorTouchque Authenticator
The second stepThe user opens the app, reads a 6-digit code and types it in.A push arrives on the phone. The user checks it and taps Approve (Face ID or fingerprint).
What the user seesA code and your name. Nothing about this sign-in.Your brand, what is being approved (a sign-in or a payment with its details), the city and the browser.
Someone else triesA phishing page asks for the code and uses it at once.The push shows another city or browser; the user taps "This wasn't me". Risky requests ask for number matching or are blocked.
Phone without internetCodes work offline.An offline QR code or an offline time-based code, if you turn them on.
Lost phoneYou reset 2FA by hand.Recovery codes, a new phone moved with colleagues vouching, or a reset by you.

Your users install Touchque Authenticator on their phone (iPhone or Android) and link it once. Nothing changes in how they sign up or type their password.

Before you start

  • In the Touchque dashboard, create a key under Developers → API keys. You get an API key starting with tq_ and an API secret; the secret is shown only once.
  • Keep both on your server only (TQ_API_KEY, TQ_API_SECRET). Never put them in the browser or a mobile app: every request is HMAC-signed with the secret.
  • Pick one stable id per user: externalUsername (email, user id). Touchque keeps it only as a reference.

1. Install the SDK and set your keys

Pick your server language. Node.js 5.2.0, Python 5.2.0, PHP 5.2.0, Go 5.2.0, .NET 5.2.0, Ruby 5.2.0 and Java 5.2.0 are published. Without an SDK, the REST API works with any language: see Authentication for the request signature.

# Node.js
npm install @touchque/node
# Python
pip install touchque-authenticator
# PHP
composer require touchque-authenticator/php-sdk:^5.0
# Go
go get github.com/Touchque/touchque-go/v5
# .NET
dotnet add package Touchque.AspNetCore
# Ruby
bundle add touchque
# Java
implementation("com.touchque:touchque-spring-boot-starter:5.2.0")
# : not published yet

# .env on your server (never in the browser or the app)
TQ_API_KEY=tq_...
TQ_API_SECRET=...

2. Link the user's phone (once)

On your settings page ("Turn on 2FA"), create a secret for the user and show the QR image it comes with. The user opens Touchque Authenticator and scans it, the same way they would add an account to Google Authenticator. The secret is valid for 5 minutes; after that, create a new one. Show the 10 recovery codes once and ask the user to keep them: they are the way in when the phone is gone.

const { Touchque } = require('@touchque/node');
const tq = new Touchque(); // TQ_API_KEY / TQ_API_SECRET from the environment

// Settings page → "Turn on 2FA"
app.post('/2fa/enroll', requireSession, async (req, res) => {
  const { qrCodeDataUrl, recoveryCodes, expiresAt } = await tq.auth.generateSecret({
    externalUsername: req.user.email,
  });
  // Show the QR (<img src=qrCodeDataUrl>) and the recovery codes, once
  res.json({ qrCodeDataUrl, recoveryCodes, expiresAt });
});

// The page asks this until the phone is linked
app.get('/2fa/enroll/status', requireSession, async (req, res) => {
  const user = await tq.auth.getUser({ externalUsername: req.user.email });
  res.json({ linked: Boolean(user.used && user.deviceId) });
});
Once the user is linked, getUser answers with used: true and a deviceId. The dashboard's 2FA users page lists linked phones too.

3. Send the sign-in request after the password

When the password is right, do not open the session yet. Call login.request from your server: Touchque sends a push to the user's phone. Pass the user's IP address and user agent so the phone can show where the sign-in comes from. The request is valid for 30 seconds.

// After the password check passed:
const r = await tq.login.request({
  externalUsername: user.email,
  type: 'LOGIN',
  clientIp: req.ip,                 // shown on the phone as city, country
  userAgent: req.get('user-agent'), // shown as browser and device
});
// r = { requestId, expiresAt, challengeCode?, caseCode }
req.session.pending2fa = { requestId: r.requestId, user: user.email };
res.json({ waiting: true, number: r.challengeCode }); // show the number if there is one
When the answer has a challengeCode (higher risk, or you asked for requireNumberMatch), show the number on your page: the phone asks the user to pick the same number. That way users only approve the sign-in they started.

4. Get the result: webhook or status

Two ways, use either. Webhook (recommended): set your Callback URL under Developers → Webhooks; when the user approves or rejects, Touchque POSTs the result there, signed with your API secret (the Signing secret on that page is the same value). Always verify the x-signature header on the raw body before you trust it. Status check: ask login.status every 2 seconds, for up to 30 seconds. Open the session only for CONFIRMED (status) or SUCCESS (webhook), and only for the requestId and user you stored in step 3.

// Webhook: Developers → Webhooks → Callback URL = https://your.app/webhooks/touchque
app.post('/webhooks/touchque', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = tq.webhook.verify({ rawBody: req.body.toString('utf8'), signature: req.get('x-signature') });
  } catch {
    return res.sendStatus(403); // not from Touchque, changed, or too old
  }
  // event.status: 'SUCCESS' | 'REJECTED', event.requestId, event.externalUsername
  saveDecision(event.requestId, event.status); // the waiting browser reads it
  res.sendStatus(200);
});

// Or ask every 2 seconds while the browser waits:
const s = await tq.login.status(pending.requestId); // PENDING | CONFIRMED | REJECTED | EXPIRED
if (s.status === 'CONFIRMED' && s.externalUsername === pending.user) startSession(pending.user);
Your webhook address must be HTTPS (localhost is fine for local tests) and answer 2xx within 5 seconds; otherwise Touchque retries a few times. The same event can arrive twice: de-duplicate on jti or requestId.

5. Handle the other answers

Every answer carries a caseCode such as K7P-2QX. When a request is refused, show it to the user; your team looks it up under Logs → Look up a case in the dashboard and sees why.

AnswerWhat to do
200 with challengeCodeNumber matching: show the number on your page. The phone asks the user to pick it.
404The user has no linked phone yet. Send them to step 2, or accept a recovery code.
403 blockedThe risk engine refused the request. Show the caseCode: "Tell your support team: K7P-2QX".
403 device_keys_lostThe phone lost its keys (reinstalled app, restored phone). The user links the phone again.
423 frozenToo many rejected requests. Check who the user is, then reset the secret (reset also unfreezes).
429 rate_limitedMore than 5 pushes in 10 minutes for this user. Wait retryAfter seconds.
requiresPasskey: trueYour policy wants a passkey for this action, so no push was sent. See Passkeys.
REJECTEDThe user tapped Reject. rejectReason says whether they reported it ("This wasn't me").
EXPIRED30 seconds passed without an answer. Offer to send a new request.
const { TouchqueAPIError } = require('@touchque/node');

try {
  await tq.login.request({ externalUsername: user.email, type: 'LOGIN', clientIp: req.ip });
} catch (err) {
  if (!(err instanceof TouchqueAPIError)) throw err;
  if (err.status === 404) return res.json({ enroll: true });            // no linked phone
  if (err.caseCode) return res.status(err.status).json({                 // blocked, frozen, rate_limited
    message: `Tell your support team: ${err.caseCode}`,
  });
  throw err;
}

Moving users off Google Authenticator

  • Google Authenticator secrets cannot be imported into Touchque: each user links their phone once with the QR from step 2.
  • Keep both methods during the move: a push for users with a linked phone, the old code for the rest. Offer "Approve with Touchque" at their next sign-in.
  • Once a user's phone is linked (used: true), delete their old TOTP secret, so no phishable code is left.
  • For users whose phone may be offline, turn on offline sign under App user policy in the dashboard.

Shortcuts: one-line middleware and ready-made screens

The steps above are the raw calls. The SDKs also wrap them: requireTouchque('LOGIN') (Express), @require_touchque (Flask, Django, FastAPI), ->middleware('touchque:LOGIN') (Laravel) and touchque.Require (Go) answer 202 until the phone approves. On the page, @touchque/web and @touchque/react give you the enrolment QR, the waiting screen with the number and the passkey step. Switch the sidebar to SDKs for these guides.

Before going live

  • API key and secret on the server only; requests over HTTPS.
  • The webhook signature is verified; a session opens only for the requestId you started.
  • Recovery codes are shown at enrolment and you have a path for a lost phone (see Troubleshooting).
  • Refused requests show the caseCode to the user.
  • Linking, approving, rejecting and expiry were tried with a real phone.