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.
member.suspendedA member is blocked at a location. Unblocking sends member.updated.
member.deletedA member is archived.
membership.createdA membership or package is purchased or moved to.
membership.activatedA membership becomes usable for the first time: sold and paid for today, or a pre-sale reaching its start date. Once per membership per location; never for a resume, restore, extension or credit reactivation.
membership.updatedA membership's status or details change, other than its first activation.
membership.cancelledA membership is cancelled.
membership.expiredA membership expires.
membership.renewedA membership renews.
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.
door.accessA member scans at a door, whether admitted or refused.
session.createdA session (class or appointment) is scheduled.
session.updatedA session is edited.
session.cancelledA session is cancelled.
session.instructor_changedWho teaches a session changes, including substitutions and cover fills.
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.

The membership.* events, member.suspended and member.deleted are being enabled location by location; until a location has them, a periodic reconcile keeps you current. Their payloads are deliberately thin — ids rather than the full record — so re-read the resource an event names rather than treating the payload as its new state. A member re-scanning into a class they have already attended deliberately emits nothing.

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

Membership events

membership.activated

Sent once per membership per location the moment the membership becomes usable: a purchase paid for today, or a pre-sale reaching its start date (including a pre-sale that was migrated in). A door system can admit the member from this event alone. Memberships inserted by a bulk import or migration as already active send nothing. It is never sent for a resume from hold, a restore after cancellation, an extension or a credit reactivation — those arrive as membership.updated. Locations the member can use the membership at but does not belong to receive nothing. The event id is derived from the membership and location, so a redelivery carries the same id; treat a repeated id as already handled.

FieldTypeDescription
clientIdstringThe member's id.
siteIdstringThe location this delivery is for.
serviceIdstringThe membership id.
serviceSiteIdstringThe location that sold the membership.
serviceTemplateIdstringThe membership plan.
parentServiceIdstring | nullThe parent membership when this is an add-on.
barcodestring | nullThe member's barcode at delivery time.
activatedAtstringWhen the membership became usable.
triggerstringpurchase, payment_activation, scheduled_activation, renewal or movement.
{
  "id": "evt_1b4f0c3d9a2e4c7f8e6d5a4b3c2d1e0f",
  "object": "event",
  "type": "membership.activated",
  "apiVersion": "2026-01-10",
  "createdAt": "2026-10-22T03:12:09.117Z",
  "livemode": true,
  "data": {
    "clientId": "5a8d2e61-0c4b-4f3a-9e7d-1b2c3d4e5f6a",
    "siteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "serviceId": "9c7b6a5d-4e3f-4a2b-8c1d-0e9f8a7b6c5d",
    "serviceSiteId": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
    "serviceTemplateId": "3e2d1c0b-9a8f-4e7d-6c5b-4a3f2e1d0c9b",
    "parentServiceId": null,
    "barcode": "8675309",
    "activatedAt": "2026-10-22T03:12:08.902Z",
    "trigger": "purchase"
  }
}

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.
scheduleTemplateIdstring | nullThe schedule template the class was generated from (see GET /sessions/templates), or null for a class created on its own.

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",
    "scheduleTemplateId": "5e550100-0001-4000-8000-000000000000"
  }
}

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",
    "scheduleTemplateId": "5e550100-0001-4000-8000-000000000000"
  }
}

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",
    "scheduleTemplateId": "5e550100-0001-4000-8000-000000000000"
  }
}

door.access

Fires for each scan at a door, including a re-entry and a door that admits without a check-in, neither of which sends a check-in event. Use it when you need to match door activity to something else in real time, such as camera footage for tailgating detection. It is being enabled location by location. Filter on outcome: a DENIED scan means the door stayed shut. Scans a door panel refuses or handles offline itself, doors opened remotely by staff, and an unrecognised fob at a location without multi-door access rules do not send this event.

{
  "doorAccessEventId": "b41e…",
  "occurredAt": "2026-11-06T01:02:03.000Z",
  "outcome": "ALLOWED",
  "siteId": "9dc9…",
  "door": { "id": "7ac2…", "name": "Front Door", "provider": "VIVA_MAC" },
  "zone": { "id": "51fd…", "name": "Main Entrance" },
  "member": {
    "clientId": "c1a7…",
    "firstName": "Sam",
    "lastName": "Lee",
    "email": "sam@example.com"
  },
  "credentialType": "FOB",
  "source": "FOB_SCAN",
  "deniedBy": null,
  "failReason": null
}
  • door.id is stable, so use it as your device id. door.name is the name shown in Core and can change.
  • outcome is what the door did, which can differ from the Core access log. For example, a Fitness Passport grant opens the door and sends ALLOWED while the log shows the refusal it overrode.
  • member is null when a scanned fob matched nobody.
  • occurredAt is when Hapana recorded the scan, normally within a couple of seconds of the door decision. A partner door that replays taps after being offline sends them late, stamped with the time Hapana received them rather than when the door opened.

session.instructor_changed

Fires after any change to who teaches a class: a substitution or instructor write through the API, a substitution or instructor edit in Core, a this-and-future recurring edit (once per affected occurrence), a class-cover fill, and a staff member being deactivated or no longer teaching (once per future class they leave). Your own API writes arrive here too, so you can confirm them.

{
  "sessionId": "3f0b…",
  "siteId": "9dc9…",
  "name": "Reformer 45",
  "startTime": "2026-10-09T19:00:00.000Z",
  "endTime": "2026-10-09T19:45:00.000Z",
  "instructors": [
    { "id": "619e…", "name": "Sarah Johnson", "role": "SUBSTITUTE", "substitutingFor": "f9c5…" }
  ],
  "changedBy": "c1a7…",
  "source": "PARTNER_API"
}
  • instructors is the roster as it now stands, not a diff. An empty array means nobody is teaching.
  • substitutingFor is set on a SUBSTITUTE entry and names the instructor whose class it is. The original instructor's assignment is removed when a substitute is made, so this field is how you see who is covering for whom.
  • source is PARTNER_API (an API key), STAFF (a person in Core), SYSTEM (a job) or NATIVE_COVER (a Hapana class-cover fill). changedBy is the staff id that made the change, or null.

On a site that sends instructor events, session.updated also carries the current instructors and a sorted changedFields list of what the edit changed: startTime and endTime when the class moved, plus date when the day changed, and instructors when who teaches changed. Instructor events are being enabled brand by brand. An occurrence of a recurring class that has not been created yet has no row of its own, so a pattern-level change to it shows up in your next GET /sessions pull with updatedSince, not as an event.

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.