Tickets

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

ConceptMeaning
Ticket typeA reusable campaign ticket category with styling, validity defaults, personalization settings, capacity metadata, and a default check-in limit.
TicketOne issued admission record with status, serial number, validity window, check-in count, and check-in limit.
Public codeOpaque raw code rendered into the QR payload. It is returned only at create or rotate time.
RecipientA persisted delivery snapshot for a generated ticket email/PDF.
Check-inA successful ticket consumption event.
Scan attemptAppend-only history for validation and check-in attempts, including invalid scans.
Audit eventAppend-only lifecycle history for ticket creation, check-in, revocation, PDF generation, and code rotation.

Lifecycle

Ticket status values:

StatusMeaning
issuedThe ticket exists and can be validated when its validity window allows it.
checked_inThe ticket reached its configured check-in limit.
revokedThe ticket was administratively invalidated.
expiredThe ticket is outside its validity window or has been marked expired by backend logic.

Scanner result values:

ResultMeaning
validThe ticket can be checked in for the selected campaign and optional event.
checked_inThe current request successfully checked in the ticket.
already_checked_inThe ticket has already reached a terminal checked-in state.
limit_reachedThe configured check-in limit has been reached.
revokedThe ticket was revoked.
expiredThe ticket is expired.
not_yet_validThe ticket is before validFrom.
wrong_campaignThe code belongs to another campaign. The response does not expose the ticket id.
wrong_eventThe selected event does not match the ticket event.
invalidThe code cannot be resolved or trusted.

Ticket Types

Ticket types are managed under:

/api/v1/campaigns/:campaignId/tickets/types

Important fields:

FieldNotes
eventMetadataIdOptional event anchor. If omitted on create, the backend resolves the campaign-scope event metadata entry.
ticketTypeStylesPersisted PDF and preview styling. Missing values are normalized by the backend/shared style defaults.
imageSrc, heroImageSrcOptional ticket-specific images. If omitted, rendering can fall back to campaign images.
isPersonalizedRequires recipient data before PDFs are generated for send flows.
displayNameTemplateOptional display name template for personalized tickets.
recipientEmailPathOptional path used by recipient mapping logic.
capacityCapacity metadata for overviews and planning. Treat hard capacity enforcement as backend behavior that should be verified for the current deployment.
checkinLimitDefault successful check-ins allowed per ticket. Defaults to 1.
validFrom, validUntilDefaults copied to issued tickets. If absent, event dates may be used.
isActiveInactive 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-image

Sample PDFs can be generated without issuing a ticket:

GET /api/v1/campaigns/:campaignId/tickets/types/:ticketTypeId/sample-pdf

Issuing Tickets

Create a single ticket:

POST /api/v1/campaigns/:campaignId/tickets

Example 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/send

Supported 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/resend

Resending 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/pdf

Payload:

{
  "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-in

Payload:

{
  "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/overview

Current 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/revoke

Token-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-in

Accepted credentials:

Authorization: Bearer scn_api_...

Current ticketing token policies:

PolicyAllows
CREATE_TICKETSCreating tickets through regular or token-only ticket endpoints.
VALIDATE_TICKETSValidating ticket codes through regular or token-only ticket endpoints.
CHECK_IN_TICKETSChecking in tickets through regular or token-only ticket endpoints.
REVOKE_TICKETSRevoking 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/:apiTokenId

Plaintext 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/rotate

Revoking 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.