Developers

Webhook events

Every event a Hapana webhook can subscribe to, with the fields each one carries and an example delivery body.

Event catalogue

EventSent when
member.createdA member is created at a location.
member.updatedA member's profile is edited.
booking.createdA member is booked into a session.
booking.cancelledA booking is cancelled or marked late-cancelled.
booking.completedA booking is marked attended.
booking.no_showA booking is marked as a no-show.
checkin.createdA member checks in successfully.
session.createdA session (class or appointment) is scheduled.
session.updatedA session is edited.
session.cancelledA session is cancelled.
staff.createdA staff member is added.
staff.updatedA staff member's profile is edited or they are reactivated.
staff.deactivatedA staff member is deactivated.
checkInLegacy v1 check-in body, for partners migrating from Hapana v1.

Every event except checkIn is wrapped in the standard event envelope (id, object, type, apiVersion, createdAt, livemode, data). The tables below describe data. All ids are Hapana ids — the same ids the Business API uses, so you can read the full record back. Timestamps are ISO 8601 in UTC.

Payloads grow. New fields may be added to data without notice, so ignore fields you don't recognise rather than rejecting the event.

Member events

member.created

A new member was created at a location.

FieldTypeDescription
clientIdstringThe new member's id.
siteIdstringThe location the member joined.
firstNamestring | nullFirst name.
lastNamestring | nullLast name.
emailstring | nullEmail address.
{
  "id": "evt_2b9c6f0e-41d7-4a55-8f0c-6a1e9b3d7c21",
  "object": "event",
  "type": "member.created",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-02T21:14:05.112Z",
  "livemode": true,
  "data": {
    "clientId": "5a8d2e61-0c4b-4f3a-9e7d-1b2c3d4e5f6a",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada@example.com"
  }
}

member.updated

A member's profile was edited. The event names the fields that changed but not their values — read the member back through the API (under your own key's scopes) to get the new values.

FieldTypeDescription
clientIdstringThe member's id.
siteIdstringThe location the edit was made at.
changedFieldsstring[]Names of the profile fields that were submitted in the edit, sorted.
{
  "id": "evt_6d0e3b8a-9c5f-4b27-a1e4-0f8d2c6b9a13",
  "object": "event",
  "type": "member.updated",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-02T21:20:41.903Z",
  "livemode": true,
  "data": {
    "clientId": "5a8d2e61-0c4b-4f3a-9e7d-1b2c3d4e5f6a",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "changedFields": [
      "email",
      "mobilePhoneNumber"
    ]
  }
}

Booking events

All booking events share the same core fields:

FieldTypeDescription
bookingIdstringThe booking id.
clientIdstringThe booked member.
sessionIdstringThe session booked into.
siteIdstringThe location of the session.
statusstringThe booking's new status: PENDING, CONFIRMED, CANCELLED, LATE_CANCELLED, ATTENDED, NO_SHOW, and so on.

booking.created

A member was booked into a session. Adds:

FieldTypeDescription
bookingSourcestringHow the booking was paid for or made: CREDIT, FREE, DROP_IN, BILLING, ADMIN, AGGREGATOR, IMPORT, or GUEST_PASS.
{
  "id": "evt_7c1e1c52-3f0a-4a8e-9a43-2f5b1d0e6c11",
  "object": "event",
  "type": "booking.created",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-02T23:08:22.481Z",
  "livemode": true,
  "data": {
    "bookingId": "b3f1c2a4-5d6e-4f70-8a91-0b2c3d4e5f60",
    "clientId": "5a8d2e61-0c4b-4f3a-9e7d-1b2c3d4e5f6a",
    "sessionId": "9e0f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "status": "CONFIRMED",
    "bookingSource": "CREDIT"
  }
}

booking.cancelled

A booking was cancelled by the member or staff (status is CANCELLED or LATE_CANCELLED), or staff marked attendance as late-cancelled (status is LATE_CANCELLED). When the member or staff cancels, the event adds:

