API · Scanner
Scanner JSON endpoints are the contract for door apps and the scanner PWA. Use them to identify the scanner account, preload event snapshots before doors open, search tickets, redeem tickets online, and sync queued offline scans.
Native apps, scanner devices, and the in-browser PWA all use /scanner/... URLs. Request JSON with Accept: application/json or a .json suffix when using these endpoints from a scanner app or integration.
Authentication
Every scanner JSON request must include a Bearer token and request JSON:
curl -H "Authorization: Bearer your-token-here" \
-H "Accept: application/json" \
https://app.usetix.io/scanner/me
Scanner JSON endpoints accept two token types:
| Token type | Where it comes from | Access |
|---|---|---|
| Scanner device token | Settings → Scanner devices | Scanner-only credential for a paired device. Can read scanner data, redeem tickets, and sync queued scans. |
| Account API token | Settings → API Tokens | Read tokens can call GET endpoints. Read + Write tokens can also redeem and sync. |
Bearer credentials are authoritative for JSON scanner requests. If a request includes an invalid, revoked, or read-only bearer token for a write endpoint, Usetix returns 401 Unauthorized even if the same HTTP client also has browser cookies.
Treat scanner device tokens like passwords. They are shown once during pairing, can be revoked from the dashboard, and should be stored in the native app’s secure storage.
Privacy boundary
Scanner payloads are operational, not full order exports. They include the fields staff need at the door:
- public ticket ID
- check-in code and formatted check-in code
- event and ticket labels
- ticket color
- buyer name
- redeemable/redeemed state
- blocked reason, if any
redeemed_atupdated_at
They do not include buyer email, phone number, billing fields, attribution data, or custom checkout answers. Ticket search can match buyer email so staff can find a guest who only knows their email address, but the email is not returned in the response.
GET /scanner/me
Returns the account and authenticated scanner credential. Native apps can call this after pairing to verify the token and display the connected account/device.
curl -H "Authorization: Bearer your-scanner-device-token" \
-H "Accept: application/json" \
https://app.usetix.io/scanner/me
Scanner device response:
{
"account": {
"id": 42,
"name": "Usetix Club",
"subdomain": "usetix-club"
},
"authentication": {
"type": "scanner_device",
"name": "Door 1 iPhone"
}
}
API token response:
{
"account": {
"id": 42,
"name": "Usetix Club",
"subdomain": "usetix-club"
},
"authentication": {
"type": "api_token",
"description": "Read-write integration",
"permission": "write"
}
}
GET /scanner/events
Returns scannable events for the token’s account. Events are ordered by start date, nearest not-ended event first.
curl -H "Authorization: Bearer your-token-here" \
-H "Accept: application/json" \
https://app.usetix.io/scanner/events
Response:
{
"events": [
{
"slug": "spring-showcase",
"title": "Spring Showcase",
"starts_at": "2026-05-01T19:00:00Z",
"ends_at": "2026-05-01T23:00:00Z",
"updated_at": "2026-05-01T12:00:00Z",
"snapshot_url": "https://app.usetix.io/scanner/events/spring-showcase/snapshot.json",
"image_url": "https://app.usetix.io/rails/active_storage/representations/...",
"venue": {
"name": "The Venue",
"city": "Berlin"
}
}
]
}
GET /scanner/events/:event_slug/snapshot
Returns an operational cache for one event. Use this before doors open to preload the native scanner with every ticket needed for offline checks.
Snapshots include blocked tickets too, so the scanner can explain why a ticket cannot be redeemed even when offline.
curl -H "Authorization: Bearer your-token-here" \
-H "Accept: application/json" \
https://app.usetix.io/scanner/events/spring-showcase/snapshot
Response:
{
"generated_at": "2026-05-01T18:00:00Z",
"event": {
"slug": "spring-showcase",
"title": "Spring Showcase",
"starts_at": "2026-05-01T19:00:00Z",
"ends_at": "2026-05-01T23:00:00Z",
"updated_at": "2026-05-01T12:00:00Z",
"snapshot_url": "https://app.usetix.io/scanner/events/spring-showcase/snapshot.json",
"image_url": "https://app.usetix.io/rails/active_storage/representations/...",
"venue": {
"name": "The Venue",
"city": "Berlin"
}
},
"tickets": [
{
"public_id": "abc123",
"check_in_code": "7K3Q9D2A",
"display_check_in_code": "7K3Q-9D2A",
"event_slug": "spring-showcase",
"event_title": "Spring Showcase",
"ticket_title": "General Admission",
"ticket_color": "#3B82F6",
"buyer_name": "Alex Buyer",
"redeemed": false,
"redeemable": true,
"blocked_reason": null,
"redeemed_at": null,
"updated_at": "2026-05-01T12:00:00Z"
}
]
}
GET /scanner/events/:event_slug/tickets
Returns the current valid ticket cache for one event. Unlike snapshots, this list excludes refunded or otherwise invalid orders. Use query to search within the event by check-in code, ticket public ID, buyer name, or buyer email.
curl -H "Authorization: Bearer your-token-here" \
-H "Accept: application/json" \
https://app.usetix.io/scanner/events/spring-showcase/tickets
Query parameters:
| Parameter | Description |
|---|---|
query |
Optional search string. Blank returns the event’s valid ticket cache. |
Response:
{
"event": {
"slug": "spring-showcase",
"title": "Spring Showcase",
"starts_at": "2026-05-01T19:00:00Z",
"ends_at": "2026-05-01T23:00:00Z",
"updated_at": "2026-05-01T12:00:00Z",
"snapshot_url": "https://app.usetix.io/scanner/events/spring-showcase/snapshot.json",
"image_url": "https://app.usetix.io/rails/active_storage/representations/...",
"venue": {
"name": "The Venue",
"city": "Berlin"
}
},
"tickets": [
{
"public_id": "abc123",
"check_in_code": "7K3Q9D2A",
"display_check_in_code": "7K3Q-9D2A",
"event_slug": "spring-showcase",
"event_title": "Spring Showcase",
"ticket_title": "General Admission",
"ticket_color": "#3B82F6",
"buyer_name": "Alex Buyer",
"redeemed": false,
"redeemable": true,
"blocked_reason": null,
"redeemed_at": null,
"updated_at": "2026-05-01T12:00:00Z"
}
]
}
The unscoped GET /scanner/tickets?query=alex@example.com endpoint returns the same event/tickets envelope with "event": null. Without either an event slug or a query, it returns an empty tickets array.
GET /scanner/search
Searches scanner tickets for the token’s account. Use event_slug to limit results to one event.
curl -G \
-H "Authorization: Bearer your-token-here" \
-H "Accept: application/json" \
--data-urlencode "event_slug=spring-showcase" \
--data-urlencode "q=alex@example.com" \
https://app.usetix.io/scanner/search
Query parameters:
| Parameter | Description |
|---|---|
q |
Search string. Matches ticket check-in code, ticket public ID, buyer name, or buyer email. Email is used only for matching and is not returned. Blank queries return an empty tickets array. |
event_slug |
Optional event slug. When present, only tickets for that event are returned. |
Exact check-in code and public ID matches can return blocked tickets. Fallback text search returns currently valid tickets.
Response:
{
"event": {
"slug": "spring-showcase",
"title": "Spring Showcase",
"starts_at": "2026-05-01T19:00:00Z",
"ends_at": "2026-05-01T23:00:00Z",
"updated_at": "2026-05-01T12:00:00Z",
"snapshot_url": "https://app.usetix.io/scanner/events/spring-showcase/snapshot.json",
"image_url": "https://app.usetix.io/rails/active_storage/representations/...",
"venue": {
"name": "The Venue",
"city": "Berlin"
}
},
"tickets": [
{
"public_id": "abc123",
"check_in_code": "7K3Q9D2A",
"display_check_in_code": "7K3Q-9D2A",
"event_slug": "spring-showcase",
"event_title": "Spring Showcase",
"ticket_title": "General Admission",
"ticket_color": "#3B82F6",
"buyer_name": "Alex Buyer",
"redeemed": false,
"redeemable": true,
"blocked_reason": null,
"redeemed_at": null,
"updated_at": "2026-05-01T12:00:00Z"
}
]
}
When event_slug is omitted, event is null.
GET /scanner/tickets/:identifier
Returns one scanner ticket by public ticket ID or check-in code.
curl -H "Authorization: Bearer your-token-here" \
-H "Accept: application/json" \
https://app.usetix.io/scanner/tickets/7K3Q-9D2A
Response: the scanner ticket object described in Scanner ticket fields.
POST /scanner/events/:event_slug/tickets/:identifier/redemption
Redeems one ticket online. Redemption is authoritative and atomic on the server; if another scanner redeemed the ticket first, the response returns the current ticket state and an error.
The :identifier segment accepts either the ticket public_id or its check-in code. Responses always include the canonical public_id; native apps should keep using that value for queued offline sync payloads.
The unscoped POST /scanner/tickets/:identifier/redemption endpoint has the same request and response shape when you do not need event scoping.
Scanner device tokens and Read + Write API tokens can redeem. Read API tokens receive 401 Unauthorized.
curl -X POST \
-H "Authorization: Bearer your-token-here" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
https://app.usetix.io/scanner/events/spring-showcase/tickets/7K3Q-9D2A/redemption
Success response:
{
"public_id": "abc123",
"check_in_code": "7K3Q9D2A",
"display_check_in_code": "7K3Q-9D2A",
"event_slug": "spring-showcase",
"event_title": "Spring Showcase",
"ticket_title": "General Admission",
"ticket_color": "#3B82F6",
"buyer_name": "Alex Buyer",
"redeemed": true,
"redeemable": false,
"blocked_reason": "Already redeemed",
"redeemed_at": "2026-05-01T19:35:12Z",
"updated_at": "2026-05-01T19:35:12Z"
}
Validation failures return 422 Unprocessable Entity with an error string and the current ticket object:
{
"error": "Already redeemed",
"ticket": {
"public_id": "abc123",
"check_in_code": "7K3Q9D2A",
"display_check_in_code": "7K3Q-9D2A",
"event_slug": "spring-showcase",
"event_title": "Spring Showcase",
"ticket_title": "General Admission",
"ticket_color": "#3B82F6",
"buyer_name": "Alex Buyer",
"redeemed": true,
"redeemable": false,
"blocked_reason": "Already redeemed",
"redeemed_at": "2026-05-01T19:35:12Z",
"updated_at": "2026-05-01T19:35:12Z"
}
}
POST /scanner/redemption_attempts
Syncs queued scans from an offline scanner. The server processes attempts in request order. If two queued attempts redeem the same ticket, the first accepted sync wins and later attempts return conflict.
Attempts are idempotent per actor and client_id, so retrying the same batch is safe. The native app should generate a stable unique client_id for each scan attempt and reuse it on retry.
Scanner device tokens and Read + Write API tokens can sync. Read API tokens receive 401 Unauthorized.
curl -X POST \
-H "Authorization: Bearer your-token-here" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"attempts": [
{
"client_id": "device-uuid:scan-001",
"event_slug": "spring-showcase",
"public_id": "abc123",
"scanned_at": "2026-05-01T19:35:12Z"
}
]
}' \
https://app.usetix.io/scanner/redemption_attempts
Response:
{
"redemption_attempts": [
{
"client_id": "device-uuid:scan-001",
"event_slug": "spring-showcase",
"public_id": "abc123",
"result": "accepted",
"message": "Ticket has been redeemed.",
"scanned_at": "2026-05-01T19:35:12Z",
"synced_at": "2026-05-01T19:40:00Z",
"accepted_at": "2026-05-01T19:40:00Z",
"ticket": {
"public_id": "abc123",
"check_in_code": "7K3Q9D2A",
"display_check_in_code": "7K3Q-9D2A",
"event_slug": "spring-showcase",
"event_title": "Spring Showcase",
"ticket_title": "General Admission",
"ticket_color": "#3B82F6",
"buyer_name": "Alex Buyer",
"redeemed": true,
"redeemable": false,
"blocked_reason": "Already redeemed",
"redeemed_at": "2026-05-01T19:40:00Z",
"updated_at": "2026-05-01T19:40:00Z"
}
}
]
}
Attempt payload fields:
| Field | Required | Notes |
|---|---|---|
client_id |
yes | Stable client-generated ID for idempotency. Reuse the same value when retrying the same scan. |
event_slug |
yes | Event slug from the event list or snapshot. |
public_id |
yes | Canonical ticket public ID. Use the public_id returned by scanner ticket responses when syncing queued offline scans. |
scanned_at |
no | ISO 8601 timestamp from the device. Invalid or missing values fall back to server time. |
Attempt results:
| Result | Meaning |
|---|---|
accepted |
Ticket was redeemed by this attempt. |
conflict |
Ticket was already redeemed, usually by an earlier online or synced attempt. |
blocked |
Ticket exists, but redemption is blocked because the order is archived, refunded, cancelled, disputed, or unpaid. |
not_found |
The event or ticket could not be found for the authenticated account. |
Scanner ticket fields
| Field | Type | Notes |
|---|---|---|
public_id |
string | Public ticket ID used in QR codes and scanner URLs. |
check_in_code |
string | Human-readable ticket code. Safe to show to staff and customers. |
display_check_in_code |
string | Formatted check-in code for UI, typically grouped as XXXX-XXXX. |
event_slug |
string | null | Event slug for event-scoped lookup. |
event_title |
string | Event title shown to staff. |
ticket_title |
string | Ticket type or group ticket label. |
ticket_color |
string | null | Hex color used by scanner rows and ticket PDFs. |
buyer_name |
string | Buyer display name. |
redeemed |
boolean | Whether this ticket has already been redeemed. |
redeemable |
boolean | Whether the server currently allows redemption. |
blocked_reason |
string | null | Human-readable reason when redeemable is false. |
redeemed_at |
string | null | ISO 8601 UTC timestamp of redemption. |
updated_at |
string | ISO 8601 UTC timestamp of the ticket row. |
Scanner event fields
| Field | Type | Notes |
|---|---|---|
slug |
string | Event slug used in scanner URLs. |
title |
string | Event title. |
starts_at |
string | ISO 8601 UTC event start. |
ends_at |
string | ISO 8601 UTC event end. |
updated_at |
string | ISO 8601 UTC timestamp of the event row. |
snapshot_url |
string | Absolute URL for the event snapshot endpoint. |
image_url |
string | null | Absolute URL to the event image thumbnail, or null when no displayable image is attached. |
venue.name |
string | null | Venue display name. |
venue.city |
string | null | Venue city. |
Error responses
| Status code | When you’ll see it |
|---|---|
401 Unauthorized |
Token is missing, malformed, invalid, revoked, or not allowed for the requested method. |
404 Not Found |
Event or ticket does not exist for the authenticated account. |
422 Unprocessable Entity |
Online redemption was blocked. The response includes an error string and current ticket object. |