Skip to content

Read endpoints

On this page

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:

HTTP
Authorization: Bearer bl_live_<id>_<secret>
PathReturns
/eventsThe events your key can read, newest first.
/events/{event_id}/ticket-typesThe event's ticket types.
/events/{event_id}/attendeesOne row per ticket holder, with only the details shared with your key.
/events/{event_id}/attendees/{attendee_id}One attendee.
/events/{event_id}/check-insCheck-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

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:

FieldMeaning
event_idThe event.
attendee_idThe holder, as an opaque ID for your key.
eligibilityeligible (holds a ticket), pending (a purchase is still processing) or ineligible.
updated_atWhen 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:

FieldDetailValue
emailEmail addressA string or null.
nameFull nameA string or null.
team_person_idTeam person IDThe person's ID in the event's team, or null. See matching the attendee export.
companyCompanyA string or null.
job_titleJob titleA string or null.
organisation_typeOrganisation typeA string or null.
registration_answersRegistration answersTheir answers to the event's other questions. See registration answers.
ticket_entitlementsTicket types and accessTheir tickets: id, ticket_type_id, status, checkin_state if venue check-in is shared, and order_status if purchase status is shared.
ticket_type_namesTicket type namesid and name of each ticket type they hold. Needs ticket types and access.
purchase_statePurchase statusTheir purchases: id, state (confirmed, pending, rejected or unknown) and order_status.
checkin_stateVenue check-inchecked_in, not_checked_in or unknown.
checkin_timeCheck-in timeWhen 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"]
}

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.

FieldValue
question_idThe question, as an opaque ID (qst_…) for your key and that event. It stays the same if the organizer renames the question.
keyA 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.
questionThe question as the organizer wrote it.
answerThe 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.

JSON
"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 exportAPI
orderStatusorder_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 processedorder_status: "processed". Its state is confirmed.
attendeeStatus Tickets receivedA ticket with order_status: "processed" and checkin_state: "not_checked_in".
attendeeStatus Checked inA ticket with order_status: "processed" and checkin_state: "checked_in".
attendeeStatus Tickets processingA ticket or order whose order_status is fulfilled, minted, processing, awaiting_processing or another in-between status.
attendeeStatus Pending approvalAn order with order_status: "pending_approval" (state: "pending", eligibility: "pending").
attendeeStatus DeclinedAn order with order_status: "rejected" (state: "rejected").
A ticket rowAn 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 null for 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 (or userProfileId), 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=processed

purchase_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>
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. Without it, this endpoint answers 404 not_found, the same as an event your key can't read.

  • 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, 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

  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.