TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import type { Metadata } from "next";2import { CodeBlock } from "@/components/ui/code-block";3import { DocPage } from "@/components/docs/doc-page";4import { A, Code, H2, Li, P, Strong, Table, TBody, Td, Th, THead, Tr, Ul } from "@/components/docs/prose";5import { Callout, ComingSoon } from "@/components/docs/callout";6import { CodeTabs } from "@/components/docs/code-tabs";78export const metadata: Metadata = {9 title: "Webhooks",10 description: "Webhooks are coming soon. Planned events (usage thresholds, failed requests, billing, API keys), delivery format and the HMAC-SHA256 signature scheme.",11};1213const EVENTS: Array<[string, string]> = [14 ["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)."],15 ["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."],16 ["billing.invoice.created", "An invoice was issued for the organization."],17 ["billing.payment.succeeded", "A payment was collected."],18 ["billing.payment.failed", "A payment attempt failed; includes the retry schedule."],19 ["billing.plan.changed", "The organization's plan changed."],20 ["api_key.created", "A key was created in one of your projects."],21 ["api_key.rotated", "A key's secret was rotated."],22 ["api_key.revoked", "A key was revoked."],23 ["api_key.expiring", "A key expires within 7 days."],24];2526const PAYLOAD = {27 id: "evt_7h8i9j0k1l2m3n4o",28 type: "usage.threshold",29 created_at: "2026-09-07T15:04:00.000Z",30 organization_id: "org_9a1b2c3d4e5f6g7h",31 project_id: "proj_2k8d1m3p9q4r7s6t",32 data: {33 metric: "requests",34 threshold: 0.8,35 used: 40_012,36 limit: 50_000,37 period_start: "2026-09-01T00:00:00.000Z",38 },39};4041const VERIFY_JS = `import { createHmac, timingSafeEqual } from "node:crypto";4243export function verifyFetchaSignature(rawBody: string, header: string, secret: string, toleranceSec = 300) {44 // Header format: t=<unix seconds>,v1=<hex hmac>45 const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=") as [string, string]));46 const t = Number(parts.t);47 if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;48 const expected = createHmac("sha256", secret).update(\`\${t}.\${rawBody}\`).digest("hex");49 const a = Buffer.from(expected, "hex");50 const b = Buffer.from(parts.v1 ?? "", "hex");51 return a.length === b.length && timingSafeEqual(a, b);52}`;5354const VERIFY_PY = `import hmac, hashlib, time5556def verify_fetcha_signature(raw_body: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool:57 # Header format: t=<unix seconds>,v1=<hex hmac>58 parts = dict(kv.split("=", 1) for kv in header.split(","))59 t = int(parts.get("t", "0"))60 if not t or abs(time.time() - t) > tolerance_sec:61 return False62 expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()63 return hmac.compare_digest(expected, parts.get("v1", ""))`;6465export default function WebhooksPage() {66 return (67 <DocPage path="/docs/webhooks" eyebrow="Reliability" title="Webhooks" description="Receive HTTP notifications when something happens in your account: usage thresholds, failed requests, billing and API key events." status="Coming soon">68 <ComingSoon title="Coming soon — configuration UI not yet available">69 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.70 The event names, payload shape and signature scheme may change before launch. Watch the <A href="/changelog">changelog</A>.71 </ComingSoon>7273 <H2>Planned events</H2>74 <Table>75 <THead>76 <Tr>77 <Th>Event</Th>78 <Th>When it fires</Th>79 </Tr>80 </THead>81 <TBody>82 {EVENTS.map(([name, desc]) => (83 <Tr key={name}>84 <Td mono className="whitespace-nowrap">85 {name}86 </Td>87 <Td>{desc}</Td>88 </Tr>89 ))}90 </TBody>91 </Table>9293 <H2>Delivery</H2>94 <Ul>95 <Li>96 Events will be delivered as <Code>POST</Code> requests with a JSON body to the HTTPS URL you register per organization, with the event types you subscribe to.97 </Li>98 <Li>99 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 the100 dashboard.101 </Li>102 <Li>103 Deliveries are <Strong>at least once</Strong>: use <Code>id</Code> to deduplicate.104 </Li>105 <Li>106 Payloads never include page content, cookies or API key secrets, only identifiers and metrics.107 </Li>108 </Ul>109 <CodeBlock lang="json" title="Planned payload" code={JSON.stringify(PAYLOAD, null, 2)} />110111 <H2>Signature</H2>112 <P>113 Each delivery will carry an <Code>X-Fetcha-Signature</Code> header so you can verify that the payload came from Fetcha and was not altered. The scheme is HMAC-SHA256 over a timestamp and the114 raw request body, keyed with the endpoint's secret shown once at creation:115 </P>116 <CodeBlock117 lang="text"118 code={`X-Fetcha-Signature: t=1788623040,v1=5f1c9c0a5b4e7d2b8a3f6e9d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9b119X-Fetcha-Event: usage.threshold120X-Fetcha-Delivery: whd_1a2b3c4d5e6f7g8h121122signed_payload = "<t>" + "." + <raw body>123v1 = hex( HMAC_SHA256( secret, signed_payload ) )`}124 />125 <Ul>126 <Li>127 Compute the HMAC over the <Strong>raw</Strong> bytes you received, before any JSON parsing or re-serialisation.128 </Li>129 <Li>Reject deliveries whose timestamp is more than five minutes old to prevent replay.</Li>130 <Li>Compare signatures with a constant-time function.</Li>131 <Li>132 During secret rotation both the old and the new secret will be valid for 24 hours; the header may then contain two <Code>v1</Code> values.133 </Li>134 </Ul>135 <CodeTabs136 tabs={[137 { label: "JavaScript", lang: "typescript", code: VERIFY_JS },138 { label: "Python", lang: "python", code: VERIFY_PY },139 ]}140 />141142 <Callout variant="info" title="What you can do today">143 Poll <Code>GET /v1/usage</Code> for <Code>remaining_requests</Code> and spend, and watch the request log in the dashboard. Hard spending limits and project request limits already stop144 traffic when reached (see <A href="/docs/rate-limits">Rate limits</A>), which covers the most important protection webhooks would otherwise provide.145 </Callout>146 </DocPage>147 );148}149