HereMe Contact us

HereMe for developers

HereMe Event Integration API

For ticketing and event systems that work with a HereMe organisation: create events, make existing HereMe users ticket holders by their HereMe ID, and read HereMe's check-in status.

Base URL
https://api.hereme.me/v1
Version
V1

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:

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#

1.2 Make a key in the console#

  1. Sign in at https://console.hereme.me and open Integrations.
  2. 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 example ticketco). It namespaces your event ids (§3.4).
    • What it may do — one or more scopes (§3.3). Choose only what you need.
  3. Choose Create key. Creating credentials needs a recent sign-in, so the console may ask the person to confirm who they are first.
  4. 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#

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#

ScopeAllowsConsole label
events:writePOST /create-event; event records in POST /importCreate and change events
attendees:writePOST /add-attendee, POST /update-attendee; attendee records in POST /importAdd and change ticket holders
checkins:readGET /read-check-in-statusRead 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#

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"
  }
}

Your own ids are natural keys too:

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:

6. Rate limits#

LimitCountedThen
120 requests a minuteper key429 RATE_LIMITED
20 refused keys a minuteper client IP429 RATE_LIMITED instead of 401
50 unknown HereMe IDs an hourper keyevery new holder lookup — known ID or not — is 429 RATE_LIMITED (details.reason: unknown_hereme_ids) until the hour turns (UTC)

7. Times and zones#

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"
  }
}
HTTPcodeWhen
400INVALID_REQUESTA 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"
401INVALID_API_KEYNo key, a malformed key, an unknown key, a wrong secret, or a closed organisation
401API_KEY_REVOKEDThe key was revoked (said only to someone holding its secret)
403FORBIDDENThe 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)
404EVENT_NOT_FOUNDNo such event in this organisation
404TICKET_NOT_FOUNDNo ticket with this external_ticket_id for this event
404HERE_ME_USER_NOT_FOUNDNo active HereMe user with this HereMe ID. It says nothing more
409DUPLICATE_EVENTYour external_event_id exists with other details (details.event_id)
409DUPLICATE_TICKETdetails.reason: external_ticket_id_taken (with details.ticket_id), holder_has_ticket, or concurrent (the same ticket was being added at the same moment)
409CHECK_IN_NOT_ALLOWEDReserved. No V1 endpoint returns it: check-in happens only at HereMe gates (§11)
413INVALID_REQUESTMore than 1 MB (details.reason: body_too_large)
429RATE_LIMITEDSee §6
503UNAVAILABLEHereMe'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 pathScopeSuccess
POST /v1/create-eventevents:write201 created · 200 unchanged
POST /v1/add-attendeeattendees:write201 created · 200 unchanged
POST /v1/update-attendeeattendees:write200 updated or unchanged
POST /v1/importthose of its records200 with a result per record
GET /v1/read-check-in-statuscheckins:read200
POST /v1/invite-attendeeattendees:write201 created · 200 unchanged

Shared field rules:

9.1 POST /v1/create-event#

Creates an event under your external_event_id. Scope events:write.

FieldType
external_event_idstringRequired. Your id
titlestringRequired. 1–160 characters
descriptionstring or nullUp to 2,000 characters
locationstring or nullUp to 200 characters
starts_attime with offsetRequired
ends_attime with offsetRequired. After starts_at, at most 31 days later
statusstringdraft, scheduled (default), live, ended or cancelled

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):

FieldType
visibilitystringunlisted (default), open (discoverable by place) or closed (by invitation, §9.6)
published_fromtime with offset or nullWhen a listed event becomes discoverable; default: when it is first listed
country_codestringISO 3166-1 alpha-2, e.g. KE. Required to list
citystring1–80 characters. Required to list
areastring or nullA district or neighbourhood, up to 80 characters
addressstringUp to 300 characters. Required to list
latitude, longitudenumbers or nullThe pin, both or neither
time_zonestringIANA, e.g. Africa/Nairobi. Required to list
pricingstringfree (default), sponsored or paid
currencystring or nullKES for sponsored and paid; none for free
capacityinteger or nullFree and sponsored: seats, first come (1–1,000,000)
ticket_typeslist or nullPaid: 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)

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.

FieldType
external_event_id or event_idstringRequired (one of them)
hereme_idstringRequired. The holder's HereMe ID
external_ticket_idstringRequired. Your ticket id, unique within the event
external_attendee_idstring or nullYour customer id
ticket_typestring or null1–60 characters, e.g. VIP
statusstringvalid (default), void or refunded

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.

FieldType
external_event_id or event_idstringRequired (one of them)
external_ticket_idstringRequired. The ticket to change
statusstringvalid, void or refunded
ticket_typestring or nullnull clears it
external_attendee_idstring or nullnull clears it
hereme_idstringOptional; if sent, it must be the holder's

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.

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_idRequired (one of them)
external_ticket_idOne ticket (then cursor and limit are ignored)
limit1–500, default 100
cursorThe 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.

FieldType
external_event_id or event_idstringRequired (one of them)
hereme_idstringRequired. The invitee's HereMe ID

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:

  1. imports the event with the first buyers (one /import);
  2. adds a late buyer by HereMe ID (/add-attendee);
  3. upgrades one ticket and refunds another (/update-attendee);
  4. 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)#

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#

14. Checklist for integrators#

  1. 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.
  2. Ask them to choose the gates each event uses in the console.
  3. Collect each buyer's HereMe ID at purchase (the person reads it in their HereMe app). Check it with them; never guess.
  4. Create the event, then add attendees — or send both in one /import.
  5. Use a fresh Idempotency-Key per write and reuse it on retries. Back off on 429 and 503.
  6. Read check-in status by polling read-check-in-status. Never infer it.
  7. Ignore fields you do not know, and tolerate new details.reason values.

15. Versioning and changelog#

DateChange
2026-10-03Additive (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-02Guide 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-02V1: create-event, import, add-attendee, update-attendee, read-check-in-status (ADR-0045).