Home Features Pricing Blog Docs
Log in Start for Free
English | Deutsch

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_at
  • updated_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.