# Read endpoints

Source: https://blocklive.io/docs/developers/reads

Every endpoint of the developer API, what it returns, how paging works, and how
to keep an onsite app up to date. For keys and authentication, see the
[developer API overview](https://blocklive.io/docs/developers).

All endpoints are `GET` under `https://blocklive.io/api/v1` and answer JSON.
Send your key on every request:

```http
Authorization: Bearer bl_live_<id>_<secret>
```

| Path                                         | Returns                                                                     |
| -------------------------------------------- | --------------------------------------------------------------------------- |
| `/events`                                    | The events your key can read, newest first.                                 |
| `/events/{event_id}/ticket-types`            | The event's ticket types.                                                   |
| `/events/{event_id}/attendees`               | One row per ticket holder, with only the details shared with your key.      |
| `/events/{event_id}/attendees/{attendee_id}` | One attendee.                                                               |
| `/events/{event_id}/check-ins`               | Check-ins and check-outs in the order they happened (needs venue check-in). |

## IDs

`event_id` is the event's Blocklive ID. Every other ID is an opaque string made
for your key and that event: `att_…` (attendees), `typ_…` (ticket types),
`ent_…` (tickets) and `pur_…` (purchases). The same person has a different
`attendee_id` at another event and for another app. Use them only with this
API, and don't try to match them across keys or events.

## Events

```
GET /api/v1/events
```

```json
{
    "data": [
        {
            "event_id": "6f1c…",
            "title": "Summer Opening",
            "starts_at": "2026-11-13T18:00:00.000Z",
            "ends_at": "2026-11-14T02:00:00.000Z",
            "time_zone": "Europe/London",
            "shared_fields": ["name", "checkin_state"]
        }
    ],
    "next_cursor": null,
    "complete": true,
    "as_of": "2026-11-05T12:00:00.000Z"
}
```

`shared_fields` lists the attendee details your key reads for that event.

## Ticket types

```
GET /api/v1/events/{event_id}/ticket-types
```

```json
{
    "data": [{ "ticket_type_id": "typ_…", "name": "General admission" }],
    "as_of": "2026-11-05T12:00:00.000Z",
    "shared_fields": ["ticket_entitlements", "ticket_type_names"]
}
```

`name` is there only if ticket type names are shared with your key. This
endpoint isn't paged.

## Attendees

```
GET /api/v1/events/{event_id}/attendees
```

```
GET /api/v1/events/{event_id}/attendees/{attendee_id}
```

One row per ticket holder. Every row has:

| Field         | Meaning                                                                                  |
| ------------- | ---------------------------------------------------------------------------------------- |
| `event_id`    | The event.                                                                               |
| `attendee_id` | The holder, as an opaque ID for your key.                                                |
| `eligibility` | `eligible` (holds a ticket), `pending` (a purchase is still processing) or `ineligible`. |
| `updated_at`  | When anything about this attendee last changed.                                          |

The rest appears only if it is shared with your key:

| Field                 | Detail                  | Value                                                                                             |
| --------------------- | ----------------------- | ------------------------------------------------------------------------------------------------- |
| `email`               | Email address           | A string or `null`.                                                                               |
| `name`                | Full name               | A string or `null`.                                                                               |
| `company`             | Company                 | A string or `null`.                                                                               |
| `job_title`           | Job title               | A string or `null`.                                                                               |
| `ticket_entitlements` | Ticket types and access | Their tickets: `id`, `ticket_type_id`, `status`, and `checkin_state` if venue check-in is shared. |
| `ticket_type_names`   | Ticket type names       | `id` and `name` of each ticket type they hold. Needs ticket types and access.                     |
| `purchase_state`      | Purchase status         | Their purchases: `id` and `state` (`confirmed`, `pending`, `rejected` or `unknown`).              |
| `checkin_state`       | Venue check-in          | `checked_in`, `not_checked_in` or `unknown`.                                                      |
| `checkin_time`        | Check-in time           | When they were checked in, or `null`. Needs venue check-in.                                       |

```json
{
    "data": [
        {
            "event_id": "6f1c…",
            "attendee_id": "att_…",
            "eligibility": "eligible",
            "updated_at": "2026-11-05T11:58:00.000Z",
            "name": "Sam Lee",
            "checkin_state": "not_checked_in"
        }
    ],
    "next_cursor": "…",
    "complete": false,
    "as_of": "2026-11-05T12:00:00.000Z",
    "shared_fields": ["name", "checkin_state"]
}
```

## Check-ins

```
GET /api/v1/events/{event_id}/check-ins?cursor=<last next_cursor>
```

```json
{
    "data": [
        {
            "attendee_id": "att_…",
            "entitlement_id": "ent_…",
            "checkin_state": "checked_in",
            "at": "2026-11-13T18:00:00.000Z"
        }
    ],
    "next_cursor": "…",
    "complete": true,
    "as_of": "2026-11-13T18:00:05.000Z"
}
```

Needs venue check-in shared with your key.

-   `checkin_state` is `checked_in` or `checked_out`, one row per scan or change
    at the door, oldest first. A ticket can appear more than once (scanned
    again, or out and back in): apply rows in order, and the last one for a
    ticket is its state.
-   A scan shows up about 5 seconds after it happens.
-   `entitlement_id` (the ticket) is there only if ticket types and access are
    shared. `attendee_id` is the ticket's holder now (it matches the attendees
    endpoint), or `null` for a ticket that no longer exists.
-   Every answer has a `next_cursor`, even an empty one. Keep the last one and
    send it on the next poll: you get only what happened since.

The attendees endpoint gives each attendee's current `checkin_state` and
`checkin_time`, if shared. It is the source of truth: use it to correct your
copy when you resync.

## Pages

`/events`, `/attendees` and `/check-ins` return up to `limit` rows (1 to 100,
default 100) and a `next_cursor`. Pass it back as `?cursor=` for the next page.
`complete: true` means there is nothing after this page. `limit` and `cursor`
are the only query parameters.

A page cursor can be used for 15 minutes; a check-in cursor for 24 hours, so a
door app restarted during doors carries on where it was.

A `410 resync_required` means the cursor can't be used any more: it is too old,
or the organizer changed what your key may read. Start again without a cursor.
See [limits and errors](https://blocklive.io/docs/developers/limits-and-errors).

## Keeping an onsite app up to date

1. **At start-up, and every 10 minutes:** read every page of
   `/events/{event_id}/attendees` for the guest list.
2. **Every 5 seconds during doors:** poll `/events/{event_id}/check-ins` with
   your last cursor, and apply the rows in order. An empty answer is cheap.
3. On `410`, drop the cursor and read check-ins from the start (it replays the
   night's scans, oldest first). On `429`, wait for `Retry-After`.

That is about 12 requests a minute for check-ins plus one guest-list sync every
10 minutes (one request per 100 attendees), well under the limit. Don't run
more than one poller per key.

Check-in itself stays in Blocklive's door app. This API reads it; it doesn't
change it.
