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:
| Header | Description |
|---|---|
X-Hub-API-Signature-256 | HMAC-SHA256 signature in sha256=<hex> format |
X-Hub-API-Timestamp | UTC 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
- PHP (SDK)
- PHP (Manual)
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
}
Verify without the SDK — suitable for projects without Composer:
<?php
// Read raw body
$payload = file_get_contents('php://input');
// Get signature from header
$signature = $_SERVER['HTTP_X_HUB_API_SIGNATURE_256'] ?? null;
$timestamp = $_SERVER['HTTP_X_HUB_API_TIMESTAMP'] ?? null;
if ($signature === null || $timestamp === null) {
http_response_code(401);
echo json_encode(['error' => 'Missing signature headers']);
exit;
}
// Validate timestamp (recommended: ±5 minutes)
$now = time();
$webhookTime = (int) $timestamp;
if (abs($now - $webhookTime) > 300) {
http_response_code(401);
echo json_encode(['error' => 'Timestamp expired']);
exit;
}
// Calculate expected signature
$secret = $_ENV['WEBHOOK_SECRET'];
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
// Timing-safe comparison (important!)
if (!hash_equals($expected, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Signature valid — process payload
$data = json_decode($payload, true);
// ...
Security Best Practices
| Practice | Why |
|---|---|
| Timing-safe comparison | hash_equals() (PHP) / crypto.timingSafeEqual() (Node.js) prevents timing attacks |
| Validate timestamp | X-Hub-API-Timestamp must be within ±5 minutes of current time |
| Never log the secret | The secret is confidential — never log or expose it to clients |
| Rotate if compromised | Generate a new API token on Hub API and update your server |
Troubleshooting
| Issue | Solution |
|---|---|
| Signature always invalid | Make sure you're comparing the raw request body, not parsed JSON |
401 Unauthorized | Secret doesn't match — make sure your server uses the same V2_API_TOKEN as the Hub API |
Timestamp expired | Ensure your server has an accurate clock (enable NTP) |
Missing X-Hub-API-Signature-256 header | Make sure the server sends headers with X-Hub-API- prefix (not X-Hub-) |