1 Get your Client ID and Secret
Every site that uses Sign in with ZapQR needs its own credentials. They're free and self-serve:
Go to auth.zapqr.ai/account and sign in — the first time, that just means entering your email and clicking the magic link we send you. Once you're in, add a passkey so future sign-ins are one touch.
Under Your sites, choose your platform (WordPress, Shopify, or Custom), enter your site's URL, and click Add site. You'll get a Client ID and a Client Secret.
For WordPress sites, the redirect URL is derived automatically from your site URL — you don't need to configure it. For other platforms, paste the redirect (callback) URL your plugin shows you.
2 WordPress
Install ZapQR Login from the WordPress plugin directory (version 1.1.0 or later) — in your admin, Plugins → Add New, search "ZapQR Login", install and activate.
Then in Settings → ZapQR Login:
- Paste your Client ID and Client Secret from step 1.
- Leave the Identity Provider as
https://auth.zapqr.ai. - Optionally enable Link Existing Users so people who already have a WordPress account with the same (verified) email get connected to it instead of a new account.
That's it — your login page now shows a Sign in with ZapQR button. New visitors are provisioned with your default role; signing out of WordPress also signs them out of ZapQR for that site.
Requires WordPress 5.8+ and HTTPS on your site.
3 Drupal
Install the OpenID Connect / OAuth client module, then add a Generic OAuth 2.0 client:
- Client ID / Client secret: from step 1 — when adding your site at auth.zapqr.ai, choose Custom, paste the redirect URL Drupal shows you (it looks like
https://yoursite.com/openid-connect/<name>), and set the token auth method to client_secret_post. - Authorization endpoint:
https://auth.zapqr.ai/auth - Token endpoint:
https://auth.zapqr.ai/token - UserInfo endpoint:
https://auth.zapqr.ai/me - Scopes:
openid email
Enable the client, and the OpenID Connect login option appears on your Drupal login form.
4 Any OpenID Connect platform
ZapQR is a standard OpenID Connect provider — Ghost, Discourse, Grafana, Nextcloud, and most SaaS admin panels can consume it with their OIDC / SSO plugin. Most tools only need the discovery URL:
| Setting | Value |
|---|---|
| Issuer | https://auth.zapqr.ai |
| Authorization | https://auth.zapqr.ai/auth |
| Token | https://auth.zapqr.ai/token |
| UserInfo | https://auth.zapqr.ai/me |
| JWKS | https://auth.zapqr.ai/jwks |
| End session (logout) | https://auth.zapqr.ai/session/end |
| Scopes | openid email profile |
| Flow | Authorization code (PKCE S256 supported) |
| Token auth | client_secret_basic (default) or client_secret_post — selectable per site |
| Claims | sub, email, email_verified, name |
Refresh tokens and RP-initiated logout are supported. Every account's email is verified before your platform ever sees it, so email_verified is honest.
5 Scan-to-sign-in QR on your login page
Every Sign in with ZapQR integration already includes QR sign-in. There's nothing extra to install and no QR library to host. Your visitor clicks your button, taps Scan with your phone on the ZapQR page, scans the code, and taps the matching number on their phone. Your site gets a signed-in user and never handles a password.
1 · Your login page
They click your button.
2 · auth.zapqr.ai
A QR and a number appear.
3 · Their phone
They tap the number the screen shows.
4 · Back on your site
You receive their verified email.
They can scan with the phone's camera or the ZapQR app. The same page also offers passkeys, Approve on my phone (a push to a phone they've already set up), and an email link, so nobody gets stuck.
Put the button on your login page
WordPress or Drupal: done already. Steps 2 and 3 add the button to your login form, and QR sign-in comes with it.
Your own site, any framework: send the button to a route on your server that starts the sign-in, then handle the callback. Here's a complete Express 5 example using openid-client. Register the site as Custom in step 1 and paste https://yoursite.com/auth/zapqr/callback as the redirect URL.
import express from 'express';
import session from 'express-session';
import * as oidc from 'openid-client';
const SITE = 'https://yoursite.com';
const config = await oidc.discovery(
new URL('https://auth.zapqr.ai'),
process.env.ZAPQR_CLIENT_ID,
process.env.ZAPQR_CLIENT_SECRET,
);
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, sameSite: 'lax' },
}));
// Your "Sign in with ZapQR" button links here.
app.get('/auth/zapqr/start', async (req, res) => {
const verifier = oidc.randomPKCECodeVerifier();
const state = oidc.randomState();
req.session.zapqr = { verifier, state };
const url = oidc.buildAuthorizationUrl(config, {
redirect_uri: `${SITE}/auth/zapqr/callback`,
scope: 'openid email profile',
code_challenge: await oidc.calculatePKCECodeChallenge(verifier),
code_challenge_method: 'S256',
state,
});
res.redirect(url.href);
});
app.get('/auth/zapqr/callback', async (req, res) => {
const { verifier, state } = req.session.zapqr ?? {};
const tokens = await oidc.authorizationCodeGrant(
config,
new URL(req.originalUrl, SITE),
{ pkceCodeVerifier: verifier, expectedState: state },
);
const { sub, email } = tokens.claims();
delete req.session.zapqr;
// Find or create your user by `sub` (stable), not by email.
req.session.user = { sub, email, idToken: tokens.id_token };
res.redirect('/');
});
app.get('/logout', (req, res) => {
const idToken = req.session.user?.idToken;
req.session.destroy(() => {
res.redirect(oidc.buildEndSessionUrl(config, {
id_token_hint: idToken,
post_logout_redirect_uri: `${SITE}/`,
}).href);
});
});
Your site's root and /login are registered as sign-out return addresses automatically. Add any others in the Extra post-logout URL(s) field when you add the site.
Sites on Supabase Auth: add ZapQR as a custom OpenID Connect provider, and Supabase handles the exchange and creates an ordinary Supabase session.
- In step 1, choose Custom and paste your Supabase callback URL,
https://<project-ref>.supabase.co/auth/v1/callback. - In your Supabase project's auth provider settings, add a custom OpenID Connect provider with the identifier
zapqr, issuerhttps://auth.zapqr.ai, and your Client ID and Secret. Don't use the built-in Keycloak provider: it expects Keycloak's URL layout and won't read ZapQR's discovery document. - Add your site's address to Supabase's allowed redirect URLs.
- Wire the button:
// Sign in
await supabase.auth.signInWithOAuth({
provider: 'custom:zapqr',
options: { scopes: 'openid email profile', redirectTo: `${location.origin}/` },
});
// Sign out of your site, then offer to sign out of ZapQR too
await supabase.auth.signOut();
location.href = 'https://auth.zapqr.ai/session/end';
A button that looks right
Use any styling you like. This one matches the ZapQR mark and works on light or dark pages:
<a href="/auth/zapqr/start" style="display:inline-flex;align-items:center;gap:10px;
padding:12px 20px;border-radius:8px;background:#0E131F;color:#fff;
font:500 15px/1 system-ui,sans-serif;text-decoration:none">
<img src="https://zapqr.ai/favicon.svg" alt="" width="20" height="20"
style="border-radius:4px">
Sign in with ZapQR
</a>
Why the scan is safe to offer
- A photo of the QR is useless. The code in the QR only asks for approval. The secret that collects the session never leaves the browser that showed it.
- Number matching. The phone offers several numbers and the visitor must tap the one on the screen. One wrong tap cancels the code, so a QR relayed from somewhere else fails.
- Right account, or no sign-in. If the visitor typed an email on the screen, a phone signed in to a different ZapQR account is refused and offered a switch.
- Shared computers. Ticking I'm on a shared or public computer makes the session end by itself, with End and Extend controls on the visitor's phone.
- You never hold a password. Your site receives
sub, a verifiedemailand (optionally)name.
What your visitors experience
They click Sign in with ZapQR and land on auth.zapqr.ai. First-timers enter their email and click a magic link — that's the whole signup. Then they're offered a passkey, so every sign-in after that is Face ID or Touch ID, no typing.
They approve sharing their email with your site once, then return signed in. No password ever existed, so there's nothing for your database to leak and nothing for a phishing page to steal.
If the screen can't use a passkey — a TV, a kiosk, a shared computer — they tap Scan with your phone and approve on their phone instead (see step 5). Your integration gets this automatically.
Stuck, or on a platform we haven't documented?
If your platform speaks OIDC, we'll get you running. Shopify support for new customer accounts is in the works.