Tickets
Scanly tickets are campaign-scoped admission records. A ticket can be created directly by an admin workflow or generated from a reusable ticket type and sent as a PDF attachment.
The backend owns ticket code generation. Raw ticket codes are returned only once when a ticket is issued or rotated. Persisted rows store only a hash of the raw code.
Core Concepts
| Concept | Meaning |
|---|---|
| Ticket type | A reusable campaign ticket category with styling, validity defaults, personalization settings, capacity metadata, and a default check-in limit. |
| Ticket | One issued admission record with status, serial number, validity window, check-in count, and check-in limit. |
| Public code | Opaque raw code rendered into the QR payload. It is returned only at create or rotate time. |
| Recipient | A persisted delivery snapshot for a generated ticket email/PDF. |
| Check-in | A successful ticket consumption event. |
| Scan attempt | Append-only history for validation and check-in attempts, including invalid scans. |
| Audit event | Append-only lifecycle history for ticket creation, check-in, revocation, PDF generation, and code rotation. |
Lifecycle
Ticket status values:
| Status | Meaning |
|---|---|
issued | The ticket exists and can be validated when its validity window allows it. |
checked_in | The ticket reached its configured check-in limit. |
revoked | The ticket was administratively invalidated. |
expired | The ticket is outside its validity window or has been marked expired by backend logic. |
Scanner result values:
| Result | Meaning |
|---|---|
valid | The ticket can be checked in for the selected campaign and optional event. |
checked_in | The current request successfully checked in the ticket. |
already_checked_in | The ticket has already reached a terminal checked-in state. |
limit_reached | The configured check-in limit has been reached. |
revoked | The ticket was revoked. |
expired | The ticket is expired. |
not_yet_valid | The ticket is before validFrom. |
wrong_campaign | The code belongs to another campaign. The response does not expose the ticket id. |
wrong_event | The selected event does not match the ticket event. |
invalid | The code cannot be resolved or trusted. |
Ticket Types
Ticket types are managed under:
/api/v1/campaigns/:campaignId/tickets/typesImportant fields:
| Field | Notes |
|---|---|
eventMetadataId | Optional event anchor. If omitted on create, the backend resolves the campaign-scope event metadata entry. |
ticketTypeStyles | Persisted PDF and preview styling. Missing values are normalized by the backend/shared style defaults. |
imageSrc, heroImageSrc | Optional ticket-specific images. If omitted, rendering can fall back to campaign images. |
isPersonalized | Requires recipient data before PDFs are generated for send flows. |
displayNameTemplate | Optional display name template for personalized tickets. |
recipientEmailPath | Optional path used by recipient mapping logic. |
capacity | Capacity metadata for overviews and planning. Treat hard capacity enforcement as backend behavior that should be verified for the current deployment. |
checkinLimit | Default successful check-ins allowed per ticket. Defaults to 1. |
validFrom, validUntil | Defaults copied to issued tickets. If absent, event dates may be used. |
isActive | Inactive types cannot be sent. |
Ticket type image upload endpoints:
PATCH /api/v1/campaigns/:campaignId/tickets/types/:ticketTypeId/image
PATCH /api/v1/campaigns/:campaignId/tickets/types/:ticketTypeId/hero-imageSample PDFs can be generated without issuing a ticket:
GET /api/v1/campaigns/:campaignId/tickets/types/:ticketTypeId/sample-pdfIssuing Tickets
Create a single ticket:
POST /api/v1/campaigns/:campaignId/ticketsExample payload:
{
"ticketTypeId": "0f90b5df-0c27-4bb4-a205-069bf078cc73",
"serialNumber": "VIP-1001",
"checkinLimit": 1
}Example response:
{
"ticket": {
"id": "7f672854-b356-4e18-8c4e-ff996fe395ad",
"campaignId": "campaign_123",
"ticketTypeId": "0f90b5df-0c27-4bb4-a205-069bf078cc73",
"status": "issued",
"serialNumber": "VIP-1001",
"checkinCount": 0,
"checkinLimit": 1
},
"publicCode": "scn_tkt_..."
}Keep publicCode private. It is the raw QR credential and is not returned by
normal list or detail endpoints.
Sending Ticket Types
Generate tickets for recipients and send PDF emails:
POST /api/v1/campaigns/:campaignId/tickets/types/:ticketTypeId/sendSupported delivery mode:
{
"deliveryMode": "pdf",
"mailTemplateId": "mail-template-id",
"themeId": "optional-theme-id",
"isPersonalized": false,
"recipient": {
"mode": "single",
"single": {
"email": "guest@example.com",
"firstName": "Ada",
"lastName": "Lovelace"
}
}
}Lead-list sends use recipient mappings:
{
"deliveryMode": "pdf",
"mailTemplateId": "mail-template-id",
"isPersonalized": true,
"recipient": {
"mode": "lead_list",
"leadListId": "lead-list-id",
"mappings": [
{ "sourcePath": "email", "targetPath": "email" },
{ "sourcePath": "first_name", "targetPath": "firstName" },
{ "sourcePath": "last_name", "targetPath": "lastName" }
]
}
}The send response summarizes created, sent, skipped, and failed recipients. Raw ticket codes are not exposed through the send response.
Send history endpoints:
GET /api/v1/campaigns/:campaignId/tickets/sends
GET /api/v1/campaigns/:campaignId/tickets/sends/:sendId/recipients
GET /api/v1/campaigns/:campaignId/tickets/types/:ticketTypeId/recipients
POST /api/v1/campaigns/:campaignId/tickets/types/:ticketTypeId/recipients/:recipientId/resendResending a recipient rotates the existing ticket code, generates a fresh PDF, and sends a new email.
PDFs
Generate a PDF for an existing ticket:
POST /api/v1/campaigns/:campaignId/tickets/:ticketId/pdfPayload:
{
"publicCode": "scn_tkt_..."
}The backend validates the raw publicCode against the stored hash before
rendering. Revoked tickets cannot be exported.
If TICKET_PUBLIC_SCAN_URL_BASE is configured, the QR code contains that URL
with the raw code in the code query parameter. Otherwise, the QR payload is
the raw ticket code itself.
Validation And Check-In
Authenticated campaign users can validate or check in tickets through:
POST /api/v1/campaigns/:campaignId/tickets/validate
POST /api/v1/campaigns/:campaignId/tickets/check-inPayload:
{
"code": "scn_tkt_...",
"eventMetadataId": "optional-event-id",
"scannerDeviceId": "optional-device-id",
"source": "scanner"
}check-in accepts the same payload plus an optional idempotencyKey:
{
"code": "scn_tkt_...",
"eventMetadataId": "optional-event-id",
"idempotencyKey": "scanner-request-123"
}Validation records a scan attempt. Successful check-in updates the ticket transactionally, writes a scan attempt, writes a check-in row, and appends a ticket audit event.
History endpoints:
GET /api/v1/campaigns/:campaignId/tickets/check-ins
GET /api/v1/campaigns/:campaignId/tickets/scan-attempts
GET /api/v1/campaigns/:campaignId/tickets/check-in/overviewCurrent External Scanner API Tokens
External scanner clients use organization API tokens with ticket policies.
Regular endpoints can also be called with API tokens when the request includes the normal session cookie and CSRF token required for write requests:
POST /api/v1/campaigns/:campaignId/tickets
POST /api/v1/campaigns/:campaignId/tickets/validate
POST /api/v1/campaigns/:campaignId/tickets/check-in
PATCH /api/v1/campaigns/:campaignId/tickets/:ticketId/revokeToken-only endpoints do not require a session cookie or CSRF token:
POST /api/v1/campaigns/:campaignId/tickets/api
POST /api/v1/campaigns/:campaignId/tickets/api/validate
POST /api/v1/campaigns/:campaignId/tickets/api/check-inAccepted credentials:
Authorization: Bearer scn_api_...Current ticketing token policies:
| Policy | Allows |
|---|---|
CREATE_TICKETS | Creating tickets through regular or token-only ticket endpoints. |
VALIDATE_TICKETS | Validating ticket codes through regular or token-only ticket endpoints. |
CHECK_IN_TICKETS | Checking in tickets through regular or token-only ticket endpoints. |
REVOKE_TICKETS | Revoking tickets through the regular revoke endpoint. |
Ticketing API tokens act on behalf of their owning user in the organization. Tokens can be organization-wide or restricted to one campaign. Campaign-scoped tokens are rejected when used against a different campaign.
Organization API-token management:
GET /api/v1/organizations/:organizationId/api-tokens
POST /api/v1/organizations/:organizationId/api-tokens
POST /api/v1/organizations/:organizationId/api-tokens/:apiTokenId/regenerate
DELETE /api/v1/organizations/:organizationId/api-tokens/:apiTokenIdPlaintext API tokens are returned only once at create or regenerate time.
Admin Ticket Management
Admin endpoints:
GET /api/v1/campaigns/:campaignId/tickets
GET /api/v1/campaigns/:campaignId/tickets/:ticketId
PATCH /api/v1/campaigns/:campaignId/tickets/:ticketId/revoke
PATCH /api/v1/campaigns/:campaignId/tickets/:ticketId/code/rotateRevoking can optionally send a notification email before invalidating the ticket:
{
"reason": "Guest requested cancellation",
"sendNotificationEmail": true,
"mailTemplateId": "revocation-mail-template-id",
"themeId": "optional-theme-id",
"idempotencyKey": "revoke-ticket-123"
}Rotating a ticket code returns a new one-time publicCode. Existing PDFs or QR
codes that contain the previous raw code become invalid.