Webhooks let your application react to events happening in your Studio in real time. Instead of polling the Fitssey API,
you register a callback URL and Fitssey sends an HTTP POST request to it every time a subscribed event occurs.
Webhooks can be configured in your Studio > Integrations > Webhooks.
This document covers webhooks. To read or change data in your Studio, see the API documentation.
Each webhook is delivered as an HTTP POST request with the following headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
lightenbody/Fitssey (+https://app.fitssey.com/{uuid}) |
Fitssey-Signature |
Signature of the request. Sent when the webhook uses the Signature authentication method, see Signature. |
Fitssey-Secret |
The authentication key. Sent when the webhook uses the Secret header authentication method, see Secret header. |
The request body is a JSON array containing a single event envelope:
[
{
"guid": "8F2B6C1D-4E7A-4B3C-9D21-5A6E7F8C9B0A",
"webhookGuid": "3C9E1A7B-2D4F-4E8A-B6C0-1F5D9A2E7B43",
"event": "event.shopping_cart.payment_status",
"data": { ... },
"firedAt": "2026-10-20T10:15:00+0200"
}
]
Your callback URL accepts requests from anyone who knows it, so always verify that a request was really sent by Fitssey before acting on it. Each webhook has an authentication key and one of two authentication methods, selected in Studio > Integrations > Webhooks:
| Method | Header | Security |
|---|---|---|
| Signature (recommended) | Fitssey-Signature |
Strong. The key is never sent; each request carries a signature valid only for its body and time. |
| Secret header | Fitssey-Secret |
Basic. The key itself is sent with every request; anyone who sees one request can forge others. |
Every request is signed with the authentication key, so you can verify that it was sent by Fitssey and was not modified on the way. The key itself is never sent.
The Fitssey-Signature header contains a Unix timestamp and an HMAC-SHA256 signature:
Fitssey-Signature: t=1760954100,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
To verify the request:
t (timestamp) and v1 (signature).t differs from the current time by more than 5 minutes to protect against replay attacks.{t}.{rawBody} using your authentication key as the key and encode it as lowercase hex.v1 using a constant-time comparison.The v1 prefix identifies the signature scheme. New schemes may be added in the future as additional vN entries, so ignore any you do not recognize.
$payload = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_FITSSEY_SIGNATURE'] ?? ''), $signature);
if(!isset($signature['t'], $signature['v1']) || 300 < abs(time() - (int) $signature['t']))
{
http_response_code(400);
exit;
}
$expected = hash_hmac('sha256', $signature['t'] . '.' . $payload, $secret);
if(!hash_equals($expected, $signature['v1']))
{
http_response_code(401);
exit;
}
$events = json_decode($payload, true);
const crypto = require('crypto');
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = Object.fromEntries((req.get('Fitssey-Signature') || '').split(',').map(part => part.split('=')));
if(!signature.t || !signature.v1 || Math.abs(Date.now() / 1000 - signature.t) > 300) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', secret).update(`${signature.t}.${req.body}`).digest('hex');
if(expected.length !== signature.v1.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature.v1))) return res.sendStatus(401);
const events = JSON.parse(req.body);
res.sendStatus(200);
});
The authentication key is sent as-is in the Fitssey-Secret header. Compare it with your key using a constant-time comparison and reject the request
when it is missing or does not match:
if(!hash_equals($secret, $_SERVER['HTTP_FITSSEY_SECRET'] ?? ''))
{
http_response_code(401);
exit;
}
This method is meant for tools that cannot compute signatures, such as no-code automation platforms. Because the key travels with every request, it does not protect the body from modification, does not prevent replays and exposes the key to anything that logs the request. Use Signature whenever you can.
Respond with any 2xx status as quickly as possible and do the heavy processing asynchronously. Redirects are not followed.
The callback URL must be a public https address. URLs resolving to private or reserved IP ranges are rejected.
The connection timeout defaults to 5 seconds. Every failed delivery increases the webhook's fail counter; once it reaches the limit (5 by default) the webhook is suspended and stops receiving events until it is re-enabled. A successful delivery resets the counter. Failed deliveries are not retried.
The same change may be reported more than once, so processing a webhook twice must have the same effect as processing it once.
Each event documents which fields identify a repeated change. The envelope guid is unique per event and is useful for logging,
but two notifications about the same change carry different guid values, so it cannot be used to detect them.
Event name: event.client.created
Fired when a new client is created - added in BackOffice, through the API, or when a client signs up in FrontOffice.
| guid | string Unique identifier of this delivery. |
| webhookGuid | string Identifier of the webhook (Studio > Integrations > Webhooks) this delivery was sent for. Useful when several webhooks point to the same callback URL. |
| event | string Value: event.client.created Name of the event. |
object (User) Event payload. Its shape depends on the event. | |
| firedAt | string <date-time> Date and time when the event was fired (ISO 8601). |
[- {
- "guid": "8F2B6C1D-4E7A-4B3C-9D21-5A6E7F8C9B0A",
- "webhookGuid": "3C9E1A7B-2D4F-4E8A-B6C0-1F5D9A2E7B43",
- "event": "event.client.created",
- "data": {
- "firstName": "Maria",
- "lastName": "Dvorak",
- "sex": 1,
- "suspensionType": 1,
- "isSuspended": false,
- "suspendedAt": "2019-09-10T15:31:47+02:00",
- "suspensionExpiresAt": "2019-09-17T15:31:47+02:00",
- "suspensionReason": "Suspended due to many cancellations",
- "isLoginEnabled": true,
- "activatedAt": "2019-09-01T12:02:35+02:00",
- "lastSeenAt": "2019-09-03T09:54:12+02:00",
- "client": {
- "uuid": "1000072",
- "clientFields": {
- "8D2A6C1E-4F3B-4A7E-9C21-5B6E0F3D7A12": 1234,
- "C47E9B05-1A2D-4E6F-8B3C-9D0E2F4A6B81": "ABCD"
}, - "agreements": null,
- "birthdateAt": "1980-01-19T23:00:00+01:00",
- "lastActiveAt": "2019-09-03T08:06:23+02:00",
- "user": { },
- "address": {
- "street": "Sunny street 77a",
- "postalCode": "1234AB",
- "state": "Noord-Holland",
- "city": "Amsterdam",
- "countryCode": "NL",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "phone": {
- "primaryPhone": "+48111222333",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "picture": {
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}, - "clientNotes": [
- {
- "type": "private",
- "note": "Client prefers morning classes.",
- "color": "#FF5733",
- "expiresAt": null,
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}
], - "tags": [
- {
- "name": "VIP",
- "color": "#FF5733",
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}
], - "age": 34
}, - "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "firedAt": "2026-10-20T10:15:00+0200"
}
]Event name: event.client.updated
Fired when client's data changes - edited in BackOffice or through the API, or updated by the client in FrontOffice.
| guid | string Unique identifier of this delivery. |
| webhookGuid | string Identifier of the webhook (Studio > Integrations > Webhooks) this delivery was sent for. Useful when several webhooks point to the same callback URL. |
| event | string Value: event.client.updated Name of the event. |
object (User) Event payload. Its shape depends on the event. | |
| firedAt | string <date-time> Date and time when the event was fired (ISO 8601). |
[- {
- "guid": "8F2B6C1D-4E7A-4B3C-9D21-5A6E7F8C9B0A",
- "webhookGuid": "3C9E1A7B-2D4F-4E8A-B6C0-1F5D9A2E7B43",
- "event": "event.client.updated",
- "data": {
- "firstName": "Maria",
- "lastName": "Dvorak",
- "sex": 1,
- "suspensionType": 1,
- "isSuspended": false,
- "suspendedAt": "2019-09-10T15:31:47+02:00",
- "suspensionExpiresAt": "2019-09-17T15:31:47+02:00",
- "suspensionReason": "Suspended due to many cancellations",
- "isLoginEnabled": true,
- "activatedAt": "2019-09-01T12:02:35+02:00",
- "lastSeenAt": "2019-09-03T09:54:12+02:00",
- "client": {
- "uuid": "1000072",
- "clientFields": {
- "8D2A6C1E-4F3B-4A7E-9C21-5B6E0F3D7A12": 1234,
- "C47E9B05-1A2D-4E6F-8B3C-9D0E2F4A6B81": "ABCD"
}, - "agreements": null,
- "birthdateAt": "1980-01-19T23:00:00+01:00",
- "lastActiveAt": "2019-09-03T08:06:23+02:00",
- "user": { },
- "address": {
- "street": "Sunny street 77a",
- "postalCode": "1234AB",
- "state": "Noord-Holland",
- "city": "Amsterdam",
- "countryCode": "NL",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "phone": {
- "primaryPhone": "+48111222333",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "picture": {
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}, - "clientNotes": [
- {
- "type": "private",
- "note": "Client prefers morning classes.",
- "color": "#FF5733",
- "expiresAt": null,
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}
], - "tags": [
- {
- "name": "VIP",
- "color": "#FF5733",
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}
], - "age": 34
}, - "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "firedAt": "2026-10-20T10:15:00+0200"
}
]Event name: event.client.deleted
Fired when a client is deleted. data.isDeleted is true and data.deletedAt holds the date of the deletion.
| guid | string Unique identifier of this delivery. |
| webhookGuid | string Identifier of the webhook (Studio > Integrations > Webhooks) this delivery was sent for. Useful when several webhooks point to the same callback URL. |
| event | string Value: event.client.deleted Name of the event. |
object (User) Event payload. Its shape depends on the event. | |
| firedAt | string <date-time> Date and time when the event was fired (ISO 8601). |
[- {
- "guid": "8F2B6C1D-4E7A-4B3C-9D21-5A6E7F8C9B0A",
- "webhookGuid": "3C9E1A7B-2D4F-4E8A-B6C0-1F5D9A2E7B43",
- "event": "event.client.deleted",
- "data": {
- "firstName": "Maria",
- "lastName": "Dvorak",
- "sex": 1,
- "suspensionType": 1,
- "isSuspended": false,
- "suspendedAt": "2019-09-10T15:31:47+02:00",
- "suspensionExpiresAt": "2019-09-17T15:31:47+02:00",
- "suspensionReason": "Suspended due to many cancellations",
- "isLoginEnabled": true,
- "activatedAt": "2019-09-01T12:02:35+02:00",
- "lastSeenAt": "2019-09-03T09:54:12+02:00",
- "client": {
- "uuid": "1000072",
- "clientFields": {
- "8D2A6C1E-4F3B-4A7E-9C21-5B6E0F3D7A12": 1234,
- "C47E9B05-1A2D-4E6F-8B3C-9D0E2F4A6B81": "ABCD"
}, - "agreements": null,
- "birthdateAt": "1980-01-19T23:00:00+01:00",
- "lastActiveAt": "2019-09-03T08:06:23+02:00",
- "user": { },
- "address": {
- "street": "Sunny street 77a",
- "postalCode": "1234AB",
- "state": "Noord-Holland",
- "city": "Amsterdam",
- "countryCode": "NL",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "phone": {
- "primaryPhone": "+48111222333",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "picture": {
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}, - "clientNotes": [
- {
- "type": "private",
- "note": "Client prefers morning classes.",
- "color": "#FF5733",
- "expiresAt": null,
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}
], - "tags": [
- {
- "name": "VIP",
- "color": "#FF5733",
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}
], - "age": 34
}, - "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "firedAt": "2026-10-20T10:15:00+0200"
}
]Event name: event.client.restored
Fired when a deleted client is restored in BackOffice.
| guid | string Unique identifier of this delivery. |
| webhookGuid | string Identifier of the webhook (Studio > Integrations > Webhooks) this delivery was sent for. Useful when several webhooks point to the same callback URL. |
| event | string Value: event.client.restored Name of the event. |
object (User) Event payload. Its shape depends on the event. | |
| firedAt | string <date-time> Date and time when the event was fired (ISO 8601). |
[- {
- "guid": "8F2B6C1D-4E7A-4B3C-9D21-5A6E7F8C9B0A",
- "webhookGuid": "3C9E1A7B-2D4F-4E8A-B6C0-1F5D9A2E7B43",
- "event": "event.client.restored",
- "data": {
- "firstName": "Maria",
- "lastName": "Dvorak",
- "sex": 1,
- "suspensionType": 1,
- "isSuspended": false,
- "suspendedAt": "2019-09-10T15:31:47+02:00",
- "suspensionExpiresAt": "2019-09-17T15:31:47+02:00",
- "suspensionReason": "Suspended due to many cancellations",
- "isLoginEnabled": true,
- "activatedAt": "2019-09-01T12:02:35+02:00",
- "lastSeenAt": "2019-09-03T09:54:12+02:00",
- "client": {
- "uuid": "1000072",
- "clientFields": {
- "8D2A6C1E-4F3B-4A7E-9C21-5B6E0F3D7A12": 1234,
- "C47E9B05-1A2D-4E6F-8B3C-9D0E2F4A6B81": "ABCD"
}, - "agreements": null,
- "birthdateAt": "1980-01-19T23:00:00+01:00",
- "lastActiveAt": "2019-09-03T08:06:23+02:00",
- "user": { },
- "address": {
- "street": "Sunny street 77a",
- "postalCode": "1234AB",
- "state": "Noord-Holland",
- "city": "Amsterdam",
- "countryCode": "NL",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "phone": {
- "primaryPhone": "+48111222333",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "picture": {
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}, - "clientNotes": [
- {
- "type": "private",
- "note": "Client prefers morning classes.",
- "color": "#FF5733",
- "expiresAt": null,
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}
], - "tags": [
- {
- "name": "VIP",
- "color": "#FF5733",
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}
], - "age": 34
}, - "guid": "364F59AB-5B43-4D02-989E-AE2811422709",
- "createdAt": "2019-05-12T12:45:47+02:00",
- "modifiedAt": "2023-09-12T19:18:23+02:00"
}, - "firedAt": "2026-10-20T10:15:00+0200"
}
]Event name: event.shopping_cart.payment_status
Fired when a Client checks out a shopping cart and every time the payment gateway reports a change of the payment status (paid, cancelled, rejected, failed...). It is also fired when a contract instalment is charged automatically.
Use data.orderStatus to determine the current state of the payment.
Duplicates
Payment gateways might notify several times about the same payment. Fitssey forwards only changes of the status - the webhook is fired
once per data.orderId + data.orderStatus pair and repeated notifications with the same status are not sent again.
A real change of the status (e.g. paid followed by a chargeback) fires the webhook again.
| guid | string Unique identifier of this delivery. |
| webhookGuid | string Identifier of the webhook (Studio > Integrations > Webhooks) this delivery was sent for. Useful when several webhooks point to the same callback URL. |
| event | string Value: event.shopping_cart.payment_status Name of the event. |
object (ShoppingCartWebhookPaymentStatus) Event payload. Its shape depends on the event. | |
| firedAt | string <date-time> Date and time when the event was fired (ISO 8601). |
[- {
- "guid": "8F2B6C1D-4E7A-4B3C-9D21-5A6E7F8C9B0A",
- "webhookGuid": "3C9E1A7B-2D4F-4E8A-B6C0-1F5D9A2E7B43",
- "event": "event.shopping_cart.payment_status",
- "data": {
- "metadata": { },
- "paymentGateway": "stripe",
- "paymentGatewayResponse": { },
- "orderId": "3C9A1E7B-2D4F-4A6B-8C1E-9F0D2B3A4C5D",
- "orderStatus": 1,
- "user": {
- "firstName": "Maria",
- "lastName": "Dvorak",
- "client": {
- "uuid": "1000072",
- "address": {
- "street": "Sunny street 77a",
- "postalCode": "1234AB",
- "state": "Noord-Holland",
- "city": "Amsterdam",
- "countryCode": "NL"
}
}, - "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}, - "purchase": {
- "items": [
- {
- "id": "1024",
- "name": "Monthly pass",
- "internalName": "Monthly pass - autumn promo",
- "price": 1750
}
], - "currency": "PLN",
- "totalPrice": 1750,
- "totalPriceBeforeDiscount": 2000,
- "discountAmount": 250,
- "paymentMethod": 0
}
}, - "firedAt": "2026-10-20T10:15:00+0200"
}
]Event name: event.retail.checkout
Fired when a sale is registered for a client - at the point of sale in BackOffice, through the API (POST /retail/orders/checkout,
POST /retail/contracts/checkout) or when a pricing option is assigned to the client automatically.
A single checkout may contain several orders, contracts and wallet payments. Online purchases made by clients in FrontOffice are reported by Payment status instead.
| guid | string Unique identifier of this delivery. |
| webhookGuid | string Identifier of the webhook (Studio > Integrations > Webhooks) this delivery was sent for. Useful when several webhooks point to the same callback URL. |
| event | string Value: event.retail.checkout Name of the event. |
object (RetailWebhookCheckout) Event payload. Its shape depends on the event. | |
| firedAt | string <date-time> Date and time when the event was fired (ISO 8601). |
[- {
- "guid": "8F2B6C1D-4E7A-4B3C-9D21-5A6E7F8C9B0A",
- "webhookGuid": "3C9E1A7B-2D4F-4E8A-B6C0-1F5D9A2E7B43",
- "event": "event.retail.checkout",
- "data": {
- "user": {
- "firstName": "Maria",
- "lastName": "Dvorak",
- "client": {
- "uuid": "1000072",
- "address": {
- "street": "Sunny street 77a",
- "postalCode": "1234AB",
- "state": "Noord-Holland",
- "city": "Amsterdam",
- "countryCode": "NL"
}
}, - "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}, - "clientOrders": [
- {
- "orderStatus": 1,
- "paymentMethod": 0,
- "paidAmount": 1750,
- "dueAmount": 0,
- "paidAt": "2025-05-12T12:45:47+02:00",
- "clientOrderItems": [
- {
- "name": "Yoga Card x4",
- "internalName": "Yoga Card x4 [previously 4x card]",
- "netPrice": 1428,
- "totalPrice": 1750,
- "taxAmount": 322,
- "taxRate": 0.23,
- "retailItemId": "5B66DF6C-00A3-4024-8A5D-D30BE8538D09",
- "retailItemType": 0,
- "serviceType": "TYPE_CLASSROOM_PRICING_OPTION_SERVICE",
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}
], - "totalPrice": 1750,
- "discountAmount": 250,
- "totalPriceBeforeDiscount": 2000,
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}
], - "clientContracts": [
- {
- "name": "Membership",
- "startsAt": "2019-06-01T22:00:00+02:00",
- "endsAt": "2020-05-31T21:59:59+02:00",
- "qualifiedName": "1 Year Contract",
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}
], - "clientWalletPayments": [
- {
- "clientOrderItem": {
- "name": "Yoga Card x4",
- "internalName": "Yoga Card x4 [previously 4x card]",
- "netPrice": 1428,
- "totalPrice": 1750,
- "taxAmount": 322,
- "taxRate": 0.23,
- "retailItemId": "5B66DF6C-00A3-4024-8A5D-D30BE8538D09",
- "retailItemType": 0,
- "serviceType": "TYPE_CLASSROOM_PRICING_OPTION_SERVICE",
- "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}, - "guid": "364F59AB-5B43-4D02-989E-AE2811422709"
}
]
}, - "firedAt": "2026-10-20T10:15:00+0200"
}
]