Webhook fogadó beállítása + aláírás-ellenőrzés
Saját szerver, Node.js / PHP / Python receiver kód, HMAC verify, idempotency
Webhook fogadó beállítása
A Bookinda webhook minden saját HTTPS endpointtal működik. Itt a teljes setup mintakóddal.
Receiver szabályai
- HTTPS kötelező — TLS nélkül nem küldünk eventet
- 2xx-szel válaszolj 30 mp-en belül — különben retry
- Idempotens legyél — ugyanaz az event kétszer is jöhet (retry miatt). Az
idmező a kulcs. - HMAC-aláírást ellenőrizd — különben hamisíthatóak az eventjeid
- Replay window — 5 percnél régebbi eventeket utasítsd el
Node.js / TypeScript (Express)
import express from "express";
import crypto from "crypto";
const app = express();
const WEBHOOK_SECRET = process.env.BOOKINDA_WEBHOOK_SECRET!;
function verify(secret: string, body: string, sigHeader: string, tolerance = 300) {
const parts = Object.fromEntries(
sigHeader.split(",").map(p => p.split("="))
);
const t = parseInt(parts.t, 10);
const v1 = parts.v1;
if (!t || !v1) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > tolerance) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(v1, "hex")
);
}
const seen = new Set<string>(); // production: Redis vagy DB
app.post(
"/bookinda-webhook",
express.raw({ type: "application/json" }),
async (req, res) => {
const body = req.body.toString();
const sig = req.headers["x-bookinda-signature"] as string;
if (!verify(WEBHOOK_SECRET, body, sig)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(body);
// Idempotency
if (seen.has(event.id)) return res.status(200).send("duplicate");
seen.add(event.id);
// Branch by event type
switch (event.event) {
case "appointment.created":
await handleNewAppointment(event.data);
break;
case "customer.created":
await handleNewCustomer(event.data);
break;
case "sale.completed":
await handleSaleCompleted(event.data);
break;
}
res.status(200).send("ok");
}
);
app.listen(3000);
PHP
<?php
$secret = getenv('BOOKINDA_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_BOOKINDA_SIGNATURE'] ?? '';
function bookindaVerify(string $secret, string $body, string $sig, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $sig) as $p) {
[$k, $v] = explode('=', $p, 2);
$parts[$k] = $v;
}
$t = intval($parts['t'] ?? 0);
$v1 = $parts['v1'] ?? '';
if (!$t || !$v1) return false;
if (abs(time() - $t) > $tolerance) return false;
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
return hash_equals($expected, $v1);
}
if (!bookindaVerify($secret, $body, $sigHeader)) {
http_response_code(401);
exit('invalid signature');
}
$event = json_decode($body, true);
// idempotency: cache event ID-t Redis/DB-ben
switch ($event['event']) {
case 'appointment.created':
// ...
break;
case 'sale.completed':
// ...
break;
}
http_response_code(200);
echo 'ok';
Python (Flask)
import os, hmac, hashlib, time, json
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['BOOKINDA_WEBHOOK_SECRET'].encode()
def verify(body: bytes, sig_header: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in sig_header.split(","))
t = int(parts.get("t", 0))
v1 = parts.get("v1", "")
if not t or not v1: return False
if abs(time.time() - t) > tolerance: return False
expected = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
@app.post("/bookinda-webhook")
def webhook():
body = request.get_data()
sig = request.headers.get("X-Bookinda-Signature", "")
if not verify(body, sig):
abort(401)
event = json.loads(body)
# idempotency check via event["id"]
# branch by event["event"]
return "ok", 200
Headerek áttekintő
| Header | Tartalom |
|---|---|
| Content-Type | application/json |
| X-Bookinda-Event | event név (pl. appointment.created) |
| X-Bookinda-Delivery | event id (idempotency key) |
| X-Bookinda-Api-Version | 2026-05 |
| X-Bookinda-Timestamp | unix másodperc |
| X-Bookinda-Signature | t=<unix>,v1=<hmac_hex> |
| User-Agent | Bookinda-Webhooks/1.0 |
Hibaelhárítás
- 401 Invalid signature: ellenőrizd, hogy a raw body bytes-okat hash-eled, NEM JSON.parse-elt + re-stringify változatot. A tested-et a Test gombbal indítsd el a manager UI-ból
- Időzítési hiba: ha a szervered órája el van csúszva, a tolerance window túlléphet. NTP fix
- Duplikált event: idempotency hiányzik, az event.id alapján skippelj
#webhook#receiver#verify#aláírás#hmac#security#sdk#node#php#python
