Skip to main content

Webhook Signature Verification

Every webhook sent to your server is signed using HMAC-SHA256. By verifying the signature, you ensure that the webhook truly originates from Fingerspot Hub API and not from a third party.

Headers

Every webhook POST includes the following headers:

HeaderDescription
X-Hub-API-Signature-256HMAC-SHA256 signature in sha256=<hex> format
X-Hub-API-TimestampUTC Unix timestamp (seconds)

How It Works

┌─────────────────────────────────────────────────────────┐
│ Hub API Server │
│ Signs every webhook with V2_API_TOKEN (HMAC-SHA256) │
└──────────────────┬──────────────────────────────────────┘
│ every webhook:
│ signs payload with API token

┌─────────────────────────────────────────────────────────┐
│ 1. Webhook POST arrives at your server │
│ Headers: X-Hub-API-Signature-256 │
│ X-Hub-API-Timestamp │
└──────────────────┬──────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 2. Verify signature using the same API token │
│ hmac(api_token, payload) == signature? │
│ YES → process webhook │
│ NO → reject (401) │
└─────────────────────────────────────────────────────────┘

Step 1 — Get the API Token

The webhook signing secret is the same V2_API_TOKEN used for API authentication. Ask your integration provider for the token — it is shared once during setup.

Step 2 — Store the Secret

Store in an environment variable. Never hardcode in source code:

# .env — use the same V2_API_TOKEN from your Hub API
WEBHOOK_SECRET=your_api_token_here

Step 3 — Verify the Signature

Use the Webhook class from the SDK — signature is verified automatically:

<?php

use Fingerspot\HubClient\Webhook\Webhook;
use Fingerspot\HubClient\Models\WebhookData\GetUserResultData;

$secret = $_ENV['WEBHOOK_SECRET'];
$webhook = new Webhook($secret);

// handle() verifies signature + parses payload
// returns null if signature is invalid
$result = $webhook->handle();

if ($result === null) {
http_response_code(401);
exit;
}

// Signature valid — process based on event type
if ($result->isPush()) {
// Push event (device activity, attendance, etc.)
$event = $result->getEvent(); // e.g. 'attendance', 'doorOpen'
$data = $result->getData(); // raw payload array
} else {
// Command result (getUser, setTime, etc.)
$command = $result->getCommand();
$data = $result->getData(GetUserResultData::class);
// $data->users contains UserInfo[] array
}

Security Best Practices

PracticeWhy
Timing-safe comparisonhash_equals() (PHP) / crypto.timingSafeEqual() (Node.js) prevents timing attacks
Validate timestampX-Hub-API-Timestamp must be within ±5 minutes of current time
Never log the secretThe secret is confidential — never log or expose it to clients
Rotate if compromisedGenerate a new API token on Hub API and update your server

Troubleshooting

IssueSolution
Signature always invalidMake sure you're comparing the raw request body, not parsed JSON
401 UnauthorizedSecret doesn't match — make sure your server uses the same V2_API_TOKEN as the Hub API
Timestamp expiredEnsure your server has an accurate clock (enable NTP)
Missing X-Hub-API-Signature-256 headerMake sure the server sends headers with X-Hub-API- prefix (not X-Hub-)