Webhook events
Every event a Hapana webhook can subscribe to, with the fields each one carries and an example delivery body.
Event catalogue
| Event | Sent when |
|---|---|
member.created | A member is created at a location. |
member.updated | A member's profile is edited. |
booking.created | A member is booked into a session. |
booking.cancelled | A booking is cancelled or marked late-cancelled. |
booking.completed | A booking is marked attended. |
booking.no_show | A booking is marked as a no-show. |
checkin.created | A member checks in successfully. |
session.created | A session (class or appointment) is scheduled. |
session.updated | A session is edited. |
session.cancelled | A session is cancelled. |
staff.created | A staff member is added. |
staff.updated | A staff member's profile is edited or they are reactivated. |
staff.deactivated | A staff member is deactivated. |
checkIn | Legacy 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.
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.
| Field | Type | Description |
|---|---|---|
clientId | string | The new member's id. |
siteId | string | The location the member joined. |
firstName | string | null | First name. |
lastName | string | null | Last name. |
email | string | null | Email 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.
| Field | Type | Description |
|---|---|---|
clientId | string | The member's id. |
siteId | string | The location the edit was made at. |
changedFields | string[] | 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:
| Field | Type | Description |
|---|---|---|
bookingId | string | The booking id. |
clientId | string | The booked member. |
sessionId | string | The session booked into. |
siteId | string | The location of the session. |
status | string | The 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:
| Field | Type | Description |
|---|---|---|
bookingSource | string | How 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:
| Field | Type | Description |
|---|---|---|
lateCancel | boolean | Whether 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.
| Field | Type | Description |
|---|---|---|
checkInId | string | The check-in id. |
clientId | string | The member who checked in. |
siteId | string | The location checked in to. |
type | string | GENERAL (gym floor / door), SESSION (a booked class), TANNING, or ZONE. |
status | string | SUCCESS, FAIL, or UNKNOWN (member not found). |
failReason | string | null | Why a check-in was refused, e.g. NO_VALID_MEMBERSHIP_OR_BOOKING, SUSPENDED, MEMBERSHIP_ON_HOLD, FAILED_PAYMENTS, NO_CREDITS. null on success. |
sessionId | string | null | The 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:
| Field | Type | Description |
|---|---|---|
sessionId | string | The session id. |
siteId | string | The location running the session. |
name | string | Session name. |
startTime | string | Start time, ISO 8601 UTC. |
endTime | string | End time, ISO 8601 UTC. |
status | string | SCHEDULED, 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.
| Field | Type | Description |
|---|---|---|
staffUserId | string | The staff member id. |
siteId | string | A location the staff member can access. |
firstName | string | First name. |
lastName | string | Last name. |
email | string | Email address. |
status | string | active 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,typeenvelope, ordatawrapper — the top-leveltypefield is always"checkIn". - Same headers. It carries the same
X-Hapana-Event-ID,X-Hapana-Event-Type: checkIn,X-Hapana-Timestamp, andX-Hapana-Signatureheaders 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.createdinstead. - 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
| Field | Type | Description |
|---|---|---|
uid | string | Unique id for this body: Unix seconds followed by the member's id. Extra bodies for additional sessions (below) get a suffix. |
type | string | Always checkIn. |
siteID | string | Encrypted location id. For a location migrated from v1, the exact value v1 sent. |
clientID | string | Encrypted member id. For a member migrated from v1, the exact value v1 sent. |
checkInTime | string | Check-in time in UTC, YYYY-MM-DDTHH:mm:ss with no offset. |
checkin_dt | string | Check-in date in the location's time zone, YYYY-MM-DD. |
checkin_id | string | Encrypted check-in id. |
firstName | string | Member's first name. |
lastName | string | Member's last name. |
email | string | Member's email. |
barcode | string | Member's barcode. |
fobNumber | string | Member's fob number. |
joinDate | string | Member's join date in the location's time zone, YYYY-MM-DD. |
packageName | string | Name of the membership or package the check-in used. |
packageID | string | Encrypted id of that membership or package. |
checkInStatus | string | Valid, Invalid, or error (member not found). |
checkInMessage | string | Human-readable outcome, using the v1 strings — see the table below. |
sessionType | string | Session type name, for a session check-in. |
sessionName | string | Session name. |
sessionID | string | Encrypted session id. |
sessionStartTime | string | Session start, ISO 8601 with the location's UTC offset. |
sessionInstructor | string | Instructor names, comma-separated. |
sessionInstructorID | string | Encrypted instructor ids, comma-separated, in the same order. |
phone | string | Member's mobile, else home phone. |
customProperties | array | The 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
| Outcome | checkInStatus | checkInMessage |
|---|---|---|
| Successful | Valid | Valid check-in |
| Successful, member has failed or pending payments | Valid | Valid check-in - with alerts |
| Refused — membership suspended | Invalid | Invalid check-in - Suspended membership |
| Refused — membership on hold | Invalid | Invalid check-in - with alerts - Membership on hold |
| Refused — any other reason | Invalid | Invalid check-in - no valid membership or booking |
| Member not found | error | no 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.
