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

  1. HTTPS kötelező — TLS nélkül nem küldünk eventet
  2. 2xx-szel válaszolj 30 mp-en belül — különben retry
  3. Idempotens legyél — ugyanaz az event kétszer is jöhet (retry miatt). Az id mező a kulcs.
  4. HMAC-aláírást ellenőrizd — különben hamisíthatóak az eventjeid
  5. 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ő

HeaderTartalom
Content-Typeapplication/json
X-Bookinda-Eventevent név (pl. appointment.created)
X-Bookinda-Deliveryevent id (idempotency key)
X-Bookinda-Api-Version2026-05
X-Bookinda-Timestampunix másodperc
X-Bookinda-Signaturet=<unix>,v1=<hmac_hex>
User-AgentBookinda-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
💬

Van kérdésed? Kérdezd az AI asszisztenst

A Bookinda AI ismeri az egész tudástárat, és másodperceken belül válaszol.

B

Bookinda AI Asszisztens

Kérdezz bármit a funkciókról, beállításokról, integrációkról.

Szia! Bookinda asszisztens vagyok. Tudok válaszolni funkciókkal, számlázással, integrációkkal kapcsolatos kérdésekre. Miben segíthetek?

Még mindig segítségre van szükséged?

Lépj kapcsolatba az ügyfélszolgálatunkkal.

Kapcsolatfelvétel