FieldTypeDescription
lateCancelbooleanWhether the cancellation fell inside the location's late-cancellation window. Absent when the event comes from marking attendance.
{
  "id": "evt_a4f2d9c1-7e3b-4c80-b5a6-1d9e0f2c3b47",
  "object": "event",
  "type": "booking.cancelled",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-03T06:42:10.027Z",
  "livemode": true,
  "data": {
    "bookingId": "b3f1c2a4-5d6e-4f70-8a91-0b2c3d4e5f60",
    "clientId": "5a8d2e61-0c4b-4f3a-9e7d-1b2c3d4e5f6a",
    "sessionId": "9e0f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "status": "LATE_CANCELLED",
    "lateCancel": true
  }
}

booking.completed

Staff marked the booking attended (status is ATTENDED). Use it to count visits.

{
  "id": "evt_e8b7c6d5-4a3f-4e21-9d0c-8b7a6f5e4d32",
  "object": "event",
  "type": "booking.completed",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-03T08:05:51.660Z",
  "livemode": true,
  "data": {
    "bookingId": "b3f1c2a4-5d6e-4f70-8a91-0b2c3d4e5f60",
    "clientId": "5a8d2e61-0c4b-4f3a-9e7d-1b2c3d4e5f6a",
    "sessionId": "9e0f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "status": "ATTENDED"
  }
}

booking.no_show

Staff marked the booking as a no-show (status is NO_SHOW). Same fields as booking.completed.

{
  "id": "evt_f1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8",
  "object": "event",
  "type": "booking.no_show",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-03T08:06:12.318Z",
  "livemode": true,
  "data": {
    "bookingId": "b3f1c2a4-5d6e-4f70-8a91-0b2c3d4e5f60",
    "clientId": "5a8d2e61-0c4b-4f3a-9e7d-1b2c3d4e5f6a",
    "sessionId": "9e0f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "status": "NO_SHOW"
  }
}

If staff undo an attendance mark (back to CONFIRMED), no event is sent.

Check-in events

checkin.created

A member checked in successfully — from a barcode or fob scan, the kiosk, the app, contactless, or staff in Core. Refused check-ins are not currently sent as checkin.created; status and failReason are included so your handler is ready if they are added. A re-scan inside the location's re-entry grace window returns the earlier check-in and sends nothing.

FieldTypeDescription
checkInIdstringThe check-in id.
clientIdstringThe member who checked in.
siteIdstringThe location checked in to.
typestringGENERAL (gym floor / door), SESSION (a booked class), TANNING, or ZONE.
statusstringSUCCESS, FAIL, or UNKNOWN (member not found).
failReasonstring | nullWhy a check-in was refused, e.g. NO_VALID_MEMBERSHIP_OR_BOOKING, SUSPENDED, MEMBERSHIP_ON_HOLD, FAILED_PAYMENTS, NO_CREDITS. null on success.
sessionIdstring | nullThe session the check-in was for, or null for a general check-in.
{
  "id": "evt_0c9b8a7f-6e5d-4c3b-a2f1-e0d9c8b7a6f5",
  "object": "event",
  "type": "checkin.created",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-02T23:08:22.517Z",
  "livemode": true,
  "data": {
    "checkInId": "d2c1b0a9-f8e7-4d6c-b5a4-93f2e1d0c9b8",
    "clientId": "5a8d2e61-0c4b-4f3a-9e7d-1b2c3d4e5f6a",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "type": "GENERAL",
    "status": "SUCCESS",
    "failReason": null,
    "sessionId": null
  }
}

Session events

session.created, session.updated, and session.cancelled all carry the same fields — the session as it is after the change:

FieldTypeDescription
sessionIdstringThe session id.
siteIdstringThe location running the session.
namestringSession name.
startTimestringStart time, ISO 8601 UTC.
endTimestringEnd time, ISO 8601 UTC.
statusstringSCHEDULED, IN_PROGRESS, COMPLETED, or CANCELLED.

