ওয়েবহুক

ইভেন্ট সাবস্ক্রাইব করুন, 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.

Constant-time verification with a replay window
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.