API Reference
Endpoints, parameters, response codes, and examples for integrating the Praticable ticket verification protocol.
This page covers practical API usage. To understand how the protocol works (cryptography, payload format, verification flow), see the protocol specification →
Overview
The API exposes three endpoints for scanner applications, served by each issuer instance.
The base URL is the client deployment's iss (e.g.
https://tickets.example.com).
Exchanges an enrollment token for an API key
Marks a ticket as used at the gate
Returns the current state of a ticket without modifying it
A public discovery endpoint is also available at
{iss}/.well-known/ticket-issuer.json (no authentication required).
Its structure is described in the
protocol specification.
Authentication
The check-in and status endpoints require a Bearer token.
The API key is obtained through the enrollment exchange (below). The enrollment endpoint
itself does not require a Bearer; it uses the enrollment token as a credential.
Authorization: Bearer sk_live_01HXXX...long-opaque-key - Each API key is scoped to an event (and optionally a gate).
- Keys expire at the
expires_attimestamp returned during enrollment. - An invalid, expired, or revoked key returns
401 UNAUTHORIZED. - Keys MUST be stored in the platform's secure storage (Keychain, Keystore).
Enroll scanner
Exchanges an enrollment token (from a setup QR code) for a long-lived API key.
The token is single-use: a second attempt with the same token returns
INVALID_TOKEN.
curl -X POST https://tickets.example.com/api/v1/scanners/enroll \
-H "Content-Type: application/json" \
-d '{
"token": "st_01HXXX...opaque-enrollment-token",
"scanner_id": "550e8400-e29b-41d4-a716-446655440000",
"scanner_name": "iPhone 14 - North Gate",
"scanner_platform": "ios-18.2"
}' Request parameters
Enrollment token from the setup QR code. Opaque, ≥ 128 bits of entropy, single-use.
Unique, stable identifier generated by the scanner app (UUID recommended). Used for cross-enrollment identification and auditing.
Human-readable name displayed in the admin interface (e.g. device name).
Platform information for support and compatibility tracking.
Response · 200 OK
{
"api_key": "sk_live_01HXXX...long-opaque-key",
"api_key_id": "sck_01HXXX...ULID",
"scope": {
"eid": "evt_01HXXX...ULID",
"event_name": "Gala Concert",
"event_date": "2026-06-15T20:00:00Z",
"gate_id": "north",
"expires_at": "2026-06-16T02:00:00Z"
},
"issuer": {
"name": "Festival des Arts",
"iss": "https://tickets.example.com"
}
} Long-lived API key. Store in Keychain/Keystore. Never log, include in crash reports, or persist in plain text.
Key identifier, safe to log. Used for audit trails.
Key scope: eid, event_name, event_date, gate_id (optional, null if all gates), expires_at.
Issuer information: name (business name) and iss (base URL).
Enrollment errors
The token is malformed, expired, or has already been used.
The token was revoked by the organizer before use.
The issuer does not support the requested protocol version.
Too many enrollment attempts from this IP.
Server error. Retry with exponential backoff.
Check-in
Marks a ticket as used. Idempotent: replaying the same request
with the same idempotency_key returns the same result.
A second request with the same tid but a different idempotency_key
is treated as a double-scan and returns already_used.
Request
curl -X POST https://tickets.example.com/api/v1/tickets/check-in \
-H "Authorization: Bearer sk_live_01HXXX..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"tid": "01HXXX...ULID",
"eid": "evt_01HXXX...ULID",
"scanned_at": "2026-06-15T19:42:13Z",
"scanner_id": "gate-north-01",
"gate_id": "north",
"idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}' Ticket identifier.
Event identifier.
Scan timestamp (ISO 8601 UTC). The issuer MUST accept timestamps up to 48 hours in the past for queued offline scans.
e.g. 2026-06-15T19:42:13ZUnique, stable device identifier.
Gate identifier, included for auditing purposes.
UUID v4 per scan attempt. Deduplicates retries. Can also be passed via the Idempotency-Key header.
Response · 200 OK
The response is always 200 with a status field indicating
the outcome. This allows scanners to distinguish between different cases without parsing
HTTP status codes.
{
"status": "valid",
"ticket": {
"tid": "01HXXX...ULID",
"eid": "evt_01HXXX...ULID",
"holder_name": "J. Smith",
"ticket_type": "VIP",
"checked_in_at": "2026-06-15T19:42:13Z"
}
} {
"status": "already_used",
"ticket": {
"tid": "01HXXX...ULID",
"eid": "evt_01HXXX...ULID",
"holder_name": "J. Smith",
"ticket_type": "VIP",
"checked_in_at": "2026-06-15T19:30:00Z"
}
} Status values
Valid ticket, check-in performed. First successful scan.
Ticket already used. The response includes the original checked_in_at.
Ticket revoked (refund, fraud, etc.).
Valid ticket but for a different event.
The ticket's expiration date has passed.
The scanner reports that the signature could not be verified. Logged for audit on the issuer side.
Ticket status
Returns the current state of a ticket without modifying it. Useful for scanner pre-checks, admin tools, or post-event reconciliation.
curl https://tickets.example.com/api/v1/tickets/01HXXX...ULID/status \
-H "Authorization: Bearer sk_live_01HXXX..." {
"tid": "01HXXX...ULID",
"eid": "evt_01HXXX...ULID",
"status": "valid",
"holder_name": "J. Smith",
"ticket_type": "VIP",
"issued_at": "2026-05-01T10:00:00Z",
"checked_in_at": null
} Ticket issued, not used, not expired.
Ticket already used. checked_in_at contains the timestamp.
Ticket revoked (refund, fraud).
The expiration date has passed.
If the tid does not exist, the issuer returns 404 NOT_FOUND.
Idempotency
The check-in endpoint uses an idempotency key to deduplicate retries.
Generate a unique UUID v4 per scan attempt and pass it either in the body
(idempotency_key) or in the HTTP header (Idempotency-Key).
Retry of the same scan. The server returns the original result (idempotent).
Double-scan detected. The server returns already_used.
Idempotency conflict. The server returns 409 IDEMPOTENCY_CONFLICT.
On network failure, the scanner queues the request with its
idempotency_key and optimistically accepts the ticket. When
connectivity returns, the queued requests are flushed. Issuer-side idempotency
guarantees no inconsistencies. See the
offline mode specification.
Error codes
All non-2xx responses use a standard error envelope:
{
"error": {
"code": "UNAUTHORIZED",
"message": "API key is missing, expired, or revoked.",
"details": {}
}
} Retry-After header indicates the delay. Rate limiting
Issuers MUST rate-limit each endpoint per scanner credential.
When the limit is reached, the server returns 429 RATE_LIMITED
with a Retry-After header indicating the number of seconds to wait.
- Enrollment endpoint: rate-limited by IP and by token to prevent brute-force.
- Check-in endpoint: rate-limited by scanner credential.
- Status endpoint: rate-limited by scanner credential.
Scanners SHOULD implement exponential backoff on 429 responses
and respect the Retry-After value.