A webhook tells another system when something happens, by posting JSON to a web address of its choosing.
The account owner adds one in Settings, Integrations, Webhooks, with the events it wants, and is given a signing secret for it.
The address must start with https://.
Each event is sent once the change behind it is complete, usually within a minute, and data is the record as the API describes it.
An event fires however the change was made: in FloorLogik, through the API, or by a customer paying online.
Headers
FloorLogik-Event: the event, such as order.created.
FloorLogik-Delivery: the event's id, the same as id in the body. A retry carries the same id, so use it to ignore one you have already handled.
FloorLogik-Signature: t=<unix time>,v1=<signature>, explained next.
Checking it came from us
The signature is a hex HMAC-SHA256 of the time, a full stop, and the body exactly as sent, keyed with the webhook's secret.
Work it out the same way and compare. Refuse anything more than five minutes old, so a captured request cannot be replayed.
PHP
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_FLOORLOGIK_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $sig); // t=..., v1=...
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, $secret);
$fresh = abs(time() - (int) ($sig['t'] ?? 0)) <= 300;
if (! $fresh || ! hash_equals($expected, $sig['v1'] ?? '')) {
http_response_code(400);
exit;
}
$event = json_decode($body, true); // then answer 200 quickly
Node.js
const crypto = require('crypto');
// rawBody must be the body exactly as received, before any JSON parsing.
function verify(rawBody, header, secret) {
const sig = Object.fromEntries(header.split(',').map(p => p.split('=')));
const expected = crypto.createHmac('sha256', secret)
.update(`${sig.t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) <= 300;
return fresh && sig.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));
}
Answering, and retries
Answer with any 2xx status within 10 seconds, and do any slow work afterwards.
Anything else, or no answer, is tried again: 6 tries in all, after
1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours.
Redirects are not followed.
A webhook that fails 25 events in a row is switched off, and shows as off in Settings.
Switch it back on there once the receiving end is fixed. Settings also shows the last few deliveries and can send a test.