Documentation

Check-in

QR scanning at the door — validate the ticket, record entry, track zone occupancy.

Check-in is how a ticket becomes an attendance. A scanner — phone, dedicated handheld, or kiosk — reads the QR code, calls POST /events/checkin, and either accepts entry or returns a denial reason. The service records the scan, updates zone occupancy, and applies re-entry rules from the ticket type.

Endpoints

POST/events/checkinJWT
POST/events/verify-scanJWT
POST/events/checkoutJWT
GET/events/:eventId/occupancyJWT
GET/events/:eventId/checkin-statsJWT
POST/events/tickets/validateJWT

Scan flow

const result = await fetch('/events/checkin', {
  method: 'POST',
  headers: {
    orgid: 'my-org',
    Authorization: `Bearer ${jwt}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    code: 'qr-payload-from-scanner',
    zone: 'venue',                  // optional — zone the scanner sits in
    checkpoint: 'main-door',        // scanPoint id from the event record
    sessionId: 'optional-session-id',
  }),
}).then(r => r.json());

// { success: true, checkIn: {...}, ticket: {...}, zoneOccupancy: { current: 412, capacity: 800 } }
// or
// { success: false, reason: 'Ticket already used. Re-entry not allowed.' }

The code is whatever the scanner produced from the QR. The service decodes it, looks up the ticket using the scan point's matchField (qrCode, credentialCode, or ticketId), and runs the validation chain:

  1. 1

    Resolve the ticket

    The QR payload is matched against the ticket record. Bad/forged codes return invalid_ticket.

  2. 2

    Check ticket state

    Cancelled or refunded tickets return ticket_not_active. Tickets for a different event return wrong_event.

  3. 3

    Apply re-entry rules

    If the ticket has already been scanned in, the ticket type's allowReentry and reentryLimit decide. reentryLimit: 3 means three total entries (initial + 2 re-entries).

  4. 4

    Validate the scan point

    If checkpoint is provided, the scan point's acceptedTicketTypes whitelist is checked. A general-admission ticket scanned at a VIP-only door returns ticket_type_not_accepted.

  5. 5

    Record and update

    On success, append to ticket.data.checkIns, increment zone occupancy counters, write a check_in_log record, and return the updated ticket + occupancy.

Verify without recording

For "show a green light before the scan goes through" UX, use POST /events/verify-scan. Same body shape as /checkin, but it does not write a check-in record — useful for self-service kiosks or pre-flight checks.

const check = await fetch('/events/verify-scan', {
  method: 'POST',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ code: qr, checkpoint: 'main-door' }),
}).then(r => r.json());

if (check.success) {
  // animation green, then call /checkin
}

Check-out

If your venue tracks exits (e.g., capped capacity, contact tracing), call POST /events/checkout:

await fetch('/events/checkout', {
  method: 'POST',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ ticketId, zone: 'venue' }),
});

This appends direction: 'out' to the ticket's check-in log and decrements the zone occupancy. Re-entries then check the same reentryLimit.

Live occupancy

const { zones } = await fetch(`/events/${eventId}/occupancy`, {
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
}).then(r => r.json());

// zones: { venue: { current: 412, capacity: 800 }, vip: { current: 38, capacity: 60 } }

The numbers are computed from the running check-in/check-out stream, not by scanning the ticket table — so a busy event stays responsive.

Stats

const stats = await fetch(`/events/${eventId}/checkin-stats?day=2026-09-12`, {
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
}).then(r => r.json());

// { totalScanned: 612, totalDenied: 14, deniedByReason: {...}, byHour: [...] }

Useful for ops dashboards during the event. The hourly breakdown drives the staffing model for the next day.

Ticket validate vs verify-scan

/events/tickets/validate takes a qrPayload and tells you if the ticket is valid in the abstract — no scan-point context. /events/verify-scan runs the full scan-point chain (ticket type acceptance, zone). Use validate for ticket-holder self-checks ("is my ticket still good?") and verify-scan for door staff.

Sessions

When sessionId is supplied, the check-in is recorded against the session (not just the event). Per-session attendance shows up in /events/:eventId/checkin-stats?sessionId=.... Use this to power "who attended the keynote" reports.