☰ Menu · Webhooks
Webhooks
Get an HTTPS POST when something happens: a check applied anywhere, a reward unlocked, an order placed at a table.
Events cover everything done through the API at this venue, by your integration and by any other one (another till, a kiosk, a booking connector). Subscribe only to the events you use.
Add an endpoint
In Developers → Webhooks or with the API. The URL must be public HTTPS. The signing secret is returned once, when the endpoint is created.
curl https://loyaltyfy.io/api/v1/webhook_endpoints \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://pos.example.com/loyaltyfy/webhooks",
"events": ["transaction.completed", "order.created"]
}'{
"object": "webhook_endpoint",
"id": "01c2a784-4bcf-43b9-8ac9-ddbad7a25341",
"url": "https://pos.example.com/loyaltyfy/webhooks",
"description": null,
"events": ["transaction.completed", "order.created"],
"status": "enabled",
"disabled_reason": null,
"livemode": false,
"created_at": "2026-10-06T18:17:32.381+00:00",
"secret": "whsec_UvjHvXfPk80ITYl_xPeAtskI0WjR8-PS"
}What you receive
POST /loyaltyfy/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: Loyaltyfy-Webhooks/1.0
Loyaltyfy-Signature: t=1791310488,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd{
"id": "evt_5a1f0c9e2b7d4e3f81a0c6d2",
"object": "event",
"type": "order.created",
"created": 1791310488,
"livemode": true,
"data": {
"object": {
"object": "order",
"id": "0c69a81c-0fbc-4333-b26c-789566b9bea4",
"number": 10,
"status": "new",
"table": "7",
...
}
}
}data.object is the same object the API returns (a transaction, a member, an order), as it was at the moment of the event. Answer with any 2xx within 10 seconds. Do the slow work after answering.
Verify the signature
Loyaltyfy-Signature has a timestamp t and a signature v1: the HMAC-SHA256 of t, a dot and the raw request body, keyed with your endpoint's secret, in hex. Compute it over the exact bytes you received, before any JSON parsing, compare in constant time, and reject timestamps older than five minutes.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (a string or Buffer, not parsed JSON)
export function verifyLoyaltyfy(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hmac, hashlib, time
def verify_loyaltyfy(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Retries
If your endpoint does not answer 2xx in time, we retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. After 20 failed deliveries in a row the endpoint is disabled and the venue sees why in the dashboard; re-enable it with PATCH /v1/webhook_endpoints/{id} and enabled: true.
Because of retries an event can arrive more than once, and events can arrive out of order. Use the event id to skip duplicates and the object's own timestamps to decide what is newer.
Test an endpoint
curl -X POST https://loyaltyfy.io/api/v1/webhook_endpoints/01c2a784-4bcf-43b9-8ac9-ddbad7a25341/test \
-H "Authorization: Bearer sk_test_..."
# { "object": "webhook_test", "delivered": true, "status_code": 200, "error": null }The full list of events is in the Events reference.