Praticable Ticket Protocol (PTP)
Open protocol for verifying tickets issued by independent instances. Each client deployment acts as an autonomous issuer; any scanner implementing this protocol can verify tickets from any issuer, without prior bilateral integration.
This page describes the protocol itself. For endpoints, parameters, response codes and request examples, see the API reference →
Overview
This protocol defines how a scanner validates a ticket issued by an independent issuer. Issuers do not coordinate with each other and do not require a central authority.
The protocol is designed for four objectives:
- Offline verification of ticket authenticity via cryptographic signatures
- Online check-in state with strong consistency to prevent double-scanning
- Issuer independence: issuers do not coordinate with each other
- Key rotation without invalidating already-issued tickets
All payloads and endpoints include a version identifier. Breaking changes require a new major version. Issuers and scanners can support multiple versions simultaneously during transitions.
Terminology
Deployment instance that creates and manages tickets. One issuer per client.
Client application that validates tickets at the event entrance.
Unique, signed credential granting access to a specific event.
The act of marking a ticket as used at the entrance.
Signed data encoded in the ticket's QR code.
Cryptography
The signature algorithm is Ed25519 (RFC 8032). The 64-byte signatures are compact enough to fit in a QR code alongside the payload data, and verification is fast with native support in the Web Crypto API.
Key lifecycle
Each issuer generates a key pair at provisioning. The private key is stored in the Worker secrets and never leaves the issuer. The public key is published at the well-known endpoint. Multiple active public keys can coexist during rotation windows.
Key identifier (kid)
Each public key has a short, unique identifier: the first 8 hexadecimal characters
of the SHA-256 hash of the raw public key. Payloads reference the
kid so that scanners know which key to use for verification.
QR payload format
The QR code contains a compact JSON payload encoded in base64url, followed by the signature.
The tkt1. prefix identifies the protocol version and distinguishes ticket QR codes
from other codes a scanner might encounter.
tkt1.<base64url(payload_json)>.<base64url(signature)>
The total QR content should stay under ~400 characters to remain scannable
on low-end cameras in low-light conditions. Avoid unnecessarily filling
the ev and hn fields.
Payload JSON structure
{
"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
} Protocol major version (currently 1).
Issuer base URL. Canonical form: HTTPS, no trailing slash. Used for discovery.
e.g. https://tickets.example.comSigning key identifier (first 8 hex characters of the SHA-256 hash of the public key).
Ticket identifier (ULID recommended, unique within the issuer).
Event identifier (ULID recommended, unique within the issuer).
Event metadata for offline display: name (string), date (ISO 8601 UTC), venue (string). Informational only.
Ticket type, for display purposes (e.g. "VIP", "Standard").
Holder display name. Keep short; no PII beyond the display name.
Unix timestamp of issuance (seconds).
Unix expiration timestamp (seconds). Typically set to a reasonable delay after the event ends.
Signature
The signature is computed over the exact byte sequence of the base64url-encoded payload
(before the second . separator), using the issuer's Ed25519 private key.
Scanners verify by recomputing the signature over the same bytes.
Issuer discovery
Each issuer MUST publish a discovery document at the following address:
{
"protocol_version": 1,
"issuer": "https://tickets.example.com",
"name": "Client business name",
"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]
} keysMAY contain multiple entries during rotation. Scanners must accept any non-expired key whosekidmatches the payload.valid_until: nullmeans currently active with no scheduled expiration.- Scanners SHOULD cache this document with a TTL of at most 1 hour, and SHOULD re-fetch it if verification fails with an unknown
kid.
Scanner setup QR
Scanners enroll in an event by scanning a time-limited setup QR code generated by the organizer's admin interface. The setup QR contains an enrollment token that the scanner exchanges, on first use, for a long-lived API key scoped to one or more events.
The enrollment flow uses a token-to-API-key exchange rather than embedding an API key directly in the QR. This makes the setup QR single-use and time-limited; a photographed or leaked QR becomes useless once consumed.
tsetup1.<base64url(setup_json)>
The tsetup1. prefix distinguishes setup QR codes from ticket QR codes (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"
} Protocol major version (currently 1).
Issuer base URL. The scanner MUST fetch the discovery document from {iss}/.well-known/ticket-issuer.json.
Exact URL for the enrollment exchange. MUST match the scanner_enroll in the discovery document.
Opaque, unpredictable enrollment token (≥ 128 bits of entropy). Single use.
Event ID for which this setup QR enrolls the scanner.
Name displayed to the operator for confirmation. The official name comes from the issuer after enrollment.
Gate identifier. If present, the API key is scoped to this gate.
ISO 8601 UTC timestamp after which the token is invalid.
Scanner capabilities
A scanner MAY hold active enrollments for multiple events simultaneously. This supports multi-stage venues, back-to-back events, and festivals. Each enrollment adds an event to the scanner's active set without replacing previous enrollments.
API key lifecycle
- Scope: each API key is bound to exactly one event (and optionally one gate). A scanner enrolled for multiple events will hold multiple API keys.
- Expiration: API keys expire at the
expires_attimestamp returned in the enrollment response (typically end of event + margin). Expired keys are rejected withUNAUTHORIZED. - Revocation: organizers can revoke a key at any time from the admin interface.
- No refresh: if an API key expires during an event, the scanner must re-enroll via a new setup QR.
- Rotation: to replace a compromised key, the organizer revokes the old key and issues a new setup QR.
Verification flow
When a ticket is scanned, the scanner MUST auto-detect the corresponding event
by reading the eid field from the verified payload. The operator does not
manually select an event. Here is the resolution order:
Parse the QR payload and verify the signature with the cached public keys of the issuer in iss. If the signature does not verify: reject with invalid_signature.
Validate the time bounds iat/exp. If expired: reject with expired.
Look up active enrollments by the (iss, eid) combination from the payload.
If a matching enrollment exists: select the API key, verify gate compatibility, then call the check-in endpoint.
If no enrollment matches: reject locally with not_enrolled. Display the event name from ev.name.
If the iss is entirely unknown to the scanner: reject with unknown_issuer.
Consistency and double-scanning
Double-scan prevention is enforced by the issuer via a Durable Object scoped per event. All check-in writes for a given event are serialized through this DO, ensuring that exactly one check-in succeeds even under concurrent requests from multiple gates.
Scanners SHOULD maintain a local cache of recently scanned tid values
(last 24 hours) and reject duplicates client-side before hitting the network.
This reduces load and enables instant rejection of obvious duplicates even offline.
Offline mode
Scanners SHOULD be able to operate with intermittent connectivity. Here is the processing order for each scan:
Verify the signature locally with the cached public keys for the issuer in iss.
Validate the time bounds iat/exp.
Look up the matching enrollment for (iss, eid) in local storage. If none: reject locally.
Check the local cache for previous scans of this tid. If found: reject as duplicate.
Attempt the check-in API call. On success: accept and record locally. On network failure: queue with the idempotency_key, accept optimistically, retry in the background.
When connectivity returns, flush the queue. The issuer's idempotence guarantees no inconsistency.
Issuers MUST accept check-in requests with scanned_at timestamps
in the past (up to a reasonable window, e.g. 48h) to support queued
offline scans.
Security
- Mandatory signature verification before trusting any payload content. Never display holder names or event information without verifying the signature first.
- Time validation: scanners MUST reject tickets whose
expis in the past at scan time, and SHOULD warn on implausibly futureiattimestamps. - Issuer URL validation: the
issfield is a URL. Scanners MUST enforce HTTPS and SHOULD restrict to a trust list of issuer domains if the context requires it. - API key storage: scanner applications MUST store API keys in the platform's secure storage (Keychain, Keystore).
- Rate limiting: issuers MUST rate-limit check-in endpoints per scanner credential to prevent abuse.
- TLS: all endpoints MUST be served over TLS 1.2+. HTTP-to-HTTPS redirects are not sufficient for API calls.
- Replay protection: the
idempotency_keyprevents accidental replay. Intentional replay with a new key will be detected by the server-sidetidstate and will returnalready_used.
Enrollment security
- Setup QR confidentiality: the setup QR is a bearer credential until consumed. It SHOULD be generated just before distributing devices to staff, displayed only on trusted screens, and not photographed or shared via chat/email.
- Token entropy: tokens MUST have at least 128 bits of entropy from a cryptographically secure RNG.
- Rate limiting: the
scanner_enrollendpoint MUST be rate-limited per IP and per token. - Mandatory TLS: scanners MUST reject any setup QR whose
issorenroll_urlis not HTTPS. - Operator confirmation: the scanner SHOULD display the
event_name, date, and issuer name, and request explicit confirmation before calling the enrollment endpoint. - Audit trail: issuers SHOULD log every enrollment attempt (success and failure).
Versioning and evolution
- The
vfield in payloads and theprotocol_versionfield in the discovery document carry the major version. - Minor additive changes (new optional fields) do not increment the major version. Scanners MUST ignore unknown fields.
- Breaking changes increment the major version. Issuers SHOULD continue to accept the previous version during a reasonable transition period (e.g. 6 months).