Cerrtify

BankID — QR code

Show BankID's animated QR code. Cerrtify gives you the QR data for every second; you only draw it.

When the user opens BankID on another device (usually their phone), you show a QR code. BankID requires an animated QR: its content changes every second, and a screenshot of an old code stops working. You don't need to compute anything — Cerrtify hands you the current QR data.

In short

Start the order, then call GET /service/bankid/qr-code?orderRef=… about once a second and draw the returned qrData as a QR code. Stop when /collect says complete or failed. The QR endpoint is free — only the BankID order itself is billed.

How the pieces fit

Browser  ──every 1 s──▶  your backend  ──▶  GET /service/bankid/qr-code   (Cerrtify)
Browser  ◀── qrData ───  your backend
Browser  ──every 2 s──▶  your backend  ──▶  POST /service/bankid/collect  (Cerrtify)

Your API key never goes to the browser. The browser asks your backend; your backend calls Cerrtify with the key.

1. Start the order

POST /service/bankid/auth (sign in), POST /service/bankid/sign (sign) or POST /service/bankid/payment (payment). The response holds the orderRef:

{
  "orderRef": "131daac9-16c6-4618-beb0-365768f37288",
  "autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfe651f9a7c6",
  "qrStartToken": "67df3917-fa0d-44e5-b327-edcc928297f8",
  "qrStartSecret": "d28db9a7-4cde-429e-a983-359be676944c"
}

2. Get the QR data every second

curl "https://sandbox-api.cerrtify.com/service/bankid/qr-code?orderRef=131daac9-16c6-4618-beb0-365768f37288" \
  -H "X-API-Key: cert_..."
{ "qrData": "bankid.67df3917-fa0d-44e5-b327-edcc928297f8.4.dee5cd3f9b..." }

Put qrData into any QR library as plain text — for example qrcode (npm), qrcode.react, or segno / qrcode (Python). Replace the image every second.

// Backend (Node/Express): the browser calls GET /bankid/qr?orderRef=… every second
app.get("/bankid/qr", async (req, res) => {
  const r = await fetch(
    `https://sandbox-api.cerrtify.com/service/bankid/qr-code?orderRef=${encodeURIComponent(req.query.orderRef)}`,
    { headers: { "X-API-Key": process.env.CERRTIFY_API_KEY } },
  );
  res.status(r.status).json(await r.json()); // { qrData: "bankid.…" }
});

3. Stop

Poll POST /service/bankid/collect every 2 seconds (auth flow). Stop the QR loop when the status is complete or failed, when the user has scanned (hintCode userSign), or after about 5 minutes — BankID orders and QR sessions expire.

Errors

ResponseMeaning
400 missing_orderRefNo orderRef in the query string.
400 no_active_qr_sessionThe order is unknown, older than 5 minutes, belongs to another organisation, or was started with ?async=true (see below). Start a new order.
429Rate limit. One QR call per second per open order is fine; count it in when you set the key's limits.

Computing the QR yourself (optional)

For orders started with ?async=true, or if you'd rather not make one call per second, compute the data from the start response. Keep qrStartSecret on your server.

qrData = "bankid." + qrStartToken + "." + seconds + "." + HMAC-SHA256(qrStartSecret, seconds) in lowercase hex, where seconds is the whole number of seconds since the order started (0, 1, 2 …).

import { createHmac } from "node:crypto";
const seconds = Math.floor((Date.now() - startedAt) / 1000);
const qrData = `bankid.${qrStartToken}.${seconds}.${createHmac("sha256", qrStartSecret).update(String(seconds)).digest("hex")}`;
import hmac, hashlib, time
seconds = int(time.time() - started_at)
qr_data = f"bankid.{qr_start_token}.{seconds}." + hmac.new(qr_start_secret.encode(), str(seconds).encode(), hashlib.sha256).hexdigest()

Same device instead of QR

If the user is already on the phone with BankID, skip the QR and open the app with the autoStartToken:

bankid:///?autostarttoken=<autoStartToken>&redirect=<your return URL, URL-encoded>

Keep polling /collect when the user comes back.

On this page