Webhooks
Terminuj posiela HTTP POST s JSON payload-om do vašich endpointov pri každej zmene rezervácie. Každý request je podpísaný HMAC SHA-256 — overte podpis pred spracovaním.
1. Konfigurácia
V dashboard → Webhooks → Vytvoriť webhook:
- URL: váš HTTPS endpoint (HTTP nepovolené)
- Events: jeden alebo viac eventov (pozri sekciu 4)
- Secret: auto-generated
whsec_*— uložte do env
2. Doručenie + retry
- Timeout: 10 s per request
- Successful: HTTP
2xxresponse - Retry pri failure (HTTP
5xx, timeout, network error): 5 attempts s exponential backoff (1m, 5m, 30m, 2h, 12h) - Po 5 attempts → marked FAILED, manual retry v dashboard
- Cron tick každú minútu — dispatch + retry due deliveries
3. HMAC SHA-256 verification
Každý request obsahuje header X-Rezervacia-Signature v formáte sha256=<hex>. Vypočítajte HMAC z RAW body (NIE parsed JSON) a porovnajte timing-safe.
Node.js (Express)
const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-rezervacia-signature']; // "sha256=<hex>"
const secret = process.env.REZERVACIA_WEBHOOK_SECRET;
const computed = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.body) // Buffer raw body — NIE parsed JSON!
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computed))) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString());
console.log('Event:', event.event, 'Data:', event.data);
res.status(200).send('ok');
});Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['REZERVACIA_WEBHOOK_SECRET'].encode()
@app.post('/webhook')
def webhook():
signature = request.headers.get('X-Rezervacia-Signature', '')
body = request.get_data() # raw bytes — NEPARSOVAŤ JSON pred verifikáciou!
computed = 'sha256=' + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, computed):
abort(401)
event = request.get_json()
print('Event:', event['event'], 'Data:', event['data'])
return 'ok', 200PHP
<?php
$secret = getenv('REZERVACIA_WEBHOOK_SECRET');
$signature = $_SERVER['HTTP_X_REZERVACIA_SIGNATURE'] ?? '';
$body = file_get_contents('php://input'); // raw — NIE $_POST!
$computed = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($signature, $computed)) {
http_response_code(401);
exit('invalid signature');
}
$event = json_decode($body, true);
error_log('Event: ' . $event['event']);
http_response_code(200);
echo 'ok';Ruby (Sinatra)
require 'sinatra'
require 'openssl'
SECRET = ENV.fetch('REZERVACIA_WEBHOOK_SECRET')
post '/webhook' do
signature = request.env['HTTP_X_REZERVACIA_SIGNATURE'] || ''
body = request.body.read
computed = 'sha256=' + OpenSSL::HMAC.hexdigest('sha256', SECRET, body)
halt 401 unless Rack::Utils.secure_compare(signature, computed)
event = JSON.parse(body)
puts "Event: #{event['event']}"
status 200
body 'ok'
end⚠ NIKDY neverifikujte podpis z parsed JSON. JSON serialization medzi knižnicami sa líši v whitespace / key order — vždy použite RAW body.
4. Event types
| Event | Trigger |
|---|---|
BOOKING_CREATED | Customer vytvorí rezerváciu cez widget alebo staff cez dashboard |
BOOKING_CANCELLED | Customer/staff zruší pred startAt |
BOOKING_RESCHEDULED | Customer/staff zmení startAt rezervácie |
BOOKING_COMPLETED | Staff označí ako dokončené po termíne |
BOOKING_NO_SHOW | Staff označí ako neúčasť (no-show) |
BOOKING_PAID | Stripe Payment Intent succeeded webhook |
CUSTOMER_CREATED | Nový zákazník (prvá rezervácia) |
5. Payload schema
Všetky eventy zdieľajú top-level shape: { event, eventId, createdAt, data }.eventId je unikátny — použite pre idempotentné spracovanie (riešenie duplicate delivery pri retry).
BOOKING_CREATED
{
"event": "BOOKING_CREATED",
"eventId": "evt_01HXYZ...",
"createdAt": "2026-06-10T10:30:00.000Z",
"data": {
"id": "uuid",
"publicReference": "BK4F2H8K9M3N7P",
"tenantSlug": "studio-jk",
"serviceId": "uuid",
"serviceName": "Strih + farbenie",
"startAt": "2026-06-15T14:00:00.000Z",
"endAt": "2026-06-15T15:30:00.000Z",
"status": "CONFIRMED",
"customer": {
"id": "uuid",
"fullName": "Jana Vassová",
"email": "jana@example.com",
"phone": "+421900123456"
},
"priceVat": "45.00",
"currency": "EUR"
}
}BOOKING_CANCELLED
{
"event": "BOOKING_CANCELLED",
"eventId": "evt_01HXYZ...",
"createdAt": "2026-06-12T08:15:00.000Z",
"data": {
"id": "uuid",
"publicReference": "BK4F2H8K9M3N7P",
"cancelledAt": "2026-06-12T08:15:00.000Z",
"cancelledBy": "customer",
"cancellationReason": "Choroba",
"refundEligible": true,
"refundAmount": "45.00"
}
}Plné schémy pre ostatné eventy: pozri lib/webhooks/payload.ts (Phase 5: OpenAPI 3 dokumentácia).
6. Testing
V dashboard → Webhooks → endpoint detail → "Test" button odošle synthetic BOOKING_CREATED payload s validným podpisom na váš URL. Použite pre integration test pred aktiváciou.