# Pilot ticket scanner API

This private pilot lets one approved third-party scanner admit an ATM ticket
and receive the attendee's Atmosphere DID for badge printing. The scanner does
not register an Atmosphere app and does not receive app service-auth keys. ATM
provides the endpoint and a short-lived bearer credential directly to the
approved scanner developer.

## Check in one scanned ticket

Send the value read from the QR code to the stable checkout origin for the
environment ATM gives you. Do not use a preview deployment URL.

```http
POST /api/pilot/tickets/check-in HTTP/1.1
Host: checkout.staging.atmosphere.money
Authorization: Bearer atm_tscan_v1_...
Content-Type: application/json

{"ticketToken":"https://tickets.staging.atmosphere.tickets/tickets/scan/tpass_..."}
```

`ticketToken` is the only accepted body field. It may be the complete hosted
scan URL or the raw opaque `tpass_...` value. Do not parse the QR or send an
event id, attendee DID, scanner DID, or list id; ATM binds those server-side.

A newly admitted ticket returns exactly:

```json
{
  "status": "checked-in",
  "eventId": "tevt_...",
  "ticketId": "tix_...",
  "did": "did:plc:...",
  "checkedInAt": "2026-08-20T18:30:00.000Z"
}
```

A repeat scan returns `already-checked-in` with the original timestamp. A valid
guest ticket returns `did: null`. Deduplicate badge-print jobs by `ticketId`,
not by DID: guest tickets have no DID, and a person may hold more than one
ticket.

An inactive, expired, or canceled-event ticket returns the same five fields
with `status: "not-valid"`, `did: null`, and `checkedInAt: null`. ATM does not
expose the invalidity reason. Credential and grant authority is checked first,
so a revoked scanner credential returns the generic credential `401` even when
the QR is malformed or belongs to another event.

Disabling the Tickets module for the event's control app does not strand
already-issued tickets: they remain admissible while this exact scanner grant
and credential remain active. ATM can independently revoke that scanner
authority.

## Errors and retries

Responses are private/no-store and include `Atm-Request-Id`. Preserve that id
when asking ATM to investigate, but never log the bearer or scanned ticket
token.

- `400 InvalidBody`, `404 TicketNotFound`, and `413 RequestTooLarge`: do not
  retry the unchanged request.
- `401 InvalidScannerCredential`: stop scanning and contact ATM. Do not retry.
- `429 RateLimited`: wait at least the response's `Retry-After` value.
- `500`, `503`, or a network timeout: the outcome is indeterminate. Retry the
  identical `ticketToken`; `503` includes a short `Retry-After` header.

For an indeterminate result, use bounded exponential backoff with full jitter:
wait a random delay between zero and `min(8 seconds, 1 second * 2^attempt)`,
never sooner than `Retry-After`, and stop after five total attempts or 30
seconds. Then show the scan as pending for manual retry. Print only after ATM
returns `checked-in` or `already-checked-in`; never infer success from sending
the request. A `503` can also represent a persistent inconsistency in that
ticket's order state rather than a transient outage. After the bounded retries
are exhausted, stop retrying and give ATM the `Atm-Request-Id` for operator
investigation.

## Credential handling

Keep the bearer in the scanner backend or secured device keystore. Never put it
in a browser bundle, public mobile bundle, source control, chat, a URL, support
text, analytics, or application logs. Do not send scanned ticket tokens to
telemetry. ATM staging and production use different credentials.

ATM operators configure, rotate, and revoke this private pilot separately; the
scanner integration needs only the contract above.
