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
/events/checkinJWT/events/verify-scanJWT/events/checkoutJWT/events/:eventId/occupancyJWT/events/:eventId/checkin-statsJWT/events/tickets/validateJWTScan 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
Resolve the ticket
The QR payload is matched against the ticket record. Bad/forged codes return
invalid_ticket. - 2
Check ticket state
Cancelled or refunded tickets return
ticket_not_active. Tickets for a different event returnwrong_event. - 3
Apply re-entry rules
If the ticket has already been scanned in, the ticket type's
allowReentryandreentryLimitdecide.reentryLimit: 3means three total entries (initial + 2 re-entries). - 4
Validate the scan point
If
checkpointis provided, the scan point'sacceptedTicketTypeswhitelist is checked. A general-admission ticket scanned at a VIP-only door returnsticket_type_not_accepted. - 5
Record and update
On success, append to
ticket.data.checkIns, increment zone occupancy counters, write acheck_in_logrecord, 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.
/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.