> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tickable.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Tickets

> List tickets for an event and scan them via the API

The Tickets API lets you retrieve tickets for your events and scan them programmatically. This is useful for building custom check-in apps, integrating with access control systems, or syncing attendee data.

## Prerequisites

You need an API key with the following scopes:

| Scope           | Required for     |
| --------------- | ---------------- |
| `tickets.read`  | Listing tickets  |
| `tickets.write` | Scanning tickets |

Create an API key from the [Tickable dashboard](https://app.tickable.nl/settings/developers).

## Listing Tickets

Tickets are scoped to an event. Pass the event ID to retrieve all tickets:

```bash theme={null}
curl https://api.tickable.io/events/550e8400-e29b-41d4-a716-446655440000/tickets \
  -H "Authorization: Bearer tk_live_YOUR_API_KEY"
```

The response includes pagination and a `status` field for each ticket:

```json theme={null}
{
  "data": [
    {
      "id": "c3d4e5f6-7890-4a1b-2c3d-4e5f6a7b8c9d",
      "scannable_code": "c3d4e5f6-7890-4a1b-2c3d-4e5f6a7b8c9d",
      "ticket_type_id": "b2c4e6a8-1234-4f5a-9c8d-0e1f2a3b4c5d",
      "order_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "owner_name": "Jane Doe",
      "owner_email": "jane@example.com",
      "seat": "A12",
      "status": "valid",
      "scanned_at": null,
      "cancelled_at": null
    }
  ],
  "pagination": {
    "total": 48,
    "limit": 25,
    "offset": 0,
    "has_more": true
  }
}
```

### Ticket Status

| Status      | Meaning                                  |
| ----------- | ---------------------------------------- |
| `valid`     | Ticket has not been scanned or cancelled |
| `scanned`   | Ticket has already been scanned          |
| `cancelled` | Ticket has been cancelled                |

### Filtering by Order

To get all tickets belonging to a specific order, use the `order_id` query parameter:

```bash theme={null}
curl "https://api.tickable.io/events/550e8400.../tickets?order_id=f47ac10b..." \
  -H "Authorization: Bearer tk_live_YOUR_API_KEY"
```

## Scanning Tickets

Use the scan endpoint to mark a ticket as scanned. The API validates scanner profiles, timeslot restrictions, and scan windows automatically.

```bash theme={null}
curl -X POST https://api.tickable.io/tickets/c3d4e5f6-7890-4a1b-2c3d-4e5f6a7b8c9d/scan \
  -H "Authorization: Bearer tk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scanner_id": "e5f6a7b8-9012-4c3d-4e5f-6a7b8c9d0e1f"}'
```

The `scanner_id` is optional. If omitted, the first active scanner in your organization is used:

```bash theme={null}
curl -X POST https://api.tickable.io/tickets/c3d4e5f6-7890-4a1b-2c3d-4e5f6a7b8c9d/scan \
  -H "Authorization: Bearer tk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### Scan Response

The response includes the scan result along with event and ticket details:

```json theme={null}
{
  "ticket_id": "c3d4e5f6-7890-4a1b-2c3d-4e5f6a7b8c9d",
  "status": "valid",
  "scanned_before": false,
  "event": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Summer Festival 2026",
    "location": "Vondelpark"
  },
  "timeslot": {
    "title": "Morning Session",
    "starts_at": "2026-06-15T10:00:00Z",
    "ends_at": "2026-06-15T12:00:00Z"
  },
  "ticket_type": {
    "id": "b2c4e6a8-1234-4f5a-9c8d-0e1f2a3b4c5d",
    "name": "Early Bird",
    "price": 2500
  },
  "owner_name": "Jane Doe",
  "owner_email": "jane@example.com",
  "seat": "A12",
  "scanned_at": "2026-06-15T10:05:00Z",
  "cancelled_at": null,
  "related_tickets": null
}
```

### Scan Status

The `status` field tells you the outcome of the scan:

| Status        | Meaning                                                 |
| ------------- | ------------------------------------------------------- |
| `valid`       | Ticket was successfully scanned                         |
| `scanned`     | Ticket was already scanned before                       |
| `cancelled`   | Ticket has been cancelled                               |
| `not_allowed` | Scanner profile rules or scan window prevented the scan |

<Warning>
  A `scanned` status means the ticket was already used. The `scanned_before` field is `true` in this case. The original `scanned_at` timestamp is preserved.
</Warning>

### Related Tickets

When the event has order scanning enabled, the response includes `related_tickets` — all other tickets from the same order. This is useful for group check-in scenarios:

```json theme={null}
{
  "related_tickets": [
    {
      "id": "a1b2c3d4-5678-4e9f-0a1b-2c3d4e5f6a7b",
      "seat": "A13",
      "owner_name": "John Doe",
      "owner_email": "john@example.com",
      "scanned_at": null,
      "cancelled_at": null,
      "ticket_type_name": "Early Bird"
    }
  ]
}
```

## Example: Custom Check-in App

<CodeGroup>
  ```javascript Node.js theme={null}
  async function scanTicket(ticketId, scannerId) {
    const response = await fetch(
      `https://api.tickable.io/tickets/${ticketId}/scan`,
      {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer tk_live_YOUR_API_KEY',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ scanner_id: scannerId }),
      }
    );

    const result = await response.json();

    switch (result.status) {
      case 'valid':
        console.log(`Welcome, ${result.owner_name}!`);
        break;
      case 'scanned':
        console.log(`Already scanned at ${result.scanned_at}`);
        break;
      case 'cancelled':
        console.log('This ticket has been cancelled');
        break;
      case 'not_allowed':
        console.log('Scan not allowed — check scanner profile or scan window');
        break;
    }

    return result;
  }
  ```

  ```python Python theme={null}
  import requests

  def scan_ticket(ticket_id, scanner_id=None):
      response = requests.post(
          f'https://api.tickable.io/tickets/{ticket_id}/scan',
          headers={
              'Authorization': 'Bearer tk_live_YOUR_API_KEY',
              'Content-Type': 'application/json',
          },
          json={'scanner_id': scanner_id},
      )

      result = response.json()

      match result['status']:
          case 'valid':
              print(f"Welcome, {result['owner_name']}!")
          case 'scanned':
              print(f"Already scanned at {result['scanned_at']}")
          case 'cancelled':
              print('This ticket has been cancelled')
          case 'not_allowed':
              print('Scan not allowed')

      return result
  ```
</CodeGroup>
