ওয়েবহুক
ইভেন্ট সাবস্ক্রাইব করুন, HMAC সিগনেচার যাচাই করুন, রিট্রাই সামলান এবং হ্যান্ডলারকে আইডেমপোটেন্ট রাখুন।
Version v1 · updated 2026-02-01
A webhook endpoint is a public URL on your infrastructure. Treat every request to it as hostile until the signature says otherwise — the payload is not proof of anything.
Subscribing#
curl -s -X POST "https://api.framique.com/api/public/v1/webhooks" \
-H "Authorization: Bearer $FRAMIQUE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.example.com/hooks/framique",
"events": ["order.created", "order.paid", "product.updated"],
"description": "Fulfilment sync"
}'Verifying the signature#
Each delivery carries `x-framique-timestamp` and `x-framique-signature`. The signed string is `timestamp.rawBody` — verify against the raw bytes, never against a re-serialised object, because key order and unicode escaping will not survive a round trip.
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verify(raw: string, headers: Headers, secret: string): boolean {
const timestamp = headers.get("x-framique-timestamp");
const signature = headers.get("x-framique-signature");
if (!timestamp || !signature) return false;
// Reject anything outside the window before spending a hash on it.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${raw}`).digest("hex");
const a = Buffer.from(signature, "utf8");
const b = Buffer.from(expected, "utf8");
// Length check first: timingSafeEqual throws on a length mismatch.
return a.length === b.length && timingSafeEqual(a, b);
}Delivery, retries and idempotency#
- Return 2xx within 5 seconds. Queue the work; do not do it inline.
- Failures retry with exponential backoff for 24 hours, then the endpoint is suspended and the merchant is notified.
- At-least-once delivery is guaranteed; exactly-once is not. Deduplicate on `event_id`.
- Events are ordered per resource, not globally. Compare `occurred_at` before overwriting your copy.