Events API

Events API — Overview

Create and manage the event records every other Qflow surface hangs off — plus the per-event tally counters and attendee statistics that ride on top.

The Events API is the root of the data model. An event is the top-level record everything else (attendees, tallies, team-members, comms templates) belongs to. Most integrations create the event up-front, then drive everything else against its id.

URL pattern

All endpoints on this page live under:

https://api.qflowhub.io/checkin/v1/api/<endpoint>

The /api/ prefix matters — Events is segment-versioned (unlike Comms, which is path-stripped). Every request needs both an OAuth bearer token AND an Ocp-Apim-Subscription-Key header — see Authentication.

What it does

Guest / attendee CRUD lives on a separate surface — see Guests. Team-member admin is on Team.

Quick start

curl -X POST 'https://api.qflowhub.io/checkin/v1/api/event' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "title":      "My first event",
    "startTime":  "2026-06-01T18:00:00",
    "endTime":    "2026-06-01T23:00:00",
    "timeZoneId": "Europe/London"
  }'

Response includes the server-assigned id — save it; that's your eventId everywhere else. With an event in hand you can:

  • Populate the attendee list — see Guests.
  • Author email templates that reference it — see Comms.
  • Add named counters — see Tallies.

Read in this order

  1. Events — create / read / list / update / lifecycle / statistics.
  2. Tallies — counter increments and ledger semantics.

For full request/response schemas + try-it, see the auto-generated API Reference.

Events API

Events

Create, read, list and update event records — the top-level object every other Qflow surface hangs off.

Concept

An event is the top-level record in Qflow. Attendees, tallies, team-members, and comms templates all belong to one. The CRUD operations on this page handle the four common cases — create, read, list, update. State changes (disable, enable, delete) and the dedicated tag / statistics endpoints have their own pages — see the sidebar.

Creating an event

POST /api/event accepts the full event body. Only title, startTime, and endTime are required.

curl -X POST 'https://api.qflowhub.io/checkin/v1/api/event' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "title":      "Soho House Summer Launch",
    "startTime":  "2026-07-15T19:00:00",
    "endTime":    "2026-07-15T23:30:00",
    "timeZoneId": "Europe/London",
    "tags":       "VIP, Press, Staff",
    "customData": "{\"venue\":\"Soho House Greek Street\",\"booker\":\"Maya\"}"
  }'

Field-level notes:

  • startTime / endTime — interpreted as UTC unless timeZoneId is supplied. Use TZDB ids (Europe/London, America/New_York); aliases like BST aren't honoured.
  • active — forced to true on create regardless of what you send. Toggle later via the Lifecycle endpoints.
  • tags — a comma-separated string, not an array. See Tags for the non-countable naming convention.
  • customData — free-form. Most integrators stringify JSON here and parse on read; there's no schema enforcement.
  • id — server-assigned by default. You may supply your own GUID to control the identifier, but create is not idempotent: re-POSTing the same id doesn't return the existing event — it fails with a duplicate-key error. Generate a fresh id per event and don't blind-retry a create. (Guest creation is idempotent on its id; events are not.)

Response contains the full saved record; save the id.

Reading an event

GET /api/event/{eventId} returns the full record — including tags (joined to a comma-separated string), which the list endpoint omits.

curl 'https://api.qflowhub.io/checkin/v1/api/event/{eventId}' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>'

Returns 404 if the id doesn't exist or you don't have access.

Listing events

GET /api/events returns events you own. Either pass no parameters to get them all (default page size 10), or filter to a date window:

curl 'https://api.qflowhub.io/checkin/v1/api/events?start=2026-06-01T00:00:00Z&end=2026-12-31T23:59:59Z' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>'

Both start and end are UTC. OData query parameters work too: ?$filter=, ?$orderby=, ?$top=, ?$skip=.

Note: tags is not populated on list responses — fetch the single-event endpoint above when you need it. timeZoneId is not returned on reads at all (neither list nor single); it's echoed only on the create / update response, so persist it yourself if you need it later.

Updating an event

PUT /api/event takes the full body — partial updates aren't supported on top-level fields. Note the following:

  • active is optional on PUT — omit it to leave the current state untouched; send true / false to set it. For state changes the Lifecycle endpoints are the dedicated path.
  • customData is a whole-string overwrite — a non-empty value replaces what's stored. A null / empty / whitespace value leaves the existing value unchanged, so PUT can't clear it (there's no wipe).
  • Tags can't be added via PUT. Use the Tags endpoint — it's the only way to add tags after creation; PUT ignores tag changes entirely.
curl -X PUT 'https://api.qflowhub.io/checkin/v1/api/event' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "id":         "<eventId>",
    "title":      "Soho House Summer Launch — Updated",
    "startTime":  "2026-07-15T19:00:00",
    "endTime":    "2026-07-16T01:00:00",
    "timeZoneId": "Europe/London"
  }'

Where to next

  • Lifecycle — disable, enable, delete.
  • Tags — add to an event's tag set after creation.
  • Statistics — live attendee roll-up.
  • Tallies — per-event named counters.
