Concepts
Webhooks
STRYK sends an HTTPS POST to your endpoint when bookings, guests, availability, rates or payments change. v1 has no invoice event; reconcile GET /invoices with updated_since. Each delivery is signed, retried on failure and visible in the delivery log.
Set up an endpoint
- Create the endpoint
In STRYK, open Channels → API, tab Webhooks, choose Create webhook, enter an HTTPS URL and choose the events.
- Store the signing secret
STRYK shows the secret (
whsec_…) once. Keep it on your server, like an API key. - Send a test event
Use Send test event. STRYK delivers a
webhook.testevent so you can check your signature verification.
The URL must use HTTPS and reach a public address. STRYK does not follow redirects and rejects private or local targets, both when you save the endpoint and on every delivery.
Events
| Event | Sent when |
|---|---|
booking.created | A new booking exists in STRYK. |
booking.updated | A booking changed: status, dates, room, guest assignment, notes. data.changes names the areas (status, rooms, guest, details, deleted). |
booking.cancelled | A booking was cancelled. |
guest.updated | Guest contact data changed. |
availability.changed | Free units or restrictions changed for room types and a date range. Changes are combined per room type and period. |
rate.changed | Daily prices changed for room types and a date range, combined like availability. |
payment.updated | The paid amount of a booking changed; data.bookingId names the booking. |
webhook.test | You requested a test delivery in STRYK. |
unit.updated | unit.updated |
GET /webhook-event-types returns the current list with descriptions.
Payload
Events are thin: they contain the type, the affected object and its ID, but not the full object. Fetch the current state from the API. This keeps personal data out of logs and queues, and you never act on a stale copy.
{
"id": "evt_1f6c9e2a-3b4d-4e5f-8a7b-6c5d4e3f2a1b",
"type": "booking.updated",
"apiVersion": "v1",
"createdAt": "2026-11-04T08:20:31.000Z",
"hotelId": "hotel-alpenblick",
"data": { "object": "booking", "id": "9d3c1b2a-7e6f-4a5b-8c9d-0e1f2a3b4c5d", "changes": ["status"] }
}{
"id": "evt_7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
"type": "availability.changed",
"apiVersion": "v1",
"createdAt": "2026-11-04T08:21:02.000Z",
"hotelId": "hotel-alpenblick",
"data": {
"object": "availability",
"roomTypeIds": ["0b7e2c1d-5f4a-4c3b-9e8d-7a6b5c4d3e2f"],
"from": "2026-12-01",
"to": "2026-12-04"
}
}Headers
POST /webhooks/stryk HTTP/1.1
Content-Type: application/json
User-Agent: STRYK-Webhooks/1.0
STRYK-Event-Id: evt_1f6c9e2a-3b4d-4e5f-8a7b-6c5d4e3f2a1b
STRYK-Event-Type: booking.updated
STRYK-Delivery-Id: 2b1a0f9e-8d7c-4b6a-a594-837261504f3e
STRYK-Delivery-Attempt: 1
STRYK-Signature: t=1793780431,v1=5f2c8a1e9b0d…| Header | Content |
|---|---|
STRYK-Event-Id | Event ID, identical to id in the body. The same event keeps its ID on every attempt. |
STRYK-Event-Type | Event type, e.g. booking.updated. |
STRYK-Delivery-Id | ID of this delivery to this endpoint; shown in the delivery log. |
STRYK-Delivery-Attempt | Attempt number, starting at 1. |
STRYK-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>; during a secret rotation two v1 values. |
Verifying the signature
STRYK computes HMAC-SHA256(secret, "<t>.<rawBody>") with your signing secret as the key, the full value as shown, including whsec_, as UTF-8 bytes, and sends it hex-encoded as v1. Verify every request before you use it:
- Read the raw body
Use the exact bytes you received. Parsing and re-serialising JSON changes spacing and order, and the signature no longer matches.
- Check the timestamp
Reject requests whose
tis more than 300 seconds away from your clock. This blocks replays of recorded requests. - Compare in constant time
Compute the HMAC over
t, a dot and the raw body, and compare it with eachv1value using a constant-time comparison. - Accept one match
After you rotate the secret, STRYK signs with the old and new secret for 24 hours. The header then has two
v1values; one valid match is enough.
import crypto from 'node:crypto';
import express from 'express';
export function verifyStrykSignature(rawBody, header, secret, toleranceSeconds = 300) {
if (!header) return false;
let timestamp = null;
const signatures = [];
for (const part of header.split(',')) {
const [key, value] = part.trim().split('=', 2);
if (key === 't') timestamp = Number(value);
if (key === 'v1' && value) signatures.push(value);
}
if (!Number.isInteger(timestamp) || signatures.length === 0) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
// During a secret rotation the header carries two v1 values; one match is enough.
return signatures.some((signature) => {
const received = Buffer.from(signature, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
// express.raw keeps the exact bytes. Parsing JSON first would break the signature.
app.post('/webhooks/stryk', express.raw({ type: 'application/json', limit: '1mb' }), (req, res) => {
if (!verifyStrykSignature(req.body, req.get('STRYK-Signature'), process.env.STRYK_WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(204); // acknowledge within seconds …
queue.add(event); // … and process asynchronously, deduplicated by event.id
});import hashlib
import hmac
import json
import os
import time
from flask import Flask, abort, request
def verify_stryk_signature(raw_body: bytes, header: str | None, secret: str, tolerance: int = 300) -> bool:
if not header:
return False
timestamp, signatures = None, []
for part in header.split(","):
key, _, value = part.strip().partition("=")
if key == "t":
timestamp = value
elif key == "v1" and value:
signatures.append(value)
if not timestamp or not timestamp.isdigit() or not signatures:
return False
if abs(int(time.time()) - int(timestamp)) > tolerance:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
# During a secret rotation the header carries two v1 values; one match is enough.
return any(hmac.compare_digest(expected, signature) for signature in signatures)
app = Flask(__name__)
@app.post("/webhooks/stryk")
def stryk_webhook():
raw_body = request.get_data() # exact bytes, before any JSON parsing
if not verify_stryk_signature(raw_body, request.headers.get("STRYK-Signature"), os.environ["STRYK_WEBHOOK_SECRET"]):
abort(400)
event = json.loads(raw_body)
enqueue(event) # process asynchronously, deduplicated by event["id"]
return "", 204Responding and processing
- Answer with any
2xxstatus within 10 seconds. STRYK discards the response body. - Acknowledge first, then process in a queue or background job. Slow handlers cause timeouts and duplicate deliveries.
- Make processing idempotent by event
id: store processed IDs and skip repeats. - Do not rely on order. Compare
updatedAtof the fetched object with what you have.
Retries and automatic disabling
A delivery fails on a timeout, a network error or any non-2xx status. STRYK retries with increasing delays:
| Attempt | Delay after the previous attempt |
|---|---|
| 1 | immediately |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
| 7 | 12 hours |
| 8 | 24 hours |
After the eighth failed attempt the delivery is marked as failed for good. If an endpoint fails 20 times in a row over at least 24 hours without a single success, STRYK disables it automatically. Fix the endpoint, then enable it again in STRYK.
Delivery log and manual retry
In Channels → API → Webhooks, the Deliveries section of a webhook shows each delivery with time, event, status, attempts, HTTP status, next attempt and delivery time. Send again retries a single delivery, for example after a fix. Rotate secret rotates the signing secret in the same place.