Open Site Pass
An open QR code standard for signing in to work sites. Any app can show a pass, and any kiosk can read one.
Version 1, November 2026. Free to use under CC BY 4.0.
Contractors sign in at a different site every week, and each one asks for the same details: name, company, mobile, vehicle, Site Safe card. Open Site Pass lets them carry those details in an app and hand them over by showing a QR code. Any app can show a pass, and any sign-in kiosk can read one, whoever made them.
Before writing this we looked for an existing standard. Visitor sign-in products that use QR passes each have their own format that only works inside that product. The general identity standards (W3C Verifiable Credentials, OpenID for Verifiable Presentations, ISO 18013-5 mobile driving licences) need a wallet app and a multi-step exchange, which is more than a tablet at a site gate needs. So this is a small format that any language can read and check with its built-in crypto.
1. What's in the QR code
The QR code holds plain text in three parts joined by dots:
OSP1.<pass>.<signature> OSP1says it's an Open Site Pass, version 1.<pass>is the pass as JSON, UTF-8, then base64url without padding.<signature>is an ECDSA P-256 signature with SHA-256 over the ASCII text of<pass>(exactly as it appears in the QR code), as the 64-byte r ‖ s form, then base64url without padding. This is the form Web Crypto'scrypto.subtle.signproduces.
2. The pass
{
"version": 1,
"issuer": "https://timeclock.kiwi",
"keyId": "2026-11-3fa9c1",
"holderId": "b7f3c2e1a94d0c55e1f3b2a8",
"issuedAt": "2026-11-01T09:42:00.000Z",
"expiresAt": "2026-11-01T09:43:00.000Z",
"firstName": "Sam",
"lastName": "Taylor",
"company": "Harbour Electrical Ltd",
"phone": "+64215550142",
"vehicleRegistration": "QKT482",
"documents": [
{ "type": "site-safe-passport", "number": "SS-482913", "expiresOn": "2027-03-31" }
]
} | version | Always 1 for this version. |
|---|---|
| issuer | The HTTPS origin of whoever signed the pass, e.g. https://timeclock.kiwi. Its keys are found from this. |
| keyId | Which of the issuer's keys signed it. |
| holderId | The same for one person with one issuer, and different with every other issuer. Lets a site recognise a returning visitor. It must not be an employee number, email or anything else that identifies them elsewhere. |
| issuedAt, expiresAt | ISO 8601 times. A pass should last about a minute and must not last more than 15 minutes, so a photo of one is soon useless. |
| firstName, lastName | Their name. |
| company | Who they work for, if they chose to share it. |
| phone | Mobile number, ideally in international form. |
| Email address. | |
| vehicleRegistration | Number plate of the vehicle they came in. |
| documents | Cards and certificates: each has type (from the list below), and optionally number, issuer and expiresOn (YYYY-MM-DD). |
Required: version, issuer, keyId, holderId, issuedAt, expiresAt, firstName. Everything else is optional and chosen by the holder each time.
Document types
site-safe-passport | Site Safe passport |
|---|---|
public-liability-insurance | Public liability insurance |
electrical-registration | Electrical registration |
plumbing-gasfitting-registration | Plumbing, gasfitting or drainlaying registration |
forklift-licence | Forklift licence (F endorsement) |
first-aid-certificate | First aid certificate |
working-at-heights | Working at heights |
confined-spaces | Confined spaces |
construction-induction-card | Construction induction card (White Card) |
cscs-card | CSCS card |
To propose another type, get in touch below. Kiosks keep documents with types they don't know, but don't match them to anything.
3. Issuer keys
Issuers publish their public keys at <issuer>/.well-known/open-site-pass/keys.json:
{
"keys": [
{
"keyId": "2026-11-3fa9c1",
"publicKey": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" }
}
]
} Each publicKey is a JWK, which Web Crypto can load directly. Keep publishing a key for at least a
day after you stop signing with it. Timeclock.Kiwi's keys are at https://timeclock.kiwi/.well-known/open-site-pass/keys.json.
4. Rules for kiosks
- Check the signature with the issuer's key, and that the pass hasn't expired. Allow up to 2 minutes either side for clock differences.
- Only fetch keys from the issuer's own HTTPS origin, and cache them so a pass can be checked offline.
- Show the details and the issuer's domain, and let the visitor confirm before anything is saved.
- Treat document details as what the holder entered, not as checked. If your site needs it checked, have a person look at the real card.
- You may only accept passes from issuers you trust, or accept any HTTPS issuer and show "Verified by <domain>".
- Never ask the holder's app for more than the pass carries. The holder chooses what to share.
5. Rules for apps
- Make a new pass at least every minute while it's on screen.
- Let the holder choose which optional details go in, every time or as a saved choice they can change.
- Never put in anything the holder didn't choose, including their employer, location or internal ids.
6. Checking a pass in JavaScript
This is all a kiosk needs, using only the browser's built-in crypto:
async function readPass(text, fetchKeys) {
const [prefix, body, signature] = text.split('.');
if (prefix !== 'OSP1') throw new Error('Not a site pass');
const pass = JSON.parse(atob(body.replace(/-/g, '+').replace(/_/g, '/')));
const keys = await fetchKeys(pass.issuer); // GET <issuer>/.well-known/open-site-pass/keys.json
const jwk = keys.find(k => k.keyId === pass.keyId)?.publicKey;
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
const fromB64 = s => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
const ok = await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, fromB64(signature), new TextEncoder().encode(body));
const now = Date.now(), slack = 2 * 60 * 1000;
if (!ok || Date.parse(pass.expiresAt) + slack < now) throw new Error('Not valid');
return pass;
} Using it with Timeclock.Kiwi
Anyone signed in to the Timeclock.Kiwi app can show a pass from My Site Pass, and every Timeclock.Kiwi site sign-in kiosk reads them, including passes from other apps that follow this format.