import type { Metadata } from "next"; import { CodeBlock } from "@/components/ui/code-block"; import { DocPage } from "@/components/docs/doc-page"; import { A, Code, H2, Li, P, Strong, Table, TBody, Td, Th, THead, Tr, Ul } from "@/components/docs/prose"; import { Callout, ComingSoon } from "@/components/docs/callout"; import { CodeTabs } from "@/components/docs/code-tabs"; export const metadata: Metadata = { title: "Webhooks", description: "Webhooks are coming soon. Planned events (usage thresholds, failed requests, billing, API keys), delivery format and the HMAC-SHA256 signature scheme.", }; const EVENTS: Array<[string, string]> = [ ["usage.threshold", "Monthly requests or spend crossed a threshold you configure (for example 80 % and 100 % of the plan quota, or a soft spending limit)."], ["request.failed", "A fetch ended in an error or a fully blocked target. Batched to avoid floods; includes request_id, domain, error code and attempts."], ["billing.invoice.created", "An invoice was issued for the organization."], ["billing.payment.succeeded", "A payment was collected."], ["billing.payment.failed", "A payment attempt failed; includes the retry schedule."], ["billing.plan.changed", "The organization's plan changed."], ["api_key.created", "A key was created in one of your projects."], ["api_key.rotated", "A key's secret was rotated."], ["api_key.revoked", "A key was revoked."], ["api_key.expiring", "A key expires within 7 days."], ]; const PAYLOAD = { id: "evt_7h8i9j0k1l2m3n4o", type: "usage.threshold", created_at: "2026-09-07T15:04:00.000Z", organization_id: "org_9a1b2c3d4e5f6g7h", project_id: "proj_2k8d1m3p9q4r7s6t", data: { metric: "requests", threshold: 0.8, used: 40_012, limit: 50_000, period_start: "2026-09-01T00:00:00.000Z", }, }; const VERIFY_JS = `import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyFetchaSignature(rawBody: string, header: string, secret: string, toleranceSec = 300) { // Header format: t=,v1= const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=") as [string, string])); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = createHmac("sha256", secret).update(\`\${t}.\${rawBody}\`).digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(parts.v1 ?? "", "hex"); return a.length === b.length && timingSafeEqual(a, b); }`; const VERIFY_PY = `import hmac, hashlib, time def verify_fetcha_signature(raw_body: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool: # Header format: t=,v1= parts = dict(kv.split("=", 1) for kv in header.split(",")) t = int(parts.get("t", "0")) if not t or abs(time.time() - t) > tolerance_sec: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", ""))`; export default function WebhooksPage() { return ( Webhook delivery is not live. There is no way to register an endpoint yet, no events are sent, and the details on this page are the planned design published for early integration work. The event names, payload shape and signature scheme may change before launch. Watch the changelog.

Planned events

{EVENTS.map(([name, desc]) => ( ))}
Event When it fires
{name} {desc}

Delivery

  • Events will be delivered as POST requests with a JSON body to the HTTPS URL you register per organization, with the event types you subscribe to.
  • Your endpoint should respond with a 2xx within 10 seconds. Non-2xx responses and timeouts are retried with exponential backoff for up to 24 hours, then recorded as failed in the dashboard.
  • Deliveries are at least once: use id to deduplicate.
  • Payloads never include page content, cookies or API key secrets, only identifiers and metrics.

Signature

Each delivery will carry an X-Fetcha-Signature header so you can verify that the payload came from Fetcha and was not altered. The scheme is HMAC-SHA256 over a timestamp and the raw request body, keyed with the endpoint's secret shown once at creation:

v1 = hex( HMAC_SHA256( secret, signed_payload ) )`} />
  • Compute the HMAC over the raw bytes you received, before any JSON parsing or re-serialisation.
  • Reject deliveries whose timestamp is more than five minutes old to prevent replay.
  • Compare signatures with a constant-time function.
  • During secret rotation both the old and the new secret will be valid for 24 hours; the header may then contain two v1 values.
Poll GET /v1/usage for remaining_requests and spend, and watch the request log in the dashboard. Hard spending limits and project request limits already stop traffic when reached (see Rate limits), which covers the most important protection webhooks would otherwise provide.
); }