HereMe is not open to the public yet. This is the V1 reference for systems preparing to work with a HereMe organisation: you can call the API once that organisation gives you a key from its HereMe console.
Through this API your system can, for one HereMe organisation:
- create or import events — and, if the organisation wants, list them on HereMe's Discover by place, with prices and ticket types (§9.1);
- invite existing HereMe users to a closed event (§9.6);
- make existing HereMe users ticket holders, by their HereMe ID;
- update tickets (type, status, your attendee reference);
- read HereMe's check-in status.
HereMe stays the authority for identity, admission and check-in. Your ids are references only. HereMe never creates an account from your request, and you never learn more about a person than you sent and the check-in state.
The examples use an obviously fake key, hm_live_0123456789abcdef0123456789abcdef.EXAMPLE-ONLY-do-not-use-xxxxxxxxxxxxxxxxxxx, read from the environment variable HEREME_API_KEY. The ids, HereMe IDs and times in the answers are illustrations.
1. Quick start#
1.1 Before you start#
- The organisation is on HereMe, and one of its owners or admins makes the key: only they see the console's Integrations page.
- Each ticket holder already has the HereMe app and a HereMe ID (
HM-XXXX-XXXX). Your shop asks for it at purchase; the person reads it in their HereMe app. - Check-in needs gates. An event you create through the API admits nobody until someone in the organisation's console opens the event (Events), chooses Edit and ticks the gates this event uses. The API does not know gate ids. Owners, admins and managers can do this.
1.2 Make a key in the console#
- Sign in at
https://console.hereme.meand open Integrations. - Choose New key and fill in:
- Name — so the organisation knows which system uses it (up to 60 characters).
- Ticketing system — your provider name: 2–40 characters, lower-case letters, digits,
-or_, starting with a letter or digit (for exampleticketco). It namespaces your event ids (§3.4). - What it may do — one or more scopes (§3.3). Choose only what you need.
- Choose Create key. Creating credentials needs a recent sign-in, so the console may ask the person to confirm who they are first.
- Copy the key now. It is shown once and never again. Store it in your system's secret settings, then choose I've stored it.
A plan may cap the number of keys (max_integration_keys); the console then says "Your plan allows no more keys". One person can make at most 20 keys a day.
1.3 Your first call#
Create an event (needs events:write):
export HEREME_API_KEY='hm_live_0123456789abcdef0123456789abcdef.EXAMPLE-ONLY-do-not-use-xxxxxxxxxxxxxxxxxxx'
curl -sS https://api.hereme.me/v1/create-event \
-H "Authorization: Bearer $HEREME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"external_event_id": "EVT-2026-HARVEST",
"title": "Harvest Concert",
"location": "Main field",
"starts_at": "2026-10-10T16:00:00+03:00",
"ends_at": "2026-10-10T22:00:00+03:00"
}'201 Created:
{
"result": "created",
"event": {
"event_id": "01a0fc8b-7095-7d88-80d4-590d1ba8494c",
"external_event_id": "EVT-2026-HARVEST",
"title": "Harvest Concert",
"description": null,
"location": "Main field",
"starts_at": "2026-10-10T13:00:00+00:00",
"ends_at": "2026-10-10T19:00:00+00:00",
"status": "scheduled",
"tickets": 0,
"checked_in": 0
}
}Run it again with a new Idempotency-Key and the same body: the answer is 200 with "result": "unchanged", and nothing new is created. Then add a ticket holder (9.2) and read check-ins (9.5).
2. Basics#
- Base URL:
https://api.hereme.me/v1. - HTTPS only. A request over plain HTTP is refused before the key is read (
400 INVALID_REQUEST,details.reason: https_required). If you ever sent a key in the clear, revoke it. - JSON in and out. Send
Content-Type: application/json. A body is at most 1 MB (413). A body must be one JSON object. - Unknown fields are refused, not ignored:
create-event,add-attendeeandupdate-attendeebodies and theread-check-in-statusquery accept only the fields listed for them (400 INVALID_REQUEST,details.field: null). - Server to server. Call the API from your back end. It does not allow browser calls from other websites (CORS), and a key must never reach a browser or a mobile app.
- Response headers:
X-Request-Idon every response. Quote it when you contact HereMe. You may send your own:req_followed by 1–64 letters, digits,_or-; anything else is replaced by one HereMe makes.Cache-Control: private, no-store.Idempotent-Replayed: trueon a replayed write (§4).WWW-Authenticate: Bearer realm="HereMe Event Integration API"on a401;Retry-After: 60on a429.
- The order of checks, for every call: HTTPS → the key → the per-key rate limit →
Idempotency-Key(writes) → the body (size, JSON, shape, no organisation) → then, inside HereMe: the scope → a suspended organisation (writes) → the idempotency record → the operation. So a key without the scope that sends a malformed body gets400, not403. - Key order in JSON answers is not significant.
3. Authentication and scopes#
3.1 The key#
hm_live_0123456789abcdef0123456789abcdef.EXAMPLE-ONLY-do-not-use-xxxxxxxxxxxxxxxxxxx
└──────── key id: 32 hex ───────┘ └──────── secret: 43 base64url ─────────┘Send it on every request:
Authorization: Bearer hm_live_<key id>.<secret>The key id is lower-case hex. The secret is 32 random bytes, base64url without padding. HereMe stores only the key id and the SHA-256 of the secret: nobody at HereMe can show the key again. A lost key is revoked and replaced.
3.2 The key decides the organisation#
A key belongs to one organisation. A body or query that names one (org_id, organization_id, organisation_id, tenant_id or account_id) is refused, not followed (400 INVALID_REQUEST, details.reason: organisation_from_key). Another organisation's event is EVENT_NOT_FOUND, even by its exact event_id.
3.3 Scopes#
| Scope | Allows | Console label |
|---|---|---|
events:write | POST /create-event; event records in POST /import | Create and change events |
attendees:write | POST /add-attendee, POST /update-attendee; attendee records in POST /import | Add and change ticket holders |
checkins:read | GET /read-check-in-status | Read check-ins |
A call outside the key's scopes is 403 FORBIDDEN with details.scope.
3.4 Provider namespace#
Your external_event_id is unique per organisation and provider. The provider is the key's, set when the key was made. Two ticketing systems serving one organisation never collide, and you only ever find your own events by external_event_id. (By HereMe's event_id you can name any event of the organisation.)
3.5 Rotating and revoking#
- Rotate: make a new key with the same provider, switch your system to it, then revoke the old one. Your events stay yours.
- Revoke: an owner or admin chooses Revoke on the Integrations page. It is immediate and final. A revoked key answers
401 API_KEY_REVOKED— said only to whoever still holds its secret; to anyone else it isINVALID_API_KEY. - A key whose organisation was closed answers
INVALID_API_KEY. A suspended organisation's key still reads, but every write is403 FORBIDDEN(details.reason: org_suspended).
4. Idempotency#
Every write (POST) needs an Idempotency-Key header: a UUID you make once per intended write and reuse on every retry of that write. Without one (or with something that is not a UUID) the write is refused:
{
"error": {
"code": "INVALID_REQUEST",
"message": "An Idempotency-Key header (a UUID) is required on every write.",
"details": { "field": "Idempotency-Key" },
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d"
}
}- Within 24 hours, a retry with the same key and the same request answers the first answer again — the same status and body, even if things changed since — with the header
Idempotent-Replayed: true. Nothing is done twice. - "The same request" means the same endpoint and the same JSON content. Key order and white space do not matter.
- The same key with a different body, or on another endpoint, is refused:
400 INVALID_REQUEST,details.reason: idempotency_key_reused. - A request that failed did nothing and is not remembered. Retrying it with the same key runs it again. Fix the cause first.
- Keys are remembered per API key. After 24 hours a key may be used again for a new request.
GET /read-check-in-statustakes noIdempotency-Key.
Your own ids are natural keys too:
create-eventwith an event that exists exactly as sent answers it (200,result: unchanged); with other details it is409 DUPLICATE_EVENT.add-attendeewith a ticket that exists exactly as sent answers it (200,result: unchanged).importcreates or updates by your ids, so sending the same file twice is safe.
5. Pagination#
Only GET /read-check-in-status pages. Without external_ticket_id it answers a page of the event's tickets in external_ticket_id order:
limit: 1–500, default 100.- The answer's
next_cursoris the lastexternal_ticket_idof a full page, otherwisenull. Pass it back ascursoruntil it isnull. - A full last page is followed by one empty page with
next_cursor: null. - Tickets added while you page appear if their
external_ticket_idsorts after your cursor.
6. Rate limits#
| Limit | Counted | Then |
|---|---|---|
| 120 requests a minute | per key | 429 RATE_LIMITED |
| 20 refused keys a minute | per client IP | 429 RATE_LIMITED instead of 401 |
| 50 unknown HereMe IDs an hour | per key | every new holder lookup — known ID or not — is 429 RATE_LIMITED (details.reason: unknown_hereme_ids) until the hour turns (UTC) |
- Every
429carriesRetry-After: 60. Forunknown_hereme_idsthe budget frees at the top of the hour, so waiting 60 seconds may not be enough. - The per-minute limits are counted per Cloudflare location: treat them as a brake, not as exact accounting. Spread bulk work with
/import(one request, up to 500 records). - The HereMe ID budget is exact. It exists so a key cannot sweep HereMe IDs: check IDs with your customers, not by trial.
7. Times and zones#
- Send times as ISO-8601 with an offset:
2026-10-10T16:00:00Zor2026-10-10T19:00:00+03:00. Seconds and up to six decimal places are optional (2026-10-10T16:00Zis fine). A time without a zone (2026-10-10T16:00:00,2026-10-10 16:00) is refused (details.field: starts_at) rather than guessed. - HereMe answers in UTC, as
2026-10-10T13:00:00+00:00; check-in times carry microseconds (2026-10-10T13:42:05.418204+00:00). Convert to the event's local zone yourself. ends_atmust be afterstarts_at, and an event lasts at most 31 days.- Ticket holders are checked in from two hours before
starts_atuntilends_at(§11).
8. Errors#
Every error has one shape:
{
"error": {
"code": "HERE_ME_USER_NOT_FOUND",
"message": "No HereMe user with this HereMe ID.",
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d"
}
}detailsappears only when it says more:field(the field at fault —nullfor a field that should not be there),reason,scope, or a conflicting id.- Branch on
codeanddetails, never onmessage: the words may change. - Tolerate
details.reasonvalues you do not know; new ones may be added (§15).
| HTTP | code | When |
|---|---|---|
| 400 | INVALID_REQUEST | A field is missing or out of shape (details.field), or details.reason is one of: https_required, not_json, not_an_object, organisation_from_key, idempotency_key_reused, identity_immutable, event_cancelled, event_closed, event_not_closed, pricing_locked, capacity_below_taken, not_an_institution. A missing Idempotency-Key is details.field: "Idempotency-Key" |
| 401 | INVALID_API_KEY | No key, a malformed key, an unknown key, a wrong secret, or a closed organisation |
| 401 | API_KEY_REVOKED | The key was revoked (said only to someone holding its secret) |
| 403 | FORBIDDEN | The key lacks the scope (details.scope); the organisation is suspended (writes only; details.reason: org_suspended); its plan allows no more active events (details.reason: plan_limit_reached, details.entitlement: max_active_events, details.limit) |
| 404 | EVENT_NOT_FOUND | No such event in this organisation |
| 404 | TICKET_NOT_FOUND | No ticket with this external_ticket_id for this event |
| 404 | HERE_ME_USER_NOT_FOUND | No active HereMe user with this HereMe ID. It says nothing more |
| 409 | DUPLICATE_EVENT | Your external_event_id exists with other details (details.event_id) |
| 409 | DUPLICATE_TICKET | details.reason: external_ticket_id_taken (with details.ticket_id), holder_has_ticket, or concurrent (the same ticket was being added at the same moment) |
| 409 | CHECK_IN_NOT_ALLOWED | Reserved. No V1 endpoint returns it: check-in happens only at HereMe gates (§11) |
| 413 | INVALID_REQUEST | More than 1 MB (details.reason: body_too_large) |
| 429 | RATE_LIMITED | See §6 |
| 503 | UNAVAILABLE | HereMe's own fault. Retry later with the same Idempotency-Key |
What to do: fix and resend on 400/404/409 (with a new Idempotency-Key if you changed the body); stop and tell the organisation on 401/403; wait Retry-After on 429; back off and retry with the same Idempotency-Key on 503 and on network errors.
8.1 Example bodies#
400 — a field out of shape:
{ "error": { "code": "INVALID_REQUEST", "message": "The request is not valid.",
"details": { "field": "starts_at" }, "request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }400 — a check HereMe makes after the shape (the words say which):
{ "error": { "code": "INVALID_REQUEST", "message": "ends_at is after starts_at",
"details": { "field": "ends_at" }, "request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }400 — the holder cannot change:
{ "error": { "code": "INVALID_REQUEST", "message": "A ticket's holder never changes: void it and add a new ticket.",
"details": { "field": "hereme_id", "reason": "identity_immutable" }, "request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }401:
{ "error": { "code": "INVALID_API_KEY", "message": "The API key is missing or not valid.",
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }{ "error": { "code": "API_KEY_REVOKED", "message": "This API key has been revoked.",
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }403:
{ "error": { "code": "FORBIDDEN", "message": "This key does not have the checkins:read scope.",
"details": { "scope": "checkins:read" }, "request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }{ "error": { "code": "FORBIDDEN", "message": "The organisation's plan allows no more of these.",
"details": { "reason": "plan_limit_reached", "entitlement": "max_active_events", "limit": 3 },
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }{ "error": { "code": "FORBIDDEN", "message": "This organisation is suspended.",
"details": { "reason": "org_suspended" }, "request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }404:
{ "error": { "code": "EVENT_NOT_FOUND", "message": "No event with this id in this organisation.",
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }{ "error": { "code": "TICKET_NOT_FOUND", "message": "No ticket with this external_ticket_id for this event.",
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }409:
{ "error": { "code": "DUPLICATE_EVENT", "message": "An event with this external_event_id exists, with other details.",
"details": { "event_id": "01a0fc8b-7095-7d88-80d4-590d1ba8494c" }, "request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }{ "error": { "code": "DUPLICATE_TICKET", "message": "This HereMe user already holds a ticket for this event.",
"details": { "reason": "holder_has_ticket" }, "request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }{ "error": { "code": "DUPLICATE_TICKET", "message": "This external_ticket_id is another holder's ticket.",
"details": { "reason": "external_ticket_id_taken", "ticket_id": "01a0fc8c-ccca-72f4-9f94-1bfbbd9f1a44" },
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }413:
{ "error": { "code": "INVALID_REQUEST", "message": "The body is larger than 1 MB.",
"details": { "reason": "body_too_large" }, "request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }429:
{ "error": { "code": "RATE_LIMITED", "message": "Too many requests. Wait a minute and try again.",
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }{ "error": { "code": "RATE_LIMITED", "message": "Too many unknown HereMe IDs from this key. Try again later.",
"details": { "reason": "unknown_hereme_ids" }, "request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }503:
{ "error": { "code": "UNAVAILABLE", "message": "HereMe is unavailable right now. Try again.",
"request_id": "req_01a0fc8d-1f2e-7a3b-8c4d-5e6f7a8b9c0d" } }9. Endpoints#
| Method and path | Scope | Success |
|---|---|---|
POST /v1/create-event | events:write | 201 created · 200 unchanged |
POST /v1/add-attendee | attendees:write | 201 created · 200 unchanged |
POST /v1/update-attendee | attendees:write | 200 updated or unchanged |
POST /v1/import | those of its records | 200 with a result per record |
GET /v1/read-check-in-status | checkins:read | 200 |
POST /v1/invite-attendee | attendees:write | 201 created · 200 unchanged |
Shared field rules:
- Your ids (
external_event_id,external_ticket_id,external_attendee_id,cursor): 1–128 printable characters, no leading or trailing spaces, kept exactly as sent. - Naming an event (every call except
create-event): send yourexternal_event_id, or HereMe'sevent_id(a UUID), or both — then both must match the same event. - HereMe IDs are forgiving: case, spaces and hyphens do not matter, and
O,IandLread as0,1and1(hm 7k3q 9xwtworks). The check symbol is verified: a mistyped ID is400 INVALID_REQUEST(details.field: hereme_id), not a lookup. Organisation IDs (HMB-…) are not accepted.
9.1 POST /v1/create-event#
Creates an event under your external_event_id. Scope events:write.
| Field | Type | |
|---|---|---|
external_event_id | string | Required. Your id |
title | string | Required. 1–160 characters |
description | string or null | Up to 2,000 characters |
location | string or null | Up to 200 characters |
starts_at | time with offset | Required |
ends_at | time with offset | Required. After starts_at, at most 31 days later |
status | string | draft, scheduled (default), live, ended or cancelled |
- Sending the same event again answers it (
200,result: unchanged). Different details under the same id are409 DUPLICATE_EVENT(details.event_id). Change events through/import. - HereMe makes the
event_id; you cannot send one. - A
scheduledorliveevent that has not ended counts against the organisation's plan (max_active_events); a plan at its limit answers403 FORBIDDEN(plan_limit_reached). Drafts, ended and cancelled events do not count.
Listing on Discover (optional, since 2026-10-03). Leave these out and the event is as before: unlisted, free, known only to the organisation and its ticket holders. Send them to list it on HereMe's Discover, where people find events by place (never by searching, and never seeing who goes):
| Field | Type | |
|---|---|---|
visibility | string | unlisted (default), open (discoverable by place) or closed (by invitation, §9.6) |
published_from | time with offset or null | When a listed event becomes discoverable; default: when it is first listed |
country_code | string | ISO 3166-1 alpha-2, e.g. KE. Required to list |
city | string | 1–80 characters. Required to list |
area | string or null | A district or neighbourhood, up to 80 characters |
address | string | Up to 300 characters. Required to list |
latitude, longitude | numbers or null | The pin, both or neither |
time_zone | string | IANA, e.g. Africa/Nairobi. Required to list |
pricing | string | free (default), sponsored or paid |
currency | string or null | KES for sponsored and paid; none for free |
capacity | integer or null | Free and sponsored: seats, first come (1–1,000,000) |
ticket_types | list or null | Paid: 1–10 of {"name", "price_minor", "capacity", "on_sale"}, matched by name; price_minor in cents of a shilling, whole shillings only (KES 1,000 is 100000) |
- Only an institution may list an event (
not_an_institution). - People take part, or buy tickets with M-Pesa, in the HereMe app; their tickets appear in
read-check-in-statuswithsourceparticipationorpurchaseand a HereMe-madeexternal_ticket_id(hm-…). - Pricing and a sold type's price lock once anything was taken or paid (
pricing_locked); a capacity never goes below the seats taken (capacity_below_taken). /v1/importchanges the listing only when a listing field is sent: an event record without any keeps the listing it has.- Every event answer also carries
visibility,slug,url(https://hereme.me/e/<slug>),published_from,place,time_zone,pricing,currency,capacityandticket_types(withseats_taken). Posters are uploaded in the console. - HereMe never moves an event's status by itself. An event past its
ends_atsimply stops admitting.
curl
curl -sS https://api.hereme.me/v1/create-event \
-H "Authorization: Bearer $HEREME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a4e-8b0d-4c7e-9a35-2d1f0b7e4c18" \
-d '{
"external_event_id": "EVT-2026-HARVEST",
"title": "Harvest Concert",
"description": "Gates open at 15:00.",
"location": "Main field",
"starts_at": "2026-10-10T16:00:00+03:00",
"ends_at": "2026-10-10T22:00:00+03:00",
"status": "scheduled"
}'JavaScript
import { randomUUID } from 'node:crypto';
const idempotencyKey = randomUUID(); // keep it with this write; reuse it on retries
const res = await fetch('https://api.hereme.me/v1/create-event', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HEREME_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
external_event_id: 'EVT-2026-HARVEST',
title: 'Harvest Concert',
description: 'Gates open at 15:00.',
location: 'Main field',
starts_at: '2026-10-10T16:00:00+03:00',
ends_at: '2026-10-10T22:00:00+03:00',
status: 'scheduled',
}),
});
console.log(res.status, await res.json());Python
import os
import uuid
import requests
idempotency_key = str(uuid.uuid4()) # keep it with this write; reuse it on retries
res = requests.post(
"https://api.hereme.me/v1/create-event",
headers={
"Authorization": f"Bearer {os.environ['HEREME_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json={
"external_event_id": "EVT-2026-HARVEST",
"title": "Harvest Concert",
"description": "Gates open at 15:00.",
"location": "Main field",
"starts_at": "2026-10-10T16:00:00+03:00",
"ends_at": "2026-10-10T22:00:00+03:00",
"status": "scheduled",
},
timeout=30,
)
print(res.status_code, res.json())Response — 201 Created:
{
"result": "created",
"event": {
"event_id": "01a0fc8b-7095-7d88-80d4-590d1ba8494c",
"external_event_id": "EVT-2026-HARVEST",
"title": "Harvest Concert",
"description": "Gates open at 15:00.",
"location": "Main field",
"starts_at": "2026-10-10T13:00:00+00:00",
"ends_at": "2026-10-10T19:00:00+00:00",
"status": "scheduled",
"tickets": 0,
"checked_in": 0
}
}9.2 POST /v1/add-attendee#
Makes an existing HereMe user the holder of a ticket. Scope attendees:write.
| Field | Type | |
|---|---|---|
external_event_id or event_id | string | Required (one of them) |
hereme_id | string | Required. The holder's HereMe ID |
external_ticket_id | string | Required. Your ticket id, unique within the event |
external_attendee_id | string or null | Your customer id |
ticket_type | string or null | 1–60 characters, e.g. VIP |
status | string | valid (default), void or refunded |
- No active person with that ID:
404 HERE_ME_USER_NOT_FOUND. It counts against the hourly budget (§6). - One person holds one ticket per event: a second is
409 DUPLICATE_TICKET(holder_has_ticket). - The same
external_ticket_idwith another holder or other details is409 DUPLICATE_TICKET(external_ticket_id_taken, withdetails.ticket_id). The same ticket exactly as sent answers it (200,result: unchanged). - A cancelled event takes no new tickets (
400,details.reason: event_cancelled).
curl
curl -sS https://api.hereme.me/v1/add-attendee \
-H "Authorization: Bearer $HEREME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9b2d7e10-4f3a-4c61-8e5b-0a7c3d9f2b64" \
-d '{
"external_event_id": "EVT-2026-HARVEST",
"hereme_id": "HM-7K3Q-9XWT",
"external_ticket_id": "TCK-000417",
"external_attendee_id": "CUST-88",
"ticket_type": "VIP"
}'JavaScript
import { randomUUID } from 'node:crypto';
const res = await fetch('https://api.hereme.me/v1/add-attendee', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HEREME_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify({
external_event_id: 'EVT-2026-HARVEST',
hereme_id: 'HM-7K3Q-9XWT',
external_ticket_id: 'TCK-000417',
external_attendee_id: 'CUST-88',
ticket_type: 'VIP',
}),
});
console.log(res.status, await res.json());Python
import os
import uuid
import requests
res = requests.post(
"https://api.hereme.me/v1/add-attendee",
headers={
"Authorization": f"Bearer {os.environ['HEREME_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"external_event_id": "EVT-2026-HARVEST",
"hereme_id": "HM-7K3Q-9XWT",
"external_ticket_id": "TCK-000417",
"external_attendee_id": "CUST-88",
"ticket_type": "VIP",
},
timeout=30,
)
print(res.status_code, res.json())Response — 201 Created:
{
"result": "created",
"ticket": {
"ticket_id": "01a0fc8c-ccca-72f4-9f94-1bfbbd9f1a44",
"event_id": "01a0fc8b-7095-7d88-80d4-590d1ba8494c",
"external_event_id": "EVT-2026-HARVEST",
"external_ticket_id": "TCK-000417",
"external_attendee_id": "CUST-88",
"hereme_id": "HM-7K3Q-9XWT",
"ticket_type": "VIP",
"status": "valid",
"check_in": { "checked_in": false, "checked_in_at": null, "gate_name": null, "device_label": null }
}
}hereme_id comes back in its canonical form, whatever spacing you sent.
9.3 POST /v1/update-attendee#
Changes a ticket's ticket_type, status or external_attendee_id. Scope attendees:write.
| Field | Type | |
|---|---|---|
external_event_id or event_id | string | Required (one of them) |
external_ticket_id | string | Required. The ticket to change |
status | string | valid, void or refunded |
ticket_type | string or null | null clears it |
external_attendee_id | string or null | null clears it |
hereme_id | string | Optional; if sent, it must be the holder's |
- Only the fields you send change. Nothing to change answers
"result": "unchanged". - The holder never changes. A different
hereme_idis400 INVALID_REQUEST(identity_immutable). To give a ticket to someone else, void it and add a new ticket with a newexternal_ticket_id. - Voiding or refunding a ticket that was already used keeps its check-in. A void or refunded ticket is refused at the gate.
- No such ticket:
404 TICKET_NOT_FOUND.
curl
curl -sS https://api.hereme.me/v1/update-attendee \
-H "Authorization: Bearer $HEREME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2c8e4b6a-1d7f-4e93-b0a2-5f6c8d1e3a79" \
-d '{
"external_event_id": "EVT-2026-HARVEST",
"external_ticket_id": "TCK-000417",
"status": "refunded"
}'JavaScript
import { randomUUID } from 'node:crypto';
const res = await fetch('https://api.hereme.me/v1/update-attendee', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HEREME_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify({
external_event_id: 'EVT-2026-HARVEST',
external_ticket_id: 'TCK-000417',
status: 'refunded',
}),
});
console.log(res.status, await res.json());Python
import os
import uuid
import requests
res = requests.post(
"https://api.hereme.me/v1/update-attendee",
headers={
"Authorization": f"Bearer {os.environ['HEREME_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"external_event_id": "EVT-2026-HARVEST",
"external_ticket_id": "TCK-000417",
"status": "refunded",
},
timeout=30,
)
print(res.status_code, res.json())Response — 200 OK:
{
"result": "updated",
"ticket": {
"ticket_id": "01a0fc8c-ccca-72f4-9f94-1bfbbd9f1a44",
"event_id": "01a0fc8b-7095-7d88-80d4-590d1ba8494c",
"external_event_id": "EVT-2026-HARVEST",
"external_ticket_id": "TCK-000417",
"external_attendee_id": "CUST-88",
"hereme_id": "HM-7K3Q-9XWT",
"ticket_type": "VIP",
"status": "refunded",
"check_in": { "checked_in": false, "checked_in_at": null, "gate_name": null, "device_label": null }
}
}9.4 POST /v1/import#
Up to 500 records (and 1 MB), applied in order, each on its own. A record that fails leaves nothing behind and says why; the others still land. Records create or update by your ids, so an import is also how you change events.
{"type": "event", …}takes the fields ofcreate-eventand needsevents:write. An existing event is updated to what you send (fields you leave out become empty or default — send the whole event, itsstatusincluded).endedandcancelledare final: moving such an event to another status isINVALID_REQUEST(details.reason: event_closed).{"type": "attendee", …}takes the fields ofadd-attendeeand needsattendees:write. An existing ticket keeps its holder (a differenthereme_idisidentity_immutable); the fields you send change, the rest stay.- A record's event may be created earlier in the same import.
- If the key lacks a scope that any record needs, the whole import is refused (
403,details.scope), and nothing is applied. - The answer is
200even when records failed: checksummary.failedand eachresults[i].result. Failed records carryerror.code(anderror.message,error.details) as in §8. - Unknown HereMe IDs count against the hourly budget; past it, further new attendee records fail with
RATE_LIMITED. - A replay with the same
Idempotency-Keyanswers the same results.
curl
curl -sS https://api.hereme.me/v1/import \
-H "Authorization: Bearer $HEREME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4a9f1c3e-7b2d-4e8a-9c51-6d0e2f8b7a13" \
-d '{
"records": [
{ "type": "event", "external_event_id": "EVT-YOUTH", "title": "Youth Day",
"location": "Hall B", "starts_at": "2026-11-01T09:00:00+03:00", "ends_at": "2026-11-01T17:00:00+03:00" },
{ "type": "attendee", "external_event_id": "EVT-YOUTH", "hereme_id": "HM-4MZ8-2QPN",
"external_ticket_id": "Y-0001", "ticket_type": "Standard" },
{ "type": "attendee", "external_event_id": "EVT-YOUTH", "hereme_id": "HM-ZZZZ-ZZY9",
"external_ticket_id": "Y-0002", "ticket_type": "Standard" }
]
}'JavaScript
import { randomUUID } from 'node:crypto';
const res = await fetch('https://api.hereme.me/v1/import', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HEREME_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify({
records: [
{ type: 'event', external_event_id: 'EVT-YOUTH', title: 'Youth Day', location: 'Hall B',
starts_at: '2026-11-01T09:00:00+03:00', ends_at: '2026-11-01T17:00:00+03:00' },
{ type: 'attendee', external_event_id: 'EVT-YOUTH', hereme_id: 'HM-4MZ8-2QPN',
external_ticket_id: 'Y-0001', ticket_type: 'Standard' },
{ type: 'attendee', external_event_id: 'EVT-YOUTH', hereme_id: 'HM-ZZZZ-ZZY9',
external_ticket_id: 'Y-0002', ticket_type: 'Standard' },
],
}),
});
const { summary, results } = await res.json();
console.log(res.status, summary);
for (const r of results.filter((r) => r.result === 'failed')) console.log(r.index, r.error.code);Python
import os
import uuid
import requests
res = requests.post(
"https://api.hereme.me/v1/import",
headers={
"Authorization": f"Bearer {os.environ['HEREME_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"records": [
{"type": "event", "external_event_id": "EVT-YOUTH", "title": "Youth Day", "location": "Hall B",
"starts_at": "2026-11-01T09:00:00+03:00", "ends_at": "2026-11-01T17:00:00+03:00"},
{"type": "attendee", "external_event_id": "EVT-YOUTH", "hereme_id": "HM-4MZ8-2QPN",
"external_ticket_id": "Y-0001", "ticket_type": "Standard"},
{"type": "attendee", "external_event_id": "EVT-YOUTH", "hereme_id": "HM-ZZZZ-ZZY9",
"external_ticket_id": "Y-0002", "ticket_type": "Standard"},
]
},
timeout=60,
)
body = res.json()
print(res.status_code, body["summary"])
for r in body["results"]:
if r["result"] == "failed":
print(r["index"], r["error"]["code"])Response — 200 OK:
{
"summary": { "created": 2, "updated": 0, "unchanged": 0, "failed": 1 },
"results": [
{ "index": 0, "type": "event", "result": "created",
"event_id": "01a0fc90-3b1e-7c42-8a6d-2f9e0b4c7d15", "external_event_id": "EVT-YOUTH" },
{ "index": 1, "type": "attendee", "result": "created",
"ticket_id": "01a0fc90-3b2a-7e19-b4c3-8d5f1a6e2c90", "external_ticket_id": "Y-0001" },
{ "index": 2, "type": "attendee", "result": "failed", "external_ticket_id": "Y-0002",
"error": { "code": "HERE_ME_USER_NOT_FOUND", "message": "No HereMe user with this HereMe ID." } }
]
}9.5 GET /v1/read-check-in-status#
HereMe's word on check-in, for one ticket or a page of an event's tickets. Scope checkins:read. Query parameters:
| Parameter | |
|---|---|
external_event_id or event_id | Required (one of them) |
external_ticket_id | One ticket (then cursor and limit are ignored) |
limit | 1–500, default 100 |
cursor | The previous page's next_cursor |
URL-encode the values (the examples below do).
curl
# One ticket
curl -sS -G https://api.hereme.me/v1/read-check-in-status \
-H "Authorization: Bearer $HEREME_API_KEY" \
--data-urlencode "external_event_id=EVT-2026-HARVEST" \
--data-urlencode "external_ticket_id=TCK-000417"
# A page of the event's tickets
curl -sS -G https://api.hereme.me/v1/read-check-in-status \
-H "Authorization: Bearer $HEREME_API_KEY" \
--data-urlencode "external_event_id=EVT-2026-HARVEST" \
--data-urlencode "limit=200"JavaScript
const url = new URL('https://api.hereme.me/v1/read-check-in-status');
url.searchParams.set('external_event_id', 'EVT-2026-HARVEST');
url.searchParams.set('external_ticket_id', 'TCK-000417');
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.HEREME_API_KEY}` },
});
console.log(res.status, await res.json());Python
import os
import requests
res = requests.get(
"https://api.hereme.me/v1/read-check-in-status",
headers={"Authorization": f"Bearer {os.environ['HEREME_API_KEY']}"},
params={"external_event_id": "EVT-2026-HARVEST", "external_ticket_id": "TCK-000417"},
timeout=30,
)
print(res.status_code, res.json())Response — 200 OK (one ticket):
{
"event": {
"event_id": "01a0fc8b-7095-7d88-80d4-590d1ba8494c",
"external_event_id": "EVT-2026-HARVEST",
"title": "Harvest Concert",
"description": "Gates open at 15:00.",
"location": "Main field",
"starts_at": "2026-10-10T13:00:00+00:00",
"ends_at": "2026-10-10T19:00:00+00:00",
"status": "live",
"tickets": 412,
"checked_in": 288
},
"tickets": [
{
"ticket_id": "01a0fc8c-ccca-72f4-9f94-1bfbbd9f1a44",
"event_id": "01a0fc8b-7095-7d88-80d4-590d1ba8494c",
"external_event_id": "EVT-2026-HARVEST",
"external_ticket_id": "TCK-000417",
"external_attendee_id": "CUST-88",
"hereme_id": "HM-7K3Q-9XWT",
"ticket_type": "VIP",
"status": "valid",
"check_in": {
"checked_in": true,
"checked_in_at": "2026-10-10T13:42:05.418204+00:00",
"gate_name": "North gate",
"device_label": "Tablet 2"
}
}
],
"next_cursor": null
}A page has the same shape, with up to limit tickets in external_ticket_id order and next_cursor set when the page is full. The event's tickets counts every ticket, whatever its status; checked_in counts those checked in.
9.6 POST /v1/invite-attendee#
Invites an existing HereMe user, by HereMe ID, to a closed event (visibility: closed). Scope attendees:write. The invitation tells nobody anything about the person, and HereMe never pushes it: the person sees it in the app and takes part (or buys a ticket) themselves.
| Field | Type | |
|---|---|---|
external_event_id or event_id | string | Required (one of them) |
hereme_id | string | Required. The invitee's HereMe ID |
- The same person again answers the invitation (
200,result: unchanged). - Only a closed event takes invitations (
400,details.reason: event_not_closed); not a cancelled or ended one (event_cancelled,event_closed). - Someone who already holds a valid ticket is
409 DUPLICATE_TICKET(holder_has_ticket). - An unknown HereMe ID is
404 HERE_ME_USER_NOT_FOUNDand counts against the hourly budget (§6), as foradd-attendee.
curl
curl -sS https://api.hereme.me/v1/invite-attendee \
-H "Authorization: Bearer $HEREME_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5d3a1e7c-2b9f-4c86-a0e4-7f1b3c5d9e20" \
-d '{ "external_event_id": "EVT-2026-RETREAT", "hereme_id": "HM-7K3Q-9XWT" }'JavaScript
import { randomUUID } from 'node:crypto';
const res = await fetch('https://api.hereme.me/v1/invite-attendee', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HEREME_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify({ external_event_id: 'EVT-2026-RETREAT', hereme_id: 'HM-7K3Q-9XWT' }),
});
console.log(res.status, await res.json());Python
import os
import uuid
import requests
res = requests.post(
"https://api.hereme.me/v1/invite-attendee",
headers={
"Authorization": f"Bearer {os.environ['HEREME_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"external_event_id": "EVT-2026-RETREAT", "hereme_id": "HM-7K3Q-9XWT"},
timeout=30,
)
print(res.status_code, res.json())Response — 201 Created:
{
"result": "created",
"invitation": {
"invitation_id": "01a0fc91-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"event_id": "01a0fc8b-7095-7d88-80d4-590d1ba8494c",
"external_event_id": "EVT-2026-RETREAT",
"hereme_id": "HM-7K3Q-9XWT",
"status": "invited",
"invited_at": "2026-10-03T09:12:44.120531+00:00"
}
}10. Worked example: from import to check-in#
A ticket shop sells tickets to the Harvest Concert. It collects each buyer's HereMe ID at checkout, then:
- imports the event with the first buyers (one
/import); - adds a late buyer by HereMe ID (
/add-attendee); - upgrades one ticket and refunds another (
/update-attendee); - on the day, polls check-in status every minute and marks tickets used in its own system (
/read-check-in-status, page by page).
Between steps 1 and 4, someone in the organisation's console ticks the gates the event uses (§1.1).
Every write keeps its Idempotency-Key with the job, so a retry after a timeout or a 503 repeats nothing. 429 waits Retry-After.
curl (steps, one by one)
H=(-H "Authorization: Bearer $HEREME_API_KEY" -H "Content-Type: application/json")
# 1. Import the event and its first ticket holders
curl -sS https://api.hereme.me/v1/import "${H[@]}" -H "Idempotency-Key: $(uuidgen)" -d '{
"records": [
{ "type": "event", "external_event_id": "EVT-2026-HARVEST", "title": "Harvest Concert", "location": "Main field",
"starts_at": "2026-10-10T16:00:00+03:00", "ends_at": "2026-10-10T22:00:00+03:00" },
{ "type": "attendee", "external_event_id": "EVT-2026-HARVEST", "hereme_id": "HM-7K3Q-9XWT",
"external_ticket_id": "TCK-000417", "external_attendee_id": "CUST-88", "ticket_type": "Standard" },
{ "type": "attendee", "external_event_id": "EVT-2026-HARVEST", "hereme_id": "HM-4MZ8-2QPN",
"external_ticket_id": "TCK-000418", "external_attendee_id": "CUST-91", "ticket_type": "Standard" }
] }'
# 2. A late buyer
curl -sS https://api.hereme.me/v1/add-attendee "${H[@]}" -H "Idempotency-Key: $(uuidgen)" -d '{
"external_event_id": "EVT-2026-HARVEST", "hereme_id": "HM-Y1X2-W3VJ",
"external_ticket_id": "TCK-000419", "external_attendee_id": "CUST-102", "ticket_type": "Standard" }'
# 3. An upgrade and a refund
curl -sS https://api.hereme.me/v1/update-attendee "${H[@]}" -H "Idempotency-Key: $(uuidgen)" -d '{
"external_event_id": "EVT-2026-HARVEST", "external_ticket_id": "TCK-000417", "ticket_type": "VIP" }'
curl -sS https://api.hereme.me/v1/update-attendee "${H[@]}" -H "Idempotency-Key: $(uuidgen)" -d '{
"external_event_id": "EVT-2026-HARVEST", "external_ticket_id": "TCK-000418", "status": "refunded" }'
# 4. Check-in status, a page at a time (repeat with cursor=<next_cursor> until it is null)
curl -sS -G https://api.hereme.me/v1/read-check-in-status -H "Authorization: Bearer $HEREME_API_KEY" \
--data-urlencode "external_event_id=EVT-2026-HARVEST" --data-urlencode "limit=500"JavaScript — save as harvest.mjs, run node harvest.mjs:
import { randomUUID } from 'node:crypto';
const BASE = 'https://api.hereme.me/v1';
const KEY = process.env.HEREME_API_KEY;
const EVENT = 'EVT-2026-HARVEST';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/** One call. A write's Idempotency-Key is made once and reused on every retry. */
async function call(method, path, { body, query } = {}) {
const url = new URL(BASE + path);
for (const [name, value] of Object.entries(query ?? {})) url.searchParams.set(name, String(value));
const headers = { Authorization: `Bearer ${KEY}` };
if (method === 'POST') {
headers['Content-Type'] = 'application/json';
headers['Idempotency-Key'] = randomUUID();
}
for (let attempt = 1; ; attempt++) {
let res;
try {
res = await fetch(url, { method, headers, body: body && JSON.stringify(body) });
} catch (networkError) {
if (attempt >= 5) throw networkError;
await sleep(2 ** attempt * 1000);
continue;
}
const json = await res.json();
if (res.ok) return json;
if ((res.status === 429 || res.status === 503) && attempt < 5) {
await sleep(Number(res.headers.get('retry-after') ?? 2 ** attempt) * 1000);
continue;
}
const error = new Error(`${json.error.code}: ${json.error.message} (${json.error.request_id})`);
error.details = json.error.details;
throw error;
}
}
// 1. Import the event and its first ticket holders.
const imported = await call('POST', '/import', { body: { records: [
{ type: 'event', external_event_id: EVENT, title: 'Harvest Concert', location: 'Main field',
starts_at: '2026-10-10T16:00:00+03:00', ends_at: '2026-10-10T22:00:00+03:00' },
{ type: 'attendee', external_event_id: EVENT, hereme_id: 'HM-7K3Q-9XWT',
external_ticket_id: 'TCK-000417', external_attendee_id: 'CUST-88', ticket_type: 'Standard' },
{ type: 'attendee', external_event_id: EVENT, hereme_id: 'HM-4MZ8-2QPN',
external_ticket_id: 'TCK-000418', external_attendee_id: 'CUST-91', ticket_type: 'Standard' },
] } });
console.log('import', imported.summary);
for (const r of imported.results) if (r.result === 'failed') console.warn('record', r.index, r.error.code);
// 2. A late buyer.
const added = await call('POST', '/add-attendee', { body: {
external_event_id: EVENT, hereme_id: 'HM-Y1X2-W3VJ',
external_ticket_id: 'TCK-000419', external_attendee_id: 'CUST-102', ticket_type: 'Standard',
} });
console.log('added', added.result, added.ticket.ticket_id);
// 3. An upgrade and a refund.
await call('POST', '/update-attendee', { body: { external_event_id: EVENT, external_ticket_id: 'TCK-000417', ticket_type: 'VIP' } });
await call('POST', '/update-attendee', { body: { external_event_id: EVENT, external_ticket_id: 'TCK-000418', status: 'refunded' } });
// 4. Poll check-in status, page by page, once a minute until the event ends.
const used = new Set();
for (;;) {
let cursor;
let event;
do {
const page = await call('GET', '/read-check-in-status', {
query: { external_event_id: EVENT, limit: 500, ...(cursor ? { cursor } : {}) },
});
event = page.event;
for (const t of page.tickets) {
if (t.check_in.checked_in && !used.has(t.external_ticket_id)) {
used.add(t.external_ticket_id);
console.log(`${t.external_ticket_id} checked in at ${t.check_in.checked_in_at}, ${t.check_in.gate_name}`);
}
}
cursor = page.next_cursor;
} while (cursor);
console.log(`${event.checked_in} of ${event.tickets} checked in`);
if (new Date(event.ends_at) < new Date()) break;
await sleep(60_000);
}Python — save as harvest.py, run python harvest.py:
import os
import time
import uuid
from datetime import datetime, timezone
import requests
BASE = "https://api.hereme.me/v1"
KEY = os.environ["HEREME_API_KEY"]
EVENT = "EVT-2026-HARVEST"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {KEY}"
class HereMeError(Exception):
def __init__(self, error):
super().__init__(f"{error['code']}: {error['message']} ({error['request_id']})")
self.code = error["code"]
self.details = error.get("details", {})
def call(method, path, body=None, params=None):
"""One call. A write's Idempotency-Key is made once and reused on every retry."""
headers = {"Idempotency-Key": str(uuid.uuid4())} if method == "POST" else {}
for attempt in range(1, 6):
try:
res = session.request(method, BASE + path, json=body, params=params, headers=headers, timeout=60)
except requests.ConnectionError:
if attempt == 5:
raise
time.sleep(2 ** attempt)
continue
if res.ok:
return res.json()
if res.status_code in (429, 503) and attempt < 5:
time.sleep(int(res.headers.get("Retry-After", 2 ** attempt)))
continue
raise HereMeError(res.json()["error"])
# 1. Import the event and its first ticket holders.
imported = call("POST", "/import", body={"records": [
{"type": "event", "external_event_id": EVENT, "title": "Harvest Concert", "location": "Main field",
"starts_at": "2026-10-10T16:00:00+03:00", "ends_at": "2026-10-10T22:00:00+03:00"},
{"type": "attendee", "external_event_id": EVENT, "hereme_id": "HM-7K3Q-9XWT",
"external_ticket_id": "TCK-000417", "external_attendee_id": "CUST-88", "ticket_type": "Standard"},
{"type": "attendee", "external_event_id": EVENT, "hereme_id": "HM-4MZ8-2QPN",
"external_ticket_id": "TCK-000418", "external_attendee_id": "CUST-91", "ticket_type": "Standard"},
]})
print("import", imported["summary"])
for r in imported["results"]:
if r["result"] == "failed":
print("record", r["index"], r["error"]["code"])
# 2. A late buyer.
added = call("POST", "/add-attendee", body={
"external_event_id": EVENT, "hereme_id": "HM-Y1X2-W3VJ",
"external_ticket_id": "TCK-000419", "external_attendee_id": "CUST-102", "ticket_type": "Standard",
})
print("added", added["result"], added["ticket"]["ticket_id"])
# 3. An upgrade and a refund.
call("POST", "/update-attendee", body={"external_event_id": EVENT, "external_ticket_id": "TCK-000417", "ticket_type": "VIP"})
call("POST", "/update-attendee", body={"external_event_id": EVENT, "external_ticket_id": "TCK-000418", "status": "refunded"})
# 4. Poll check-in status, page by page, once a minute until the event ends.
used = set()
while True:
cursor = None
while True:
params = {"external_event_id": EVENT, "limit": 500}
if cursor:
params["cursor"] = cursor
page = call("GET", "/read-check-in-status", params=params)
event = page["event"]
for t in page["tickets"]:
if t["check_in"]["checked_in"] and t["external_ticket_id"] not in used:
used.add(t["external_ticket_id"])
print(f"{t['external_ticket_id']} checked in at {t['check_in']['checked_in_at']}, {t['check_in']['gate_name']}")
cursor = page["next_cursor"]
if not cursor:
break
print(f"{event['checked_in']} of {event['tickets']} checked in")
if datetime.fromisoformat(event["ends_at"]) < datetime.now(timezone.utc):
break
time.sleep(60)11. How check-in works (and why you only read it)#
- Ticket holders show their HereMe pass at the gate. The organisation's HereMe Gate tablet, at a gate the event uses, scans it as it scans any visitor.
- HereMe, online, checks that an event is checking tickets in at that gate now —
scheduledorlive, from two hours before its start to its end — then the holder's ticket, then that it isvalid. It then consumes the ticket in a single statement. - A ticket is checked in once. If two gates scan it at the same moment, exactly one admits it; the other shows red: "Already checked in at 13:42, North gate."
- Offline gates do not admit tickets. The tablet says "Can't check tickets offline — check the list".
- A void or refunded ticket, or a person with no ticket, is not admitted.
- The check-in is also an ordinary HereMe visit at the organisation. The person sees it in their app; the organisation's retention rules apply.
- The API reads check-ins and never makes them: there is no endpoint to check a ticket in or to undo a check-in.
12. What HereMe returns, and never returns#
Returned: what you sent (your ids, the HereMe ID, ticket type and status, event details and listing), HereMe's ids, the counts, how a ticket came to be (source), and the check-in state: whether, when, the gate's name and the device's label.
Never returned: the person's name, contact details, photo, other visits, other tickets, messages, vault content or appointments; whether a HereMe ID exists beyond the bare HERE_ME_USER_NOT_FOUND; and anything about another organisation.
13. Data, retention and logs#
- Tickets are the organisation's operational data. HereMe processes them for the organisation; your system acts for the organisation too.
- A ticket is deleted once the organisation's visit retention (1–7 days, 7 by default) has passed after its event ended. An organisation that has never recorded a visit or saved its visit settings has no retention set yet; its tickets then go 30 days after the event. The event's own record stays: it holds no personal data.
- Every call is logged for the organisation (key, operation, your references, outcome, error code,
Idempotency-Key, time) and shown to its owners and admins on the Integrations page. The log never holds the key, its secret or a HereMe ID. It is kept 24 months.
14. Checklist for integrators#
- Ask the organisation's owner or admin for a key with only the scopes you need. Store it as a secret, on your server only.
- Ask them to choose the gates each event uses in the console.
- Collect each buyer's HereMe ID at purchase (the person reads it in their HereMe app). Check it with them; never guess.
- Create the event, then add attendees — or send both in one
/import. - Use a fresh
Idempotency-Keyper write and reuse it on retries. Back off on429and503. - Read check-in status by polling
read-check-in-status. Never infer it. - Ignore fields you do not know, and tolerate new
details.reasonvalues.
15. Versioning and changelog#
- The version is in the path:
/v1. Within v1 changes are additive only: new endpoints, new optional request fields, new response fields, newdetails.reasonvalues. Your client must ignore what it does not know. - Anything that would break a working client — removing or renaming a field, changing a status code or a meaning — comes as
/v2, announced in advance, with/v1kept running alongside for a support window.
| Date | Change |
|---|---|
| 2026-10-03 | Additive (ADR-0053): listing fields on create-event and import (§9.1: visibility, place, pricing, capacity, ticket types); every event answer carries the listing and every ticket its source; POST /v1/invite-attendee (§9.6); new details.reason values event_not_closed, pricing_locked, capacity_below_taken, not_an_institution. |
| 2026-10-02 | Guide completed: quick start, examples in curl, JavaScript and Python, every error with its body, worked example; published at hereme.me/developers. Corrections: request ids are req_<UUIDv7> with hyphens; ticket retention follows the host's 1–7 day visit retention; DUPLICATE_TICKET may say concurrent and carries ticket_id; unknown fields are refused with details.field: null; Retry-After is always 60; events need gates chosen in the console before anyone is admitted. |
| 2026-10-02 | V1: create-event, import, add-attendee, update-attendee, read-check-in-status (ADR-0045). |