Receive webhooks
Get a signed HTTP request on your own server whenever a member, membership, check-in or purchase changes in FitManager.
Webhooks push events to your own software as they happen, so it does not have to keep asking the API for changes. When something happens in your gym, FitManager sends a POST request with a JSON body to every endpoint you subscribed to that event.
Before you start
- You need the settings.manage permission and a plan with API access.
- You need a public HTTPS address on your server that accepts
POSTrequests and answers within 15 seconds. - Webhooks are on the same page as your API keys: Settings > Integrations > API integration, card Webhook endpoints.
Add an endpoint
- In Webhook endpoints, click Add endpoint.
- Enter the Endpoint URL, for example
https://example.com/webhooks/fitmanager. - Optionally add a Description so you remember what the endpoint is for.
- Choose the Environment: Live for your real gym. This cannot be changed later.
- Tick the Events you want. Tick
*to receive every event, including any added in future. - Click Save endpoint.
The endpoint appears in the list with its URL, environment and events. Every endpoint gets its own signing secret, which you need to verify requests (see below).
Events
| Event | Sent when | data.object |
|---|---|---|
member.created |
A member is added | member |
member.updated |
Any change is saved on a member | member |
membership.assigned |
A membership is given to a member | member_membership |
membership.expired |
A member's membership changes to expired | member_membership |
checkin.recorded |
A scan or entry is logged at an access point, granted or denied | access_log |
purchase.recorded |
A sale is recorded | purchase |
* |
Any of the above |
What a delivery looks like
Every delivery is a POST with these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-FitManager-Event |
The event name, for example member.created |
X-FitManager-Signature |
t={timestamp},v1={signature} |
The body is the event wrapped in an envelope. data holds the record as it was saved, with passwords removed:
{
"id": "evt_01JA3K7Q8ZJ4X6V2N9P5R1T0WB",
"event": "member.created",
"created": 1791105164,
"gym_id": 12,
"environment": "live",
"data": {
"id": 4821,
"facility_id": 31,
"first_name": "Elena",
"last_name": "Stojanovska",
"email": "elena@example.mk",
"member_code": "K7PX2QHM",
"phone": "+389 70 123 456",
"date_of_birth": null,
"gender": null,
"city": "Skopje",
"country": "MK",
"rfid_tag": null,
"status": "active",
"created_at": "2026-10-04T09:12:44.000000Z",
"updated_at": "2026-10-04T09:12:44.000000Z",
"object": "member"
}
}
The fields in data follow the record, so new fields can appear over time. Ignore fields you do not use rather than rejecting the request.
Verify the signature
Check every request before you trust it. To verify:
- Read the
X-FitManager-Signatureheader and split it intot(a Unix timestamp) andv1(the signature). - Take the raw request body exactly as received, before any JSON parsing.
- Compute HMAC-SHA256 of
{t}.{body}with your endpoint's signing secret, as a lowercase hex string. - Compare it with
v1using a constant-time comparison. - Reject the request if
tis more than 5 minutes away from your server's clock, to stop replays.
To see the secret, click Signing secret on the endpoint, then Copy. It starts with whsec_.
PHP
$secret = getenv('FITMANAGER_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_FITMANAGER_SIGNATURE'] ?? '';
$parts = [];
foreach (explode(',', $header) as $segment) {
[$key, $value] = array_pad(explode('=', trim($segment), 2), 2, null);
$parts[$key] = $value;
}
$timestamp = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $timestamp.'.'.$body, $secret);
if (abs(time() - $timestamp) > 300 || ! hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
http_response_code(200);
Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhooks/fitmanager', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.FITMANAGER_WEBHOOK_SECRET;
const header = req.get('X-FitManager-Signature') || '';
const parts = Object.fromEntries(header.split(',').map(p => p.trim().split('=')));
const timestamp = parseInt(parts.t, 10);
const expected = crypto.createHmac('sha256', secret)
.update(`${timestamp}.${req.body.toString('utf8')}`)
.digest('hex');
const valid = parts.v1 && parts.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
&& Math.abs(Date.now() / 1000 - timestamp) <= 300;
if (!valid) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
});
Respond quickly
Answer with any 2xx status within 15 seconds. Anything else, a timeout or a connection error counts as a failure. If your handling takes longer, store the event, answer 200, and process it afterwards.
Failures and redelivery
- FitManager sends each event once. It does not retry a failed delivery on its own, so use Redeliver (below) after fixing your server.
- After 10 failures in a row, the endpoint is disabled automatically. It shows a Disabled badge and the reason "Auto-disabled after 10 consecutive delivery failures." Any successful delivery resets the count.
- To turn it back on, click Enable on the endpoint. This also resets the failure count. Events that happened while it was disabled are not sent, so redeliver the ones you need.
- A redelivery sends the same body with the same
id, with a fresh timestamp and signature. Use theidto ignore events you have already processed.
The delivery log
The Recent deliveries card lists every request sent, newest first, with the Event, the Endpoint, the response Status (or No response), the Attempt number and when it was Delivered.
- Filter with All results, Succeeded or Failed.
- Click Inspect to see the full payload, with a Copy button.
- Click Redeliver to send it again. It is added to the log as a new row with the next attempt number. Redeliver only works while the endpoint is enabled.
You can also redeliver from code with POST /api/v1/webhook-deliveries/{id}/redeliver, which answers 202.
Manage an endpoint
| Button | What it does |
|---|---|
| Signing secret / Hide secret | Shows or hides the secret, with Copy and Rotate |
| Rotate | Replaces the secret straight away. Requests signed with the old secret stop verifying, so update your server right after |
| Disable / Enable | Pauses or resumes deliveries |
| Edit | Changes the URL, description and events. The environment stays fixed |
| Delete | Removes the endpoint. Its deliveries stop at once |
Troubleshooting
My endpoint gets nothing
Check that the endpoint is not Disabled, that the event is ticked, and that the action really happened in the gym whose dashboard you are using. Look at Recent deliveries with the Failed filter.
Signatures never match
Verify against the raw body. Frameworks that parse and re-encode JSON change spacing and escaping, which breaks the signature. In Express use express.raw(), in Laravel use $request->getContent(). Also make sure you rotated the secret on both sides.
Test endpoints stay silent
An endpoint with Environment set to Test is meant for events from your sandbox gym, but test endpoints currently do not receive deliveries. While building, point a Live endpoint at your development server instead.
Was this page helpful?
Related articles
Still stuck?
Write to us and we will get back to you within one working day.