Integration API Reference
Connect your ticketing platform to Scan Manager to import events, sync and manage tickets in real time.
Overview
The Integration API lets authorised third-party platforms push event and ticket data directly into Scan Manager. Once imported, tickets are immediately available to door staff scanning via the mobile app or Zebra scanner.
- Base URL:
https://scan-manager.app/api/integration
- Format: All requests and responses use
application/json.
- Auth: API key + email headers on every request (see Authentication below).
- Door users: Create and manage scanner login users via API, with per-event restrictions.
- Plans: API access requires an Enterprise subscription.
- Rate limits: Write endpoints are limited to 60 requests per minute per API key.
Authentication
Every request must include these HTTP headers:
| Header | Value | |
X-Api-Key | Your API key (starts with es_) | required |
X-Api-Email | The email address associated with your API key | required |
Content-Type | application/json | required |
Accept | application/json | recommended |
Generate API keys from your promoter dashboard under Settings → API Keys. The plain-text key is shown only once — store it securely in a secrets manager or environment variable, never in source code.
Example authenticated request
curl -X GET "https://scan-manager.app/api/integration/handshake" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com" \
-H "Accept: application/json"
GET
/handshake
Verify that your credentials are valid and the connection is working. Call this first when setting up your integration. Marks the key as connected in the dashboard.
200 Response
{
"success": true,
"message": "Connection established successfully.",
"data": {
"promoter_name": "Acme Events Ltd",
"promoter_id": 42,
"connected_at": "2025-10-01T12:00:00+00:00"
}
}
Door User Provisioning
Use these endpoints when your ticketing platform needs to provision scanner users automatically. Door users authenticate in the scanner app with email + password and can be restricted to selected events.
ⓘ Write endpoints are rate-limited to 60 requests/min per API key.
GET
/door-users
List all door users for the authenticated promoter.
200 Response
{
"success": true,
"data": [
{
"id": 77,
"name": "Main Entrance",
"email": "door.main@example.com",
"role": "door",
"allowed_event_ids": [101, 102],
"event_restricted": true,
"created_at": "2026-06-26T09:00:00+00:00",
"updated_at": "2026-06-26T09:00:00+00:00"
}
]
}
POST
/door-users
Create a new door user for scanner login.
| Field | Type | Notes | |
name | string | Display name for the scanner operator | required |
email | string | Must be globally unique | required |
password | string | Minimum 8 characters | required |
allowed_event_ids | integer[] | If omitted or empty, user can scan all promoter events | optional |
Example request body
{
"name": "North Gate",
"email": "north.gate@example.com",
"password": "ChangeMe123!",
"allowed_event_ids": [101, 102]
}
201 Response
{
"success": true,
"message": "Door user created successfully.",
"data": {
"id": 78,
"name": "North Gate",
"email": "north.gate@example.com",
"role": "door",
"allowed_event_ids": [101, 102],
"event_restricted": true,
"created_at": "2026-06-26T09:05:00+00:00",
"updated_at": "2026-06-26T09:05:00+00:00"
}
}
POST
PUT /door-users/{id}
Update name, email, password, and/or event restrictions for an existing door user.
Request body (all fields optional)
{
"name": "North Gate Team",
"allowed_event_ids": [101]
}
200 Response
{
"success": true,
"message": "Door user updated successfully.",
"data": {
"id": 78,
"name": "North Gate Team",
"email": "north.gate@example.com",
"role": "door",
"allowed_event_ids": [101],
"event_restricted": true,
"created_at": "2026-06-26T09:05:00+00:00",
"updated_at": "2026-06-26T09:20:00+00:00"
}
}
POST
DELETE /door-users/{id}
Delete a door user. Any active scanner session tokens for that user should be considered invalid for future logins.
200 Response
{
"success": true,
"message": "Door user deleted successfully."
}
GET
/events
Returns all events belonging to your promoter account, ordered by date descending. Use this to look up event_id values for subsequent calls.
200 Response
{
"success": true,
"data": [
{
"id": 101,
"name": "Summer Festival 2025",
"event_date": "2025-08-15 19:00:00",
"venue": "Brixton Academy, London",
"tickets_count": 1200
}
]
}
GET
/events/{id}
Returns a single event with live scan statistics. Only returns events belonging to your promoter account.
200 Response
{
"success": true,
"data": {
"id": 101,
"name": "Summer Festival 2025",
"event_date": "2025-08-15T19:00:00+00:00",
"venue": "Brixton Academy, London",
"status": "active",
"capacity": null,
"tickets_total": 1200,
"tickets_scanned": 843,
"tickets_cancelled": 12,
"scan_rate": 70.3
}
}
404 Response
{ "success": false, "message": "Event not found." }
GET
/events/{id}/tickets
Returns a paginated list of all tickets for a given event. Supports filtering by scan status or ticket status.
Query parameters
| Parameter | Type | Notes | |
per_page | integer | Results per page (default 100, max 500) | optional |
page | integer | Page number (default 1) | optional |
scanned | boolean | Filter by scan state: true or false | optional |
status | string | Filter by ticket status: valid or cancelled | optional |
200 Response
{
"success": true,
"event": {
"id": 101,
"name": "Summer Festival 2025",
"event_date": "2025-08-15T19:00:00+00:00",
"venue": "Brixton Academy, London"
},
"data": [
{
"ticket_uid": "TKT-ABC123",
"order_ref": "ORD-9001",
"name": "Jane Smith",
"email": "jane@example.com",
"ticket_type": "General Admission",
"provider": "MyTicketingApp",
"status": "valid",
"scanned": true,
"scanned_at": "2025-08-15T21:34:12+00:00"
}
],
"meta": {
"total": 1200,
"per_page": 100,
"current_page": 1,
"last_page": 12
}
}
404 Response
{ "success": false, "message": "Event not found." }
POST
/import
Create a new event and import its tickets in one request. Duplicate ticket_uid values within the same event are automatically skipped.
Request body
| Field | Type | Notes | |
event.name | string | Event name (max 255) | required |
event.event_date | datetime | YYYY-MM-DD HH:MM:SS or ISO 8601 | required |
event.venue | string | Venue name or address (max 255) | required |
tickets[].ticket_uid | string | Unique identifier — the value encoded in the barcode or QR code | required |
tickets[].order_ref | string | Order or booking reference | optional |
tickets[].name | string | Attendee full name | optional |
tickets[].email | string | Attendee email address | optional |
tickets[].ticket_type | string | e.g. "General Admission", "VIP" | optional |
tickets[].provider | string | Name of the originating platform | optional |
Example request body
{
"event": {
"name": "Summer Festival 2025",
"event_date": "2025-08-15 19:00:00",
"venue": "Brixton Academy, London"
},
"tickets": [
{
"ticket_uid": "TKT-ABC123",
"order_ref": "ORD-9001",
"name": "Jane Smith",
"email": "jane@example.com",
"ticket_type": "General Admission",
"provider": "MyTicketingApp"
}
]
}
201 Response
{
"success": true,
"message": "Event created with 1 tickets imported.",
"event": { "id": 101, "name": "Summer Festival 2025", "ticket_count": 1 },
"imported": 1,
"skipped": 0,
"data": { "event_id": 101, "event_name": "Summer Festival 2025", "tickets_imported": 1, "tickets_skipped": 0, "errors": [] }
}
POST
/sync-tickets
Add more tickets to an existing event. Identify the event by event_id (preferred) or by matching on event_name + event_date. Tickets whose ticket_uid already exists on that event are silently skipped.
Request body
| Field | Type | Notes | |
event_id | integer | Preferred — use id from GET /events | optional* |
event_name | string | Exact event name (fallback) | optional* |
event_date | date | Narrows the event_name lookup | optional |
tickets[] | array | Same fields as POST /import | required |
* Provide at least one of event_id or event_name.
Example request body
{
"event_id": 101,
"tickets": [
{
"ticket_uid": "TKT-XYZ789",
"order_ref": "ORD-9003",
"name": "Alice Jones",
"email": "alice@example.com",
"ticket_type": "VIP",
"provider": "MyTicketingApp"
},
{
"ticket_uid": "TKT-XYZ790",
"name": "Bob Brown",
"ticket_type": "General Admission"
}
]
}
200 Response
{
"success": true,
"message": "2 tickets imported, 0 skipped.",
"event_id": 101,
"imported": 2,
"skipped": 0
}
POST
/tickets/status
Mark tickets as cancelled. Cancelled tickets stay in the system but cannot be scanned — the scanner app rejects them and shows a Cancelled result to door staff. Use this for refunded or transferred tickets where you want an audit trail.
ⓘ Rate limit: 60 requests per minute per API key.
Request body
| Field | Type | Notes | |
ticket_uids | string[] | Array of ticket UIDs to cancel | required |
status | string | Must be "cancelled" | required |
Example request body
{
"ticket_uids": [
"TKT-ABC123",
"TKT-DEF456"
],
"status": "cancelled"
}
200 Response
{
"success": true,
"updated": 2,
"skipped": 0
}
Tickets not found or belonging to another promoter are silently skipped.
POST
/tickets/remove
Permanently remove tickets from the event. Removed tickets are soft-deleted and will no longer appear in the scanner app or ticket lists. Use this when a ticket must be fully invalidated (e.g. duplicate orders, data corrections).
⚠
Destructive action. Removed tickets cannot be recovered via the API. If you may need the ticket again, use
POST /tickets/status to cancel it instead.
ⓘ Rate limit: 60 requests per minute per API key. Maximum 200 UIDs per request.
Request body
| Field | Type | Notes | |
ticket_uids | string[] | Array of ticket UIDs to remove (max 200 per request) | required |
reason | string | Short reason for audit log (e.g. "refunded", "duplicate") | optional |
Example request body
{
"ticket_uids": [
"TKT-ABC123",
"TKT-DEF456"
],
"reason": "refunded"
}
200 Response
{
"success": true,
"message": "2 ticket(s) removed, 0 not found or already removed.",
"removed": 2,
"skipped": 0
}
Tickets not found, already removed, or belonging to another promoter are counted as skipped.
Cancel vs Remove — which to use?
| POST /tickets/status | POST /tickets/remove |
| Effect | Sets status = cancelled | Soft-deletes the ticket row |
| Scanner behaviour | Shows "Cancelled" to door staff | Ticket not found |
| Reversible? | Yes | No (via API) |
| Best for | Refunds with audit trail | Duplicates, data corrections |
POST
/tickets/lookup
Query the scan status of one or more tickets by ticket_uid. Returns whether each ticket has been scanned, when it was scanned, and its current status. Useful for syncing your ticketing platform after an event.
ⓘ Rate limit: 60 requests per minute per API key. Maximum 200 UIDs per request.
Request body
| Field | Type | Notes | |
ticket_uids | string[] | Array of ticket UIDs to look up (max 200 per request) | required |
Example request body
{
"ticket_uids": [
"TKT-ABC123",
"TKT-DEF456"
]
}
200 Response
{
"success": true,
"data": {
"TKT-ABC123": {
"ticket_uid": "TKT-ABC123",
"order_ref": "ORD-9001",
"name": "Jane Smith",
"email": "jane@example.com",
"ticket_type": "General Admission",
"status": "valid",
"scanned": true,
"scanned_at": "2025-08-15T21:34:12+00:00",
"event": {
"id": 101,
"name": "Summer Festival 2025",
"event_date": "2025-08-15 19:00:00",
"venue": "Brixton Academy, London"
}
},
"TKT-DEF456": {
"ticket_uid": "TKT-DEF456",
"order_ref": "ORD-9002",
"name": "John Doe",
"email": "john@example.com",
"ticket_type": "VIP",
"status": "valid",
"scanned": false,
"scanned_at": null,
"event": {
"id": 101,
"name": "Summer Festival 2025",
"event_date": "2025-08-15 19:00:00",
"venue": "Brixton Academy, London"
}
}
},
"missing": []
}
The data object is keyed by ticket_uid for easy lookup. UIDs not found or belonging to another promoter are listed in missing.
Status field values
| status | scanned | Meaning |
valid | false | Ticket is valid and has not been scanned yet |
valid | true | Ticket was successfully scanned at the door |
cancelled | false | Ticket has been cancelled and will be rejected at the door |
Webhook Guide
When a ticket is scanned successfully, Scan Manager can push a webhook to your endpoint. Configure endpoint URLs from the promoter dashboard under Webhooks.
Supported webhook events
| Event | When it fires |
ticket.scanned | A valid ticket is checked in by a door user |
event.created | An event is created in Scan Manager |
event.updated | An event is updated in Scan Manager |
Delivery headers
| Header | Description |
X-Webhook-Event | Event type, e.g. ticket.scanned |
X-Webhook-Delivery | Unique delivery ID |
X-Webhook-Signature | HMAC signature in format sha256=<hash> |
Signature verification
Compute an HMAC SHA-256 hash of the raw request body using your webhook secret, then compare to X-Webhook-Signature.
# Pseudocode
signature_header = request.headers["X-Webhook-Signature"]
expected = "sha256=" + HMAC_SHA256(raw_body, webhook_secret)
if !constant_time_equals(signature_header, expected):
reject 401
ticket.scanned payload example
{
"ticket_uid": "TKT-ABC123",
"ticket_name": "Jane Smith",
"ticket_type": "General Admission",
"order_ref": "ORD-9001",
"event_id": 101,
"event_name": "Summer Festival 2025",
"scanned_at": "2026-06-26T09:30:01+00:00",
"door_user": "North Gate",
"event": "ticket.scanned",
"timestamp": "2026-06-26T09:30:02+00:00",
"promoter": 42
}
ⓘ Webhook deliveries are retried up to 3 attempts on non-2xx responses or network failures.
Error Codes
| HTTP Status | Meaning | Common cause |
| 401 | Unauthorized | Missing or invalid X-Api-Key / X-Api-Email, or key is inactive. |
| 402 | Payment Required | Subscription expired or inactive. |
| 403 | Forbidden | Plan does not include API access. Upgrade to Enterprise. |
| 404 | Not Found | Event not found or belongs to a different promoter. |
| 422 | Unprocessable Entity | Validation failed. The errors field lists specific issues. |
| 429 | Too Many Requests | Rate limit exceeded. Retry after one minute. |
| 5xx | Server Error | Unexpected error. Contact support if this persists. |
Error response shape
{
"success": false,
"message": "Validation failed.",
"errors": { "ticket_uids": ["The ticket uids field is required."] }
}
Quick-start cURL Examples
1. Verify connection
curl -X GET "https://scan-manager.app/api/integration/handshake" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com"
2. Create event + import tickets
curl -X POST "https://scan-manager.app/api/integration/import" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com" \
-H "Content-Type: application/json" \
-d '{ "event": { "name": "Summer Festival 2025", "event_date": "2025-08-15 19:00:00", "venue": "Brixton Academy" }, "tickets": [{ "ticket_uid": "TKT-001", "name": "Jane Smith" }] }'
3. Add more tickets to an existing event
curl -X POST "https://scan-manager.app/api/integration/sync-tickets" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com" \
-H "Content-Type: application/json" \
-d '{
"event_id": 101,
"tickets": [
{
"ticket_uid": "TKT-002",
"order_ref": "ORD-9002",
"name": "Bob Jones",
"email": "bob@example.com",
"ticket_type": "General Admission",
"provider": "MyTicketingApp"
}
]
}'
4. Cancel a refunded ticket
curl -X POST "https://scan-manager.app/api/integration/tickets/status" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com" \
-H "Content-Type: application/json" \
-d '{ "ticket_uids": ["TKT-001"], "status": "cancelled" }'
5. Remove a ticket entirely
curl -X POST "https://scan-manager.app/api/integration/tickets/remove" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com" \
-H "Content-Type: application/json" \
-d '{ "ticket_uids": ["TKT-001"], "reason": "refunded" }'
6. Look up scan status of tickets
curl -X POST "https://scan-manager.app/api/integration/tickets/lookup" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com" \
-H "Content-Type: application/json" \
-d '{
"ticket_uids": [
"TKT-ABC123",
"TKT-DEF456"
]
}'
7. Get a single event with stats
curl -X GET "https://scan-manager.app/api/integration/events/101" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com"
8. Get tickets for an event (paginated)
curl -X GET "https://scan-manager.app/api/integration/events/101/tickets?per_page=100&page=1" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com"
9. Get only unscanned tickets for an event
curl -X GET "https://scan-manager.app/api/integration/events/101/tickets?scanned=false" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com"
10. Create a restricted door user
curl -X POST "https://scan-manager.app/api/integration/door-users" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com" \
-H "Content-Type: application/json" \
-d '{
"name": "North Gate",
"email": "north.gate@example.com",
"password": "ChangeMe123!",
"allowed_event_ids": [101, 102]
}'
11. Update door user event access
curl -X PUT "https://scan-manager.app/api/integration/door-users/78" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com" \
-H "Content-Type: application/json" \
-d '{ "allowed_event_ids": [101] }'
12. Delete a door user
curl -X DELETE "https://scan-manager.app/api/integration/door-users/78" \
-H "X-Api-Key: es_yourkeyhere" \
-H "X-Api-Email: you@example.com"