SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
6.7 KB · 149 lines tsx
Raw Blame History
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&apos;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