Spécification du protocole
Protocole ouvert de vérification des billets émis par des instances indépendantes. Chaque déploiement client agit comme un issuer autonome ; n'importe quel scanner implémentant ce protocole peut vérifier les billets de n'importe quel issuer, sans intégration bilatérale préalable.
Cette page décrit le protocole lui-même. Pour les endpoints, paramètres, codes de réponse et exemples de requêtes, consultez la référence API →
Vue d'ensemble
Ce protocole définit comment un scanner valide un billet émis par un issuer indépendant. Les issuers ne se coordonnent pas entre eux et n'ont pas besoin d'une autorité centrale.
Le protocole est conçu pour quatre objectifs :
- Vérification hors ligne de l'authenticité des billets via signatures cryptographiques
- État de check-in en ligne avec cohérence forte pour empêcher le double-scan
- Indépendance des issuers : les issuers ne se coordonnent pas entre eux
- Rotation des clés sans invalider les billets déjà émis
Tous les payloads et endpoints incluent un identifiant de version. Les changements cassants requièrent une nouvelle version majeure. Issuers et scanners peuvent supporter plusieurs versions simultanément pendant les transitions.
Terminologie
Instance de déploiement qui crée et gère les billets. Un issuer par client.
Application cliente qui valide les billets à l'entrée de l'événement.
Credential unique et signé donnant accès à un événement spécifique.
Acte de marquer un billet comme utilisé à l'entrée.
Données signées encodées dans le QR code du billet.
Cryptographie
L'algorithme de signature est Ed25519 (RFC 8032). Les signatures de 64 octets sont suffisamment compactes pour tenir dans un QR code aux côtés des données du payload, et la vérification est rapide avec un support natif dans la Web Crypto API.
Cycle de vie des clés
Chaque issuer génère une paire de clés au provisioning. La clé privée est stockée dans les secrets du Worker et ne quitte jamais l'issuer. La clé publique est publiée au endpoint well-known. Plusieurs clés publiques actives peuvent coexister pendant les fenêtres de rotation.
Identifiant de clé (kid)
Chaque clé publique possède un identifiant court et unique : les 8 premiers caractères
hexadécimaux du SHA-256 de la clé publique brute. Les payloads référencent le
kid pour que les scanners sachent quelle clé utiliser pour la vérification.
Format du payload QR
Le QR code contient un payload JSON compact encodé en base64url, suivi de la signature.
Le préfixe tkt1. identifie la version du protocole et distingue les QR codes
de billets des autres codes qu'un scanner pourrait rencontrer.
tkt1.<base64url(payload_json)>.<base64url(signature)>
Le contenu total du QR doit rester sous ~400 caractères pour rester scannable
sur des caméras bas de gamme en faible luminosité. Évitez de remplir inutilement
les champs ev et hn.
Structure JSON du payload
{
"v": 1,
"iss": "https://tickets.example.com",
"kid": "a1b2c3d4",
"tid": "01HXXX...ULID",
"eid": "evt_01HXXX...ULID",
"ev": {
"name": "Gala Concert",
"date": "2026-06-15T20:00:00Z",
"venue": "Royal Hall"
},
"tt": "VIP",
"hn": "J. Smith",
"iat": 1745000000,
"exp": 1765000000
} Version majeure du protocole (actuellement 1).
URL de base de l'issuer. Forme canonique : HTTPS, sans slash final. Utilisé pour la découverte.
ex : https://tickets.example.comIdentifiant de la clé de signature (8 premiers hex de SHA-256 de la clé publique).
Identifiant du billet (ULID recommandé, unique au sein de l'issuer).
Identifiant de l'événement (ULID recommandé, unique au sein de l'issuer).
Métadonnées de l'événement pour affichage hors ligne : name (string), date (ISO 8601 UTC), venue (string). Informatif uniquement.
Type de billet, pour affichage (ex : "VIP", "Standard").
Nom d'affichage du porteur. Garder court ; pas de PII au-delà du nom d'affichage.
Timestamp Unix d'émission (secondes).
Timestamp Unix d'expiration (secondes). Généralement fixé à un délai raisonnable après la fin de l'événement.
Signature
La signature est calculée sur la séquence exacte d'octets du payload encodé en base64url
(avant le second séparateur .), en utilisant la clé privée Ed25519 de l'issuer.
Les scanners vérifient en recalculant la signature sur les mêmes octets.
Issuer discovery
Chaque issuer DOIT publier un document de découverte à l'adresse suivante :
{
"protocol_version": 1,
"issuer": "https://tickets.example.com",
"name": "Nom commercial du client",
"keys": [
{
"kid": "a1b2c3d4",
"alg": "Ed25519",
"public_key": "base64url-encoded-raw-32-byte-public-key",
"valid_from": "2026-01-01T00:00:00Z",
"valid_until": null
}
],
"endpoints": {
"check_in": "https://tickets.example.com/api/v1/tickets/check-in",
"status": "https://tickets.example.com/api/v1/tickets/{tid}/status",
"scanner_enroll": "https://tickets.example.com/api/v1/scanners/enroll"
},
"supported_versions": [1]
} keysPEUT contenir plusieurs entrées pendant la rotation. Les scanners doivent accepter toute clé non expirée dont lekidcorrespond au payload.valid_until: nullsignifie actuellement active sans expiration programmée.- Les scanners DEVRAIENT mettre ce document en cache avec un TTL d'au plus 1 heure, et DEVRAIENT le re-récupérer si la vérification échoue avec un
kidinconnu.
QR de setup du scanner
Les scanners s'enrôlent dans un événement en scannant un QR code de setup à durée limitée généré par l'interface admin de l'organisateur. Le QR de setup contient un token d'enrollment que le scanner échange, à la première utilisation, contre une clé API longue durée scopée à un ou plusieurs événements.
Le flux d'enrollment utilise un échange token → clé API plutôt que d'embarquer directement une clé API dans le QR. Le QR de setup est ainsi à usage unique et à durée limitée ; un QR photografié ou divulgué devient inutile une fois consommé.
tsetup1.<base64url(setup_json)>
Le préfixe tsetup1. distingue les QR de setup des QR de billet (tkt1.).
{
"v": 1,
"iss": "https://tickets.example.com",
"enroll_url": "https://tickets.example.com/api/v1/scanners/enroll",
"token": "st_01HXXX...opaque-enrollment-token",
"eid": "evt_01HXXX...ULID",
"event_name": "Gala Concert",
"gate_id": "north",
"expires_at": "2026-06-16T02:00:00Z"
} Version majeure du protocole (actuellement 1).
URL de base de l'issuer. Le scanner DOIT récupérer le document de découverte depuis {iss}/.well-known/ticket-issuer.json.
URL exacte pour l'échange d'enrollment. DOIT correspondre au scanner_enroll du document de découverte.
Token d'enrollment opaque et imprévisible (≥ 128 bits d'entropie). Usage unique.
ID de l'événement pour lequel ce QR de setup enrôle le scanner.
Nom affiché à l'opérateur pour confirmation. Le nom officiel vient de l'issuer après enrollment.
Identifiant de porte. Si présent, la clé API est scopée à cette porte.
Timestamp ISO 8601 UTC après lequel le token est invalide.
Capacités du scanner
Un scanner PEUT détenir des enrollments actifs pour plusieurs événements simultanément. Cela supporte les lieux multi-scènes, les événements consécutifs et les festivals. Chaque enrollment ajoute un événement à l'ensemble actif du scanner sans remplacer les enrollments précédents.
Cycle de vie de la clé API
- Scope : chaque clé API est liée à exactement un événement (et optionnellement une porte). Un scanner inscrit pour plusieurs événements détiendra plusieurs clés API.
- Expiration : les clés API expirent au timestamp
expires_atretourné dans la réponse d'enrollment (généralement fin de l'événement + marge). Les clés expirées sont rejetées avecUNAUTHORIZED. - Révocation : les organisateurs peuvent révoquer une clé à tout moment depuis l'interface admin.
- Pas de refresh : si une clé API expire en cours d'événement, le scanner doit se ré-enroller via un nouveau QR de setup.
- Rotation : pour remplacer une clé compromise, l'organisateur révoque l'ancienne clé et émet un nouveau QR de setup.
Flux de vérification
Quand un billet est scanné, le scanner DOIT auto-détecter l'événement correspondant
en lisant le champ eid du payload vérifié. L'opérateur ne sélectionne pas
manuellement un événement. Voici l'ordre de résolution :
Parser le payload QR et vérifier la signature avec les clés publiques en cache de l'issuer dans iss. Si la signature ne vérifie pas : rejeter avec invalid_signature.
Valider les bornes temporelles iat/exp. Si expiré : rejeter avec expired.
Chercher les enrollments actifs par la combinaison (iss, eid) du payload.
Si un enrollment correspondant existe : sélectionner la clé API, vérifier la compatibilité de porte, puis appeler l'endpoint de check-in.
Si aucun enrollment ne correspond : rejeter localement avec not_enrolled. Afficher le nom de l'événement depuis ev.name.
Si l'iss est totalement inconnu du scanner : rejeter avec unknown_issuer.
Cohérence et double-scan
La prévention du double-scan est appliquée par l'issuer via un Durable Object scopé par événement. Toutes les écritures de check-in pour un événement donné sont sérialisées à travers ce DO, garantissant qu'exactement un check-in réussit même sous requêtes concurrentes depuis plusieurs portes.
Les scanners DEVRAIENT maintenir un cache local des tid récemment scannés
(dernières 24h) et rejeter les doublons côté client avant de solliciter le réseau.
Cela réduit la charge et permet le rejet instantané des doublons évidents même hors ligne.
Mode hors ligne
Les scanners DEVRAIENT pouvoir fonctionner avec une connectivité intermittente. Voici l'ordre de traitement pour chaque scan :
Vérifier la signature localement avec les clés publiques en cache pour l'issuer dans iss.
Valider les bornes temporelles iat/exp.
Chercher l'enrollment correspondant pour (iss, eid) dans le stockage local. Si aucun : rejeter localement.
Vérifier le cache local des scans antérieurs de ce tid. Si trouvé : rejeter comme doublon.
Tenter l'appel API de check-in. Sur succès : accepter et enregistrer localement. Sur échec réseau : mettre en file d'attente avec l'idempotency_key, accepter de manière optimiste, retenter en arrière-plan.
Quand la connectivité revient, vider la file. L'idempotence de l'issuer garantit l'absence d'incohérence.
Les issuers DOIVENT accepter des requêtes de check-in avec des timestamps scanned_at
dans le passé (jusqu'à une fenêtre raisonnable, ex : 48h) pour supporter les scans
hors ligne en file d'attente.
Sécurité
- Vérification de signature obligatoire avant toute confiance dans le contenu du payload. Ne jamais afficher les noms des porteurs ou les infos d'événement sans vérifier la signature d'abord.
- Validation temporelle : les scanners DOIVENT rejeter les billets dont
expest dans le passé au moment du scan, et DEVRAIENT avertir sur des timestampsiatdans le futur de manière invraisemblable. - Validation de l'URL issuer : le champ
issest une URL. Les scanners DOIVENT imposer HTTPS et DEVRAIENT restreindre à une liste de confiance de domaines issuers si le contexte l'exige. - Stockage des clés API : les applications scanner DOIVENT stocker les clés API dans le stockage sécurisé de la plateforme (Keychain, Keystore).
- Rate limiting : les issuers DOIVENT limiter le débit des endpoints de check-in par credential scanner pour prévenir les abus.
- TLS : tous les endpoints DOIVENT être servis en TLS 1.2+. Les redirections HTTP vers HTTPS ne suffisent pas pour les appels API.
- Protection contre le replay : l'
idempotency_keyprévient le replay accidentel. Le replay intentionnel avec une nouvelle clé sera détecté par l'état dutidcôté serveur et retourneraalready_used.
Sécurité de l'enrollment
- Confidentialité du QR de setup : le QR de setup est un bearer credential jusqu'à consommation. Il DEVRAIT être généré juste avant de distribuer les appareils au staff, affiché uniquement sur des écrans de confiance, et ne pas être photografié ou partagé par chat/email.
- Entropie du token : les tokens DOIVENT avoir au moins 128 bits d'entropie provenant d'un RNG cryptographiquement sécurisé.
- Rate limiting : l'endpoint
scanner_enrollDOIT être rate-limité par IP et par token. - TLS obligatoire : les scanners DOIVENT rejeter tout QR de setup dont
issouenroll_urln'est pas HTTPS. - Confirmation opérateur : le scanner DEVRAIT afficher le
event_name, la date et le nom de l'issuer, et demander confirmation explicite avant d'appeler l'endpoint d'enrollment. - Piste d'audit : les issuers DEVRAIENT logger chaque tentative d'enrollment (succès et échec).
Versioning et évolution
- Le champ
vdans les payloads et le champprotocol_versiondans le document de découverte portent la version majeure. - Les changements additifs mineurs (nouveaux champs optionnels) n'incrémentent pas la version majeure. Les scanners DOIVENT ignorer les champs inconnus.
- Les changements cassants incrémentent la version majeure. Les issuers DEVRAIENT continuer à accepter la version précédente pendant une période de transition raisonnable (ex : 6 mois).