Try it
GET/events GET/event/{eventId} PUT/event POST/event
Events API

Event lifecycle

Disabling, enabling, and deleting events — the state-change endpoints that take just an id, not the whole event body.

Concept

The CRUD operations on Events work against the full event body — title, dates, custom data and so on. State changes (toggling visibility, removing the record) go through dedicated endpoints that take just the event id. They're separate for two reasons:

  • It's a much smaller payload — no risk of a stale event body overwriting concurrent edits.
  • PUT /api/event only changes active when you include it — omitting it leaves the state untouched. These endpoints are the explicit, one-field way to flip it.

Disable

Disabling hides the event from the check-in app and pauses any registration links pointing at it. Attendee data and tally history are preserved untouched; you can re-enable at any time.

curl -X POST 'https://api.qflowhub.io/checkin/v1/api/event/disable' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": "<eventId>" }'

Idempotent — calling it on an already-disabled event returns success with no change.

Enable

Symmetric to disable. Re-publishes the event without re-creating any of the underlying records.

curl -X POST 'https://api.qflowhub.io/checkin/v1/api/event/enable' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": "<eventId>" }'

Delete

Deletion is permanent. Once the event is deleted, attendees, tallies, and history go with it — there's no recovery via the API.

curl -X DELETE 'https://api.qflowhub.io/checkin/v1/api/event' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": "<eventId>" }'

For "take this offline" workflows, prefer disable — it's reversible and preserves attendee history (useful for re-using bookings, refunding, or re-issuing tickets later). Reserve delete for events that genuinely shouldn't exist (test runs, accidental creates).

Where to next

  • Tags — add to an event's tag set after creation.
  • Statistics — read attendee roll-ups for a disabled or active event.
Try it
DELETE/event POST/event/disable POST/event/enable
Events API

Tags

The full tag taxonomy — plain, prefixed, tree — plus the dedicated endpoint for adding tags to an event's roster.

Concept

Tags are labels you attach to attendees that drive filtering, statistics grouping, and check-in scanner display. A tag can be anything (VIP, Speaker, Vegan), but prefixed tags trigger special behaviour in the Qflow dashboard and scanner apps — icons, statistics grouping, and whether a tag counts toward headline guest totals.

Tags live on the event — assignment happens at the attendee level (see Guests), but the taxonomy is event-scoped.

Plain tags

Any string without a special prefix is a plain tag. Filtering, search, and reports work fine on plain tags; the only things they don't get are special icons or grouping.

"tags": "VIP, Speaker, Vegan"

Prefixed tags

Five prefixes carry meaning. Add them at the start of the tag name:

Prefix Meaning Icon Counts toward total?
{tickettype} Ticket type (Main entry, Day pass, Comp, …)
{session} Session / time-slot / room
{extra} Extra add-on (parking, locker, plus-one upgrade)
{merch} Merchandise (t-shirt size, swag bag)
{rsvp} RSVP status

Example:

"tags": "{tickettype}Main Entry, {session}Keynote, {extra}Parking, {merch}T-Shirt L"

The dashboard shows the tag with its icon and groups statistics by prefix. Scanner apps pick up the same convention.

Why "counts toward total?" matters

Headline guest totals on event statistics show "X of Y" — how many of the actual attendees have arrived. {extra} and {merch} tags don't represent extra people (they're add-ons attached to an existing attendee), so they're excluded from the headline count. Plain tags and the other three prefixes count.

Tree tags

For hierarchical classification (e.g. session schedule with multiple sub-sessions), use the :: tree-tag format:

"tags": "::Sessions:Day 1:Keynote, ::Sessions:Day 2:Workshop A"

The dashboard renders these in a collapsible tree under a top-level Sessions header. Useful when you've got 50+ session tags and a flat list would overwhelm the UI.

Non-countable tree tags

Suffix a tree-tag leaf with *- to mark it non-countable (same effect as {extra} / {merch} — excluded from headline totals):

"tags": "::Merch:T-Shirt:Large*-"

Adding tags to an event

POST /api/events/tags is the only way to grow the tag set after creation — PUT /api/event ignores tag changes. Accepts a single name or a comma-separated list. Tags already present are skipped silently — no duplicates, no error.

curl -X POST 'https://api.qflowhub.io/checkin/v1/api/events/tags' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "eventId": "<eventId>",
    "tagName": "Hosted, Comp, {tickettype}VIP, ::Sessions:Day 1:Keynote"
  }'

Returns 201 whether or not anything new was added. Returns 404 if the event doesn't exist or you don't have access.

Leading / trailing whitespace is trimmed; tag names are otherwise stored verbatim.

There's no removal endpoint — the only way to drop a tag is to delete every attendee carrying it, then re-create the event. Plan the taxonomy up front where possible.

Querying by tag

For attendee retrieval by tag, see Guests → Lookup & search — specifically GET /api/guests/{eventId}/bytag.

Where to next

  • Statistics — read per-tag attendance counts.
  • Guests — assign tags to attendees on create or upsert.
Try it
POST/events/tags
Events API

Statistics

Read the live attendee roll-up for an event — invited, checked in, no-shows, per tag.

Concept

The statistics endpoint gives you a per-event headcount roll-up in one call: total countable attendees, how many have been admitted (checked in), the average check-in rate, and a time-bucketed arrival timeline. Useful for a monitoring panel or a post-event report.

Results are cached for three hours per (eventId, tagId) pair — the first call computes fresh against the attendee table, and calls within that window return the cached snapshot. Treat it as a periodic roll-up, not a real-time counter (see Caching).

Reading statistics

GET /api/event/{eventId}/statistics returns the roll-up. An optional ?tagId=<guid> query parameter restricts the counts to attendees carrying that one tag.

curl 'https://api.qflowhub.io/checkin/v1/api/event/{eventId}/statistics?tagId=<tagId>' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>'

Response:

  • total — total countable attendees on the event (includes plus-ones and tally entries).
  • admitted — how many have been admitted / checked in (includes their plus-ones and tally additions).
  • averageCheckinsPerMin — average check-ins per minute across the event run (1-minute buckets, excluding the final partial minute).
  • arrivalActivity — a time-bucketed arrival timeline. Bucket size is chosen dynamically (5 minutes for short events, ~10% of runtime for longer ones).
{
  "total": 420,
  "admitted": 287,
  "averageCheckinsPerMin": 4.2,
  "arrivalActivity": [
    { "admitted": "19:00", "admittedCount": 38 },
    { "admitted": "19:05", "admittedCount": 51 }
  ]
}

Non-countable tags (those starting with {extra} / {merch}, or ending with *-) don't add to total / admitted — see Tags. There's no per-tag breakdown in a single response; call again with ?tagId= for each tag you want a slice of.

Returns 204 No Content when the event has no attendees and no tally entries.

Caching

The roll-up is cached for three hours per (eventId, tagId). The first call after the cache expires computes fresh; every call within the window returns the same snapshot — so a check-in mid-window won't appear until the cache turns over. That makes this a periodic summary, not a real-time counter: polling faster than the cache window returns the same numbers. There's no rate limit beyond the standard APIM quotas, but there's nothing to gain by polling tightly.

Where to next

  • Tags — set up the tag conventions that drive the perTag breakdown.
  • Attendee-level reads live on the Guests surface.
Try it
GET/event/{eventId}/statistics
Events API

Tallies

Per-event named counters with an append-only +1 / −1 ledger.

Concept

A tally is a named counter attached to an event — "Drinks served", "VIP plus-ones used", "Coat-check tickets issued". Each tally has a one-time-created header and an append-only ledger of +1 / −1 rows. The total is computed on read: count(+1) − count(−1).

Tallies are not attendees — they're for things you want a running count of independent of who used them. If you need a per-attendee counter, use a custom field on the guest record instead.

Creating tally headers

POST /api/tallies/{eventId} registers one or more named counters on the event. The body is a single object whose name is one header name or a comma-separated list. Re-posting a name that already exists is skipped — no duplicate, no error.

curl -X POST 'https://api.qflowhub.io/checkin/v1/api/tallies/{eventId}' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{ "name": "Drinks served, Coat-check tickets" }'

Response includes each header's server-assigned tallyId — that's what you increment / decrement against.

Listing totals

GET /api/tallies/{eventId} returns every tally header on the event with its current running total computed from the ledger:

curl 'https://api.qflowhub.io/checkin/v1/api/tallies/{eventId}' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>'

Totals are computed live on each call — no cache, no eventual-consistency window. Inexpensive to poll for a live dashboard.

Incrementing / decrementing

Each call appends a new ledger row:

curl -X POST 'https://api.qflowhub.io/checkin/v1/api/tallies/increment/{tallyId}' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>'

POST /api/tallies/decrement/{tallyId} is symmetric — appends a −1 row.

There's no batch / multi-increment endpoint; loop client-side if you need to move the counter by more than one.

Operational notes

  • No idempotency — retries double-count. Each call appends a row, and the endpoint accepts no request-id or dedupe token, so a retry after a failed or timed-out increment moves the counter a second time. Over an unreliable network, wrap each call in your own dedupe layer — typically a UUID per logical action, held client-side until the server confirms. Where the count is financially significant this matters; where it's only for staff awareness, the occasional over-count is usually acceptable.

  • Totals can go negative. Decrementing past zero isn't clamped — sometimes that's intended (refunds, returned tickets), sometimes it indicates a double-count bug in the caller.

  • No bulk import. Tallies are intended for live runtime counting, not for back-filling historical totals. To seed a starting total, issue the increments in a loop at startup — the ledger retains those entries permanently.

Try it
GET/tallies/{eventId} POST/tallies/{eventId} POST/tallies/increment/{tallyId} POST/tallies/decrement/{tallyId}
Events API

API Reference

Interactive reference for the Events surface — event CRUD plus the tally-counter ledger that lives on each event.