Read endpoints
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.
All endpoints are GET under https://blocklive.io/api/v1 and answer JSON. Send your key on every request:
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, in lower case (an upper-case ID gets 404). Every other ID is an opaque string made for your key and that event: att_… (attendees), typ_… (ticket types), ent_… (tickets), pur_… (purchases) and qst_… (registration questions). 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. The one ID made to match is team_person_id, if it is shared with your key: see matching the attendee export.
Events
/api/v1/events{
"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
/api/v1/events/{event_id}/ticket-types{
"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
/api/v1/events/{event_id}/attendees/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. |
A guest order (placed without a Blocklive account) that is still waiting for its ticket, or was refused, is an attendee of its own. Once its ticket is issued, that person appears under the ticket holder's attendee_id instead, and the order's earlier attendee_id stops answering.
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. |
team_person_id | Team person ID | The person's ID in the event's team, or null. See matching the attendee export. |
company | Company | A string or null. |
job_title | Job title | A string or null. |
organisation_type | Organisation type | A string or null. |
registration_answers | Registration answers | Their answers to the event's other questions. See registration answers. |
ticket_entitlements | Ticket types and access | Their tickets: id, ticket_type_id, status, checkin_state if venue check-in is shared, and order_status if purchase status 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, state (confirmed, pending, rejected or unknown) and order_status. |
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. |
{
"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"]
}Job title, company and organisation type
job_title, company and organisation_type come only from the event's registration questions that the organizer marked as that attendee field in the registration form (Attendee field: Job title, Company or Organisation type). Nothing is read from a question's wording. A question the organizer left as their own question is in registration_answers; marking it later keeps the answers already given, and they fill the field from then on.
Events made before marking existed had their job title and company questions marked once by Blocklive, from their wording ("What's your job title?", "Company name"; never a company website), so they read as before.
If an event marks two questions for one field, the first in the form's order fills it.
Each detail is the attendee's own answer: first the answers on their own tickets, then the answers given for their order while it waits for a ticket (the ones with the buyer's email, when the order holds tickets for others), then the ones they gave from their account on no ticket and for no other ticket, the newest first within each. An answer someone gave on another person's ticket belongs to that ticket's holder. An attendee with two tickets and a different answer on each gets the newer one; registration_answers has the same rule.
Registration answers
With registration answers shared, each attendee has registration_answers: their answer to each of the event's other registration questions they answered, in the order the event asks them.
| Field | Value |
|---|---|
question_id | The question, as an opaque ID (qst_…) for your key and that event. It stays the same if the organizer renames the question. |
key | A readable key: the question's title as lower-case words joined by _, then the first 8 characters of question_id. It changes if the question is renamed, so match on question_id. |
question | The question as the organizer wrote it. |
answer | The answer as text. A choice question gives the option they picked; a checkbox gives Yes or No. |
Only questions the event still asks are included. Questions that fill an attendee field (job title, company, organisation type) and the email, full name, phone number, wallet and social account questions are left out: email, name and the marked ones have fields of their own. A question the attendee left blank isn't listed.
"registration_answers": [
{
"question_id": "qst_…",
"key": "whats_your_job_title_3f9a12c4",
"question": "What's your job title?",
"answer": "Head of Partnerships"
},
{
"question_id": "qst_…",
"key": "do_you_want_to_receive_updates_and_newsletter_8b20e7d1",
"question": "Do you want to receive updates and newsletter?",
"answer": "Yes"
}
]Statuses as the attendee export names them
An organizer's attendee export (Event → Attendees → Export) and this API describe the same orders and tickets. The export's words are Blocklive's own order statuses, which the API gives as order_status:
| Attendee export | API |
|---|---|
orderStatus | order_status, on each item of purchase_state and of ticket_entitlements (needs purchase status). The same word: processed, fulfilled, minted, processing, pending_approval, rejected, and so on. |
orderStatus processed | order_status: "processed". Its state is confirmed. |
attendeeStatus Tickets received | A ticket with order_status: "processed" and checkin_state: "not_checked_in". |
attendeeStatus Checked in | A ticket with order_status: "processed" and checkin_state: "checked_in". |
attendeeStatus Tickets processing | A ticket or order whose order_status is fulfilled, minted, processing, awaiting_processing or another in-between status. |
attendeeStatus Pending approval | An order with order_status: "pending_approval" (state: "pending", eligibility: "pending"). |
attendeeStatus Declined | An order with order_status: "rejected" (state: "rejected"). |
| A ticket row | An item of ticket_entitlements. Its status is always active: the attendee holds it now. |
state groups the order statuses: confirmed is processed, fulfilled, minted or failed_to_deliver (the ticket is theirs, even if an email failed); pending is still on its way or waiting for approval; rejected is declined. The export counts only processed as Tickets received, so use order_status to match it exactly.
Matching the attendee export
team_person_id is a person's ID in the event's team: tpi_ and 22 letters, digits, - and _. It is the same for that person at every event of the team, in this API and in the team's exports, where it is the last column, team_person_id, of the attendee export (Event → Attendees → Export) and of the order export (Event → Orders → Export, the buyer's). Join the API's rows and an export's on it.
- Another team has a different ID for the same person, so match what you read with exports from the team the event belongs to. It doesn't matter whether your key is the team's or its organization's.
- An event moved to another team has that team's IDs from then on.
- It is
nullfor a ticket held by a wallet without a Blocklive account and for an order placed without one. - An export's ID is always the one for its row's
profileId(oruserProfileId), so files keyed by those can move to it: export once, and the file has both side by side. A ticket in a wallet that was unlinked from an account can still be listed under that account in the export, with its ID, while this API lists it under the wallet, without one. - If an ID can't be worked out at the time, the answer leaves the field out (read again later), and an export goes out with the column empty.
Only confirmed or processed attendees
GET /api/v1/events/{event_id}/attendees?purchase_state=confirmed
GET /api/v1/events/{event_id}/attendees?order_status=processedpurchase_state keeps only the attendees with at least one purchase in one of the given states, as the purchase_state field reports them: confirmed, pending, rejected or unknown, or several separated by commas (confirmed,pending).
order_status keeps only the attendees with at least one order in one of the given order statuses, the attendee export's orderStatus words, as order_status reports them, or several separated by commas (processed,fulfilled). order_status=processed is exactly the export's processed orders; purchase_state=confirmed also takes orders the export still shows as processing (see the table above).
Both need purchase status shared with your key; without it, or with any other value, the answer is 400 invalid_request. Given together, an attendee needs an order that matches both. Attendees with no order (such as tickets an organizer sent) aren't listed when either is set. An attendee who also has an order in another state is listed, with all their purchases in purchase_state.
Pass the same filters with each cursor. A cursor from a list with other filters, or none, answers 410 resync_required.
Check-ins
GET /api/v1/events/{event_id}/check-ins?cursor=<last next_cursor>{
"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. Without it, this endpoint answers 404 not_found, the same as an event your key can't read.
checkin_stateischecked_inorchecked_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_idis the ticket's holder now (it matches the attendees endpoint), ornullfor 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, except purchase_state and order_status on /attendees (see only confirmed or processed attendees).
A cursor is sealed: it can't be read or changed. Pass it back exactly as you got it.
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.
Keeping an onsite app up to date
- At start-up, and every 10 minutes: read every page of
/events/{event_id}/attendeesfor the guest list. - Every 5 seconds during doors: poll
/events/{event_id}/check-inswith your last cursor, and apply the rows in order. An empty answer is cheap. - On
410, drop the cursor and read check-ins from the start (it replays the night's scans, oldest first). On429, wait forRetry-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.