Documentation / Praticable Ticket Protocol (PTP) / v1
Protocol specificationAPI referenceGuides
Documentation / Praticable Ticket Protocol (PTP) / v1
STABLE · SINCE V1

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.

API reference

This page describes the protocol itself. For endpoints, parameters, response codes and request examples, see the API reference →

§ 01

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
Current version: 1

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.

§ 02

Terminology

Issuer

Deployment instance that creates and manages tickets. One issuer per client.

Scanner

Client application that validates tickets at the event entrance.

Ticket

Unique, signed credential granting access to a specific event.

Check-in

The act of marking a ticket as used at the entrance.

Payload

Signed data encoded in the ticket's QR code.

§ 03

Cryptography

Ed25519 RFC 8032 64 bytes · Web Crypto API

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.

§ 04

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.

Format
tkt1.<base64url(payload_json)>.<base64url(signature)>
Maximum size

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

Payload JSON
{
  "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
}
Field Description
v
integerrequired

Protocol major version (currently 1).

iss
stringrequired

Issuer base URL. Canonical form: HTTPS, no trailing slash. Used for discovery.

e.g. https://tickets.example.com
kid
stringrequired

Signing key identifier (first 8 hex characters of the SHA-256 hash of the public key).

tid
stringrequired

Ticket identifier (ULID recommended, unique within the issuer).

eid
stringrequired

Event identifier (ULID recommended, unique within the issuer).

ev
objectrequired

Event metadata for offline display: name (string), date (ISO 8601 UTC), venue (string). Informational only.

tt
stringoptional

Ticket type, for display purposes (e.g. "VIP", "Standard").

hn
stringoptional

Holder display name. Keep short; no PII beyond the display name.

iat
integerrequired

Unix timestamp of issuance (seconds).

exp
integerrequired

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.

§ 05

Issuer discovery

Each issuer MUST publish a discovery document at the following address:

GET {iss}/.well-known/ticket-issuer.json
Response · 200 OK
{
  "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]
}
  • keys MAY contain multiple entries during rotation. Scanners must accept any non-expired key whose kid matches the payload.
  • valid_until: null means 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.
§ 06

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.

Two-step exchange

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.

Setup QR format
tsetup1.<base64url(setup_json)>

The tsetup1. prefix distinguishes setup QR codes from ticket QR codes (tkt1.).

Setup JSON
{
  "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"
}
Field Description
v
integerrequired

Protocol major version (currently 1).

iss
stringrequired

Issuer base URL. The scanner MUST fetch the discovery document from {iss}/.well-known/ticket-issuer.json.

enroll_url
stringrequired

Exact URL for the enrollment exchange. MUST match the scanner_enroll in the discovery document.

token
stringrequired

Opaque, unpredictable enrollment token (≥ 128 bits of entropy). Single use.

eid
stringrequired

Event ID for which this setup QR enrolls the scanner.

event_name
stringoptional

Name displayed to the operator for confirmation. The official name comes from the issuer after enrollment.

gate_id
stringoptional

Gate identifier. If present, the API key is scoped to this gate.

expires_at
stringrequired

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_at timestamp returned in the enrollment response (typically end of event + margin). Expired keys are rejected with UNAUTHORIZED.
  • 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.
§ 07

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:

1. Signature

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.

2. Time

Validate the time bounds iat/exp. If expired: reject with expired.

3. Enrollment

Look up active enrollments by the (iss, eid) combination from the payload.

4. Match

If a matching enrollment exists: select the API key, verify gate compatibility, then call the check-in endpoint.

5. No match

If no enrollment matches: reject locally with not_enrolled. Display the event name from ev.name.

6. Unknown

If the iss is entirely unknown to the scanner: reject with unknown_issuer.

§ 08

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.

Recommended local cache

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.

§ 09

Offline mode

Scanners SHOULD be able to operate with intermittent connectivity. Here is the processing order for each scan:

1

Verify the signature locally with the cached public keys for the issuer in iss.

2

Validate the time bounds iat/exp.

3

Look up the matching enrollment for (iss, eid) in local storage. If none: reject locally.

4

Check the local cache for previous scans of this tid. If found: reject as duplicate.

5

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.

6

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.

§ 10

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 exp is in the past at scan time, and SHOULD warn on implausibly future iat timestamps.
  • Issuer URL validation: the iss field 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_key prevents accidental replay. Intentional replay with a new key will be detected by the server-side tid state and will return already_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_enroll endpoint MUST be rate-limited per IP and per token.
  • Mandatory TLS: scanners MUST reject any setup QR whose iss or enroll_url is 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).
§ 11

Versioning and evolution

  • The v field in payloads and the protocol_version field 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).
Last updated : 20 May 2026