Documentation / API Reference / v1
Protocol specificationAPI referenceGuides
Documentation / API Reference / v1
STABLE · SINCE V1

API Reference

Endpoints, parameters, response codes, and examples for integrating the Praticable ticket verification protocol.

Protocol specification

This page covers practical API usage. To understand how the protocol works (cryptography, payload format, verification flow), see the protocol specification →

§ 01

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).

Endpoint Description
POST /api/v1/scanners/enroll

Exchanges an enrollment token for an API key

POST /api/v1/tickets/check-in

Marks a ticket as used at the gate

GET /api/v1/tickets/{tid}/status

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.

§ 02

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.

Authentication header
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_at timestamp returned during enrollment.
  • An invalid, expired, or revoked key returns 401 UNAUTHORIZED.
  • Keys MUST be stored in the platform's secure storage (Keychain, Keystore).
§ 03

Enroll scanner

POST /api/v1/scanners/enroll no Bearer required

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
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

Field Description
token
stringrequired

Enrollment token from the setup QR code. Opaque, ≥ 128 bits of entropy, single-use.

scanner_id
stringrequired

Unique, stable identifier generated by the scanner app (UUID recommended). Used for cross-enrollment identification and auditing.

scanner_name
stringoptional

Human-readable name displayed in the admin interface (e.g. device name).

scanner_platform
stringoptional

Platform information for support and compatibility tracking.

Response · 200 OK

JSON response
{
  "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"
  }
}
Field Description
api_key
string

Long-lived API key. Store in Keychain/Keystore. Never log, include in crash reports, or persist in plain text.

api_key_id
string

Key identifier, safe to log. Used for audit trails.

scope
object

Key scope: eid, event_name, event_date, gate_id (optional, null if all gates), expires_at.

issuer
object

Issuer information: name (business name) and iss (base URL).

Enrollment errors

Code Meaning
INVALID_TOKEN
400

The token is malformed, expired, or has already been used.

TOKEN_REVOKED
403

The token was revoked by the organizer before use.

PROTOCOL_VERSION_UNSUPPORTED
400

The issuer does not support the requested protocol version.

RATE_LIMITED
429

Too many enrollment attempts from this IP.

INTERNAL
500

Server error. Retry with exponential backoff.

§ 04

Check-in

POST /api/v1/tickets/check-in Bearer required

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
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"
  }'
Field Description
tid
stringrequired

Ticket identifier.

eid
stringrequired

Event identifier.

scanned_at
stringrequired

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:13Z
scanner_id
stringrequired

Unique, stable device identifier.

gate_id
stringoptional

Gate identifier, included for auditing purposes.

idempotency_key
stringrequired

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.

ADMITTED 200 · status: valid
{
  "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"
  }
}
REFUSED 200 · status: already_used
{
  "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

Status Meaning
valid
200

Valid ticket, check-in performed. First successful scan.

already_used
200

Ticket already used. The response includes the original checked_in_at.

revoked
200

Ticket revoked (refund, fraud, etc.).

wrong_event
200

Valid ticket but for a different event.

expired
200

The ticket's expiration date has passed.

invalid_signature
400

The scanner reports that the signature could not be verified. Logged for audit on the issuer side.

§ 05

Ticket status

GET /api/v1/tickets/{tid}/status Bearer required

Returns the current state of a ticket without modifying it. Useful for scanner pre-checks, admin tools, or post-event reconciliation.

curl
curl https://tickets.example.com/api/v1/tickets/01HXXX...ULID/status \
  -H "Authorization: Bearer sk_live_01HXXX..."
Response · 200 OK
{
  "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
}
Status Meaning
valid

Ticket issued, not used, not expired.

checked_in

Ticket already used. checked_in_at contains the timestamp.

revoked

Ticket revoked (refund, fraud).

expired

The expiration date has passed.

If the tid does not exist, the issuer returns 404 NOT_FOUND.

§ 06

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).

Same tid + same key

Retry of the same scan. The server returns the original result (idempotent).

Same tid + different key

Double-scan detected. The server returns already_used.

Same key + different body

Idempotency conflict. The server returns 409 IDEMPOTENCY_CONFLICT.

Offline mode

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.

§ 07

Error codes

All non-2xx responses use a standard error envelope:

Error envelope
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "API key is missing, expired, or revoked.",
    "details": {}
  }
}
Code Type Description
400 INVALID_REQUEST Malformed payload or missing required fields.
400 INVALID_TOKEN Enrollment token is malformed, expired, or already used.
400 PROTOCOL_VERSION_UNSUPPORTED The issuer does not support the payload's protocol version.
401 UNAUTHORIZED API key is missing, invalid, expired, or revoked.
403 FORBIDDEN Valid API key but without permission for this resource.
403 TOKEN_REVOKED Enrollment token revoked by the organizer before use.
404 NOT_FOUND The requested resource does not exist.
409 IDEMPOTENCY_CONFLICT Same idempotency key used with a different payload.
429 RATE_LIMITED Too many requests. The Retry-After header indicates the delay.
500 INTERNAL Server error. Retry with exponential backoff.
§ 08

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.

Last updated : 20 May 2026