session.created

A session was scheduled.

{
  "id": "evt_3e4f5a6b-7c8d-4e9f-a0b1-c2d3e4f5a6b7",
  "object": "event",
  "type": "session.created",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-01T02:11:37.204Z",
  "livemode": true,
  "data": {
    "sessionId": "9e0f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "name": "Morning Yoga",
    "startTime": "2026-10-03T10:00:00.000Z",
    "endTime": "2026-10-03T11:00:00.000Z",
    "status": "SCHEDULED"
  }
}

session.updated

A session was edited — time, name, capacity, instructors, and so on. The event carries the session's current fields; read it back through the API for anything else.

{
  "id": "evt_8a9b0c1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d",
  "object": "event",
  "type": "session.updated",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-02T04:55:09.871Z",
  "livemode": true,
  "data": {
    "sessionId": "9e0f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "name": "Morning Yoga",
    "startTime": "2026-10-03T10:30:00.000Z",
    "endTime": "2026-10-03T11:30:00.000Z",
    "status": "SCHEDULED"
  }
}

session.cancelled

A session was cancelled. status is CANCELLED.

{
  "id": "evt_1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
  "object": "event",
  "type": "session.cancelled",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-02T19:30:44.390Z",
  "livemode": true,
  "data": {
    "sessionId": "9e0f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "name": "Morning Yoga",
    "startTime": "2026-10-03T10:30:00.000Z",
    "endTime": "2026-10-03T11:30:00.000Z",
    "status": "CANCELLED"
  }
}

Staff events

Staff can work across several locations, so one staff change produces one event per location the staff member can access (each with its own event id and siteId). Only the locations your webhook covers are delivered. Deduplicate on staffUserId if you only need one notification per change.

staff.created

A staff member was added.

FieldTypeDescription
staffUserIdstringThe staff member id.
siteIdstringA location the staff member can access.
firstNamestringFirst name.
lastNamestringLast name.
emailstringEmail address.
statusstringactive or inactive.
{
  "id": "evt_4c5d6e7f-8a9b-4c0d-a1e2-f3a4b5c6d7e8",
  "object": "event",
  "type": "staff.created",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-01T22:03:18.554Z",
  "livemode": true,
  "data": {
    "staffUserId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane.smith@example.com",
    "status": "active",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b"
  }
}

staff.updated

A staff member's profile was edited (staffUserId and siteId only — read the profile back for details), or they were reactivated (status is also included, set to active).

{
  "id": "evt_9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b",
  "object": "event",
  "type": "staff.updated",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-02T03:47:26.119Z",
  "livemode": true,
  "data": {
    "staffUserId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b"
  }
}

staff.deactivated

A staff member was deactivated. status is inactive.

{
  "id": "evt_5e6f7a8b-9c0d-4e1f-b2a3-c4d5e6f7a8b9",
  "object": "event",
  "type": "staff.deactivated",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-02T05:12:00.736Z",
  "livemode": true,
  "data": {
    "staffUserId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
    "status": "inactive",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b"
  }
}

Legacy v1 check-in

The checkIn event exists for partners that integrated with the Hapana v1 platform's check-in webhook — for example, Realtime Feedback — and need the same body unchanged. It reproduces the v1 payload field for field, in the same order.

  • No envelope. The body is the bare v1 JSON object. There is no id, type envelope, or data wrapper — the top-level type field is always "checkIn".
  • Same headers. It carries the same X-Hapana-Event-ID, X-Hapana-Event-Type: checkIn, X-Hapana-Timestamp, and X-Hapana-Signature headers as every other event, plus your custom header if set. A receiver built for v1 can ignore them; new code should verify the signature and use the event id for idempotency.
  • Opt-in. It appears in its own Legacy group in Core and is not included by Select all events. For a new integration, use checkin.created instead.
  • Fires for refused check-ins too, like v1.

Example body

{
  "uid": "17909825160599fd1f-fe1a-4ce6-9b41-3d7e00530401",
  "type": "checkIn",
  "siteID": "<encrypted>",
  "clientID": "<encrypted>",
  "checkInTime": "2026-10-02T23:08:22",
  "checkin_dt": "2026-10-02",
  "checkin_id": "<encrypted>",
  "firstName": "Brad",
  "lastName": "Shipman",
  "email": "member@example.com",
  "barcode": "0707",
  "fobNumber": "",
  "joinDate": "2026-07-23",
  "packageName": "Employee Membership",
  "packageID": "<encrypted>",
  "checkInStatus": "Valid",
  "checkInMessage": "Valid check-in - with alerts",
  "sessionType": "",
  "sessionName": "",
  "sessionID": "",
  "sessionStartTime": "",
  "sessionInstructor": "",
  "sessionInstructorID": "",
  "phone": "+15555550123",
  "customProperties": []
}

Fields

FieldTypeDescription
uidstringUnique id for this body: Unix seconds followed by the member's id. Extra bodies for additional sessions (below) get a suffix.
typestringAlways checkIn.
siteIDstringEncrypted location id. For a location migrated from v1, the exact value v1 sent.
clientIDstringEncrypted member id. For a member migrated from v1, the exact value v1 sent.
checkInTimestringCheck-in time in UTC, YYYY-MM-DDTHH:mm:ss with no offset.
checkin_dtstringCheck-in date in the location's time zone, YYYY-MM-DD.
checkin_idstringEncrypted check-in id.
firstNamestringMember's first name.
lastNamestringMember's last name.
emailstringMember's email.
barcodestringMember's barcode.
fobNumberstringMember's fob number.
joinDatestringMember's join date in the location's time zone, YYYY-MM-DD.
packageNamestringName of the membership or package the check-in used.
packageIDstringEncrypted id of that membership or package.
checkInStatusstringValid, Invalid, or error (member not found).
checkInMessagestringHuman-readable outcome, using the v1 strings — see the table below.
sessionTypestringSession type name, for a session check-in.
sessionNamestringSession name.
sessionIDstringEncrypted session id.
sessionStartTimestringSession start, ISO 8601 with the location's UTC offset.
sessionInstructorstringInstructor names, comma-separated.
sessionInstructorIDstringEncrypted instructor ids, comma-separated, in the same order.
phonestringMember's mobile, else home phone.
customPropertiesarrayThe member's filled custom properties at the location, as { "fieldLabel": "…", "fieldValue": "…" } objects.

Fields with no value are sent as an empty string "", never null — the session fields are all "" for a general (non-session) check-in, as in the example above.

Status and message

OutcomecheckInStatuscheckInMessage
SuccessfulValidValid check-in
Successful, member has failed or pending paymentsValidValid check-in - with alerts
Refused — membership suspendedInvalidInvalid check-in - Suspended membership
Refused — membership on holdInvalidInvalid check-in - with alerts - Membership on hold
Refused — any other reasonInvalidInvalid check-in - no valid membership or booking
Member not founderrorno record found

As in v1, the success messages are capitalised (Valid check-in) for kiosk, Core, and widget check-ins, and lowercase (valid check-in) for barcode, fob, contactless, QR, app, and other sources. Compare case-insensitively.

Several sessions on one scan

When one scan checks a member into several booked sessions, Hapana sends one body for the first session and one extra body for each additional session — each as its own delivery with its own event id. The extra bodies are identical except for the session fields and a uid with a suffix, matching v1.

Re-scans

A re-scan inside the location's re-entry grace window (15 minutes by default) sends nothing. A later re-scan sends a new body, as v1 did.

Ids

Every id in the legacy body (siteID, clientID, checkin_id, packageID, sessionID, sessionInstructorID) is encrypted the same way v1 encrypted them. For records migrated from v1, the values match what v1 sent, so ids you stored from v1 keep matching. For records created after migration the format is the same and the value is stable. These are not the ids used by checkin.created or the Business API.