Receive webhooks

Get a signed HTTP request on your own server whenever a member, membership, check-in or purchase changes in FitManager.

6 min read Updated 04.10.2026 Requires: API access

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 POST requests 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

  1. In Webhook endpoints, click Add endpoint.
  2. Enter the Endpoint URL, for example https://example.com/webhooks/fitmanager.
  3. Optionally add a Description so you remember what the endpoint is for.
  4. Choose the Environment: Live for your real gym. This cannot be changed later.
  5. Tick the Events you want. Tick * to receive every event, including any added in future.
  6. 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:

  1. Read the X-FitManager-Signature header and split it into t (a Unix timestamp) and v1 (the signature).
  2. Take the raw request body exactly as received, before any JSON parsing.
  3. Compute HMAC-SHA256 of {t}.{body} with your endpoint's signing secret, as a lowercase hex string.
  4. Compare it with v1 using a constant-time comparison.
  5. Reject the request if t is 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 the id to 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.

Newsletter

Get tips in your inbox

One email a month: what we shipped, what gym owners asked for, and one practical idea for running the floor. No spam, unsubscribe in one click.

Unsubscribe any time. We never share your address.

We use cookies to keep you signed in, remember your preferences, and understand how the site is used. Privacy Policy