Preskočiť na hlavný obsah
← Hlavná stránka

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 2xx response
  • 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', 200

PHP

<?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

EventTrigger
BOOKING_CREATEDCustomer vytvorí rezerváciu cez widget alebo staff cez dashboard
BOOKING_CANCELLEDCustomer/staff zruší pred startAt
BOOKING_RESCHEDULEDCustomer/staff zmení startAt rezervácie
BOOKING_COMPLETEDStaff označí ako dokončené po termíne
BOOKING_NO_SHOWStaff označí ako neúčasť (no-show)
BOOKING_PAIDStripe Payment Intent succeeded webhook
CUSTOMER_CREATEDNový 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.