@farthershore/backend/webhooks exports
Every public export and declaration from @farthershore/backend/webhooks.
Every public export and declaration from @farthershore/backend/webhooks.
Import from @farthershore/backend/webhooks. This reference is extracted from the published declaration surface for version 0.21.2. Read the collection's guides for workflows, prerequisites and failure handling.
Public export createWebhookHandler.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L237.
export declare function createWebhookHandler(options: CreateWebhookHandlerOptions): WebhookHandler;
Public export CreateWebhookHandlerOptions.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L116.
export interface CreateWebhookHandlerOptions {
/** The endpoint's signing secret (`fswh_…`). */
secret?: string;
/** Several accepted secrets (current first) — use while rolling a rotation. */
secrets?: readonly string[];
/** Typed per-event handlers. Missing types are acknowledged and ignored. */
on: WebhookHandlers;
/**
* Called for an event `type` this SDK does not know (a newer platform).
* Always acknowledged 2xx; use this to log or forward.
*/
onUnknown?: (event: {
id: string;
type: string;
body: unknown;
}) => void | Promise<void>;
/** Called for every rejected delivery (never throws the request). */
onRejected?: (rejection: WebhookRejection) => void;
/** Called when a duplicate `webhook-id` is acknowledged without running. */
onDuplicate?: (event: {
id: string;
type: string;
}) => void;
/**
* Delivery-id store for deduplication. Defaults to a bounded in-memory
* cache (single instance, `WEBHOOK_DEDUPE_TTL_MS`). Provide a shared store
* (Redis, DB) with an atomic `claim`/`settle` lease when the receiver runs on
* more than one instance (see {@link WebhookNonceStore}).
*/
nonceStore?: WebhookNonceStore;
/**
* Largest request body the adapters will buffer (default 1 MiB). Checked
* against `Content-Length` and enforced on the stream BEFORE the signature
* is verified, so an unauthenticated caller cannot exhaust memory.
*/
maxBodyBytes?: number;
/** Override for tests; defaults to `Date.now`. */
now?: () => number;
}
Runtime mirror of the typed keys in `WebhookEventData` (types.ts). Kept
SDK-local (no zod in the published bundle) and pinned field-by-field to the
contracts schemas (`WEBHOOK_EVENT_DATA_SCHEMAS`) by `contracts-drift.test.ts`
— a wrong `kind` here (e.g. dropping the `|null` from `amount`) would reject
a legitimately-signed delivery, so the pin proves each kind matches the zod
field's null/type behavior. Exported solely so that pin can read it.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L247.
declare const DATA_FIELD_TYPES: Record<Exclude<WebhookEnvelopeType, "webhook.test">, Record<string, DataFieldKind>>;
Public export DataFieldKind.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L238.
export type DataFieldKind = "string" | "number" | "string|null" | "number|null";
Public export isWebhookEnvelopeType.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L9.
export declare function isWebhookEnvelopeType(value: unknown): value is WebhookEnvelopeType;
Default single-instance delivery-id store: an atomic claim/settle lease,
TTL-bounded, capacity-bounded, and honest about saturation (returns
`"saturated"` rather than lying when it cannot lease a fresh id).
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L94.
export declare class MemoryWebhookNonceStore implements WebhookNonceStore {
private readonly options;
private readonly seen;
private tokenSeq;
constructor(options?: {
ttlMs?: number;
leaseMs?: number;
maxEntries?: number;
now?: () => number;
});
private horizonFor;
claim(id: string): WebhookClaim;
settle(id: string, token: string, outcome: "processed" | "release"): void;
get size(): number;
}
Read a Fetch request body without ever buffering more than `limit` bytes:
a declared `Content-Length` over the cap is refused before any allocation,
and a stream that exceeds it mid-flight is abandoned. Returns `null` when
the cap is exceeded.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L236.
export declare function readBoundedRequestBody(request: Request, limit: number): Promise<Uint8Array | null>;
Verify one delivery's Standard Webhooks signature + timestamp window. The
low-level primitive; `createWebhookHandler` adds dedupe, parsing and routing.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L8.
export declare function verifyWebhook(input: VerifyWebhookInput): WebhookVerifyResult;
Public export VerifyWebhookInput.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L96.
export interface VerifyWebhookInput {
/** Raw request body, exactly as received. */
body: string | Uint8Array;
/** Request headers (any case). */
headers: Record<string, string | string[] | undefined> | Headers;
/**
* Accepted signing secrets. Pass `[current, previous]` while you roll a
* rotated secret on your side; the platform already dual-signs for 24 h.
*/
secrets: readonly string[];
/** Override for tests; defaults to `Date.now()`. */
now?: () => number;
/** Override for tests; defaults to ±300 s. */
toleranceSeconds?: number;
}
Capacity of the default in-memory store (ids retained per TTL window).
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L81.
declare const WEBHOOK_DEDUPE_MAX_ENTRIES = 250000;
How long a delivery id is remembered. The platform retries a delivery at
30 s, 5 min and 30 min after the first attempt (≈ 35.5 min horizon), so the
window must outlast the LAST retry plus clock tolerance — a shorter window
would let the final retry re-run a handler whose 2xx the platform never saw.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L115.
declare const WEBHOOK_DEDUPE_TTL_MS: number;
Public export WEBHOOK_EVENT_HEADER.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L82.
declare const WEBHOOK_EVENT_HEADER = "x-fs-webhook-event";
Event names a webhook endpoint can subscribe to (the platform events
catalog's webhook-deliverable subset), in catalog order.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L5.
declare const WEBHOOK_EVENT_NAMES: readonly ["subscription.created", "subscription.updated", "subscription.canceled", "payment.succeeded", "payment.failed", "entitlement.changed", "usage.threshold_reached"];
Header names on the wire (Standard Webhooks + our convenience header).
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L79.
declare const WEBHOOK_ID_HEADER = "webhook-id";
How long an in-flight lease is held before a crashed / hung handler's id
becomes reclaimable. Longer than any reasonable handler, shorter than the
retry horizon, so a genuinely stuck delivery is retried rather than wedged
as a permanent "in_flight".
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L88.
declare const WEBHOOK_INFLIGHT_LEASE_MS: number;
Default cap on a delivery body. Platform envelopes are a few KiB.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L156.
declare const WEBHOOK_MAX_BODY_BYTES: number;
Prefix every platform-minted signing secret carries.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L86.
declare const WEBHOOK_SECRET_PREFIX = "fswh_";
Public export WEBHOOK_SIGNATURE_HEADER.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L81.
declare const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
The synthetic event a dashboard / CLI "Send test" produces.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L8.
declare const WEBHOOK_TEST_EVENT: "webhook.test";
Public export WEBHOOK_TIMESTAMP_HEADER.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L80.
declare const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
Accept deliveries within ±this many seconds of the receiver's clock.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L84.
declare const WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 300;
The result of `claim`. A `token` is present ONLY for a "fresh" claim — it
identifies THIS lease so `settle` cannot act on a lease that has since been
reclaimed by another delivery (see {@link WebhookNonceStore}).
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L38.
export interface WebhookClaim {
outcome: WebhookClaimOutcome;
token?: string;
}
Delivery-id store for deduplication. Unlike the request verifier's
`NonceStore`, a webhook store leases an id: `claim` before the handler and
`settle` after, so a thrown handler releases the id and the platform's retry
(same `webhook-id`) re-runs it instead of being acknowledged as a duplicate,
while a concurrent retry of an in-flight id never double-runs the handler.
See {@link WebhookNonceStore}.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L34.
export type WebhookClaimOutcome = "fresh" | "in_flight" | "processed" | "saturated";
The JSON body of every delivery. `id` equals the `webhook-id` header.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L68.
export type WebhookEnvelope<T extends WebhookEnvelopeType = WebhookEnvelopeType> = T extends WebhookEnvelopeType ? {
id: string;
type: T;
/** ISO 8601 — when the event was recorded (stable across retries). */
createdAt: string;
businessId: string;
/** `null` = production. */
environmentId: string | null;
data: WebhookEventData[T];
} : never;
Every `type` a delivery envelope can carry.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L10.
export type WebhookEnvelopeType = WebhookEventName | typeof WEBHOOK_TEST_EVENT;
Per-event `data`. The keys listed are what the platform's producers carry
today; extra keys pass through untouched (automation rules may attach
builder-authored fields), so every shape is open.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L16.
export interface WebhookEventData {
"subscription.created": {
subscriptionId?: string;
compiledPlanId?: string;
[key: string]: unknown;
};
"subscription.updated": {
subscriptionId?: string;
/** Producer-specific change reason, e.g. `plan_changed`, `trial_ending`. */
reason?: string;
lifecycle?: string;
compiledPlanId?: string;
[key: string]: unknown;
};
"subscription.canceled": {
subscriptionId?: string;
reason?: string;
lifecycle?: string;
[key: string]: unknown;
};
"payment.succeeded": WebhookPaymentData__dc4de7029bac;
"payment.failed": WebhookPaymentData__dc4de7029bac;
"entitlement.changed": {
compiledPlanId?: string;
lineageId?: string;
status?: string;
[key: string]: unknown;
};
"usage.threshold_reached": {
subscriptionId?: string;
subscriberId?: string;
limitId?: string;
threshold?: number;
windowStartMs?: number;
windowEndMs?: number;
[key: string]: unknown;
};
"webhook.test": {
businessId: string;
sentAt: string;
[key: string]: unknown;
};
}
Public export WebhookEventHandler.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L10.
export type WebhookEventHandler<T extends WebhookEnvelopeType> = (event: WebhookEnvelope<T>) => void | Promise<void>;
Public export WebhookEventName.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L6.
export type WebhookEventName = (typeof WEBHOOK_EVENT_NAMES)[number];
Minimal Express-shaped types so we don't hard-depend on
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L208.
export type WebhookExpressRequestLike = {
headers: Record<string, string | string[] | undefined>;
/** Raw body bytes when captured by a raw parser (`express.raw()`). */
body?: unknown;
rawBody?: Buffer | Uint8Array;
};
Public export WebhookExpressResponseLike.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L214.
export type WebhookExpressResponseLike = {
status(code: number): WebhookExpressResponseLike;
json(body: unknown): unknown;
};
Public export WebhookHandleInput.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L202.
export interface WebhookHandleInput {
/** Raw request body, exactly as received. */
body: string | Uint8Array;
headers: Record<string, string | string[] | undefined> | Headers;
}
Public export WebhookHandleOutcome.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L157.
export type WebhookHandleOutcome = {
status: 200;
kind: "handled";
id: string;
type: WebhookEnvelopeType;
} | {
status: 200;
kind: "ignored";
id: string;
type: WebhookEnvelopeType;
} | {
status: 200;
kind: "unknown_type";
id: string;
type: string;
} | {
status: 200;
kind: "duplicate";
id: string;
type: string;
} | {
status: 400 | 401;
kind: "rejected";
rejection: WebhookRejection;
} | {
status: 413;
kind: "rejected";
rejection: WebhookRejection;
} | {
status: 503;
kind: "store_unavailable";
id: string;
type: string;
} | {
status: 503;
kind: "in_flight";
id: string;
type: string;
} | {
status: 500;
kind: "handler_error";
id: string;
type: string;
error: unknown;
};
Public export WebhookHandler.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L218.
export interface WebhookHandler {
/** Framework-neutral: verify, dedupe, parse, route. Never throws. */
handle(input: WebhookHandleInput): Promise<WebhookHandleOutcome>;
/** Fetch-API handler (Next.js route handlers, Hono, Bun, Workers…). */
fetch(request: Request): Promise<Response>;
/**
* Express handler. Mount AFTER a raw-body parser
* (`express.raw({ type: "application/json" })`) or set `req.rawBody`; a pre-parsed
* JSON body cannot be verified and is rejected 400.
*/
express(): (req: WebhookExpressRequestLike, res: WebhookExpressResponseLike) => Promise<void>;
}
Public export WebhookHandlers.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L11.
export type WebhookHandlers = {
[T in WebhookEnvelopeType]?: WebhookEventHandler<T>;
};
Delivery-id store protocol — an ATOMIC, OWNERSHIP-FENCED claim/settle lease.
The earlier split `has(id)` … handler … `remember(id)` had a check-then-act
race: two concurrent deliveries of the SAME `webhook-id` could BOTH observe
`has(id) === false` and both run a payment / lifecycle handler. An atomic
claim closes that window.
`claim(id)` is taken BEFORE the handler:
- "fresh" → unseen; the caller HOLDS the lease (identified by the
returned `token`) and must settle() with that token.
- "in_flight" → another delivery of this id is mid-handler; the caller
answers 503 and NEVER runs the handler. The platform
retries; that retry sees "processed" once the first wins.
- "processed" → already handled to success; answer 200 without re-running.
- "saturated" → the store cannot retain the id; the caller runs the
handler best-effort WITHOUT a lease (at-least-once holds).
`settle(id, token, outcome)` resolves a "fresh" claim, and is a NO-OP unless
`token` still identifies the live lease:
- "processed" → keep the id so future retries dedupe.
- "release" → drop the lease so the platform's retry re-runs the handler.
The token fences the case where a handler outlives the in-flight lease TTL:
the id becomes reclaimable, a retry re-leases it with a NEW token, and the
original handler's late `settle` (with the OLD token) is ignored rather than
deleting or overwriting the replacement's lease. (A handler that exceeds the
lease TTL still degrades to at-least-once — the receiver dedupes on
webhook-id — but never corrupts another delivery's lease.)
A shared store (Redis, DB) implements `claim` as an atomic upsert stamping a
unique token, a short in-flight lease TTL (so a crashed handler's id becomes
reclaimable) and a longer processed TTL of at least `WEBHOOK_DEDUPE_TTL_MS`.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L76.
export interface WebhookNonceStore {
claim(id: string): WebhookClaim | Promise<WebhookClaim>;
settle(id: string, token: string, outcome: "processed" | "release"): void | Promise<void>;
}
Why a delivery was rejected (401 / 400) — surfaced through `onRejected`.
Declaration source: packages/backend/dist/types/webhooks/index.d.ts#L15.
export type WebhookRejection = {
reason: WebhookVerifyFailure;
} | {
reason: "invalid_json";
} | {
reason: "invalid_envelope";
detail: string;
} | {
reason: "payload_too_large";
limitBytes: number;
};
Public export WebhookVerifyFailure.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L87.
export type WebhookVerifyFailure = "missing_headers" | "invalid_timestamp" | "timestamp_out_of_tolerance" | "no_matching_signature";
Public export WebhookVerifyResult.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L88.
export type WebhookVerifyResult = {
ok: true;
id: string;
timestamp: number;
} | {
ok: false;
reason: WebhookVerifyFailure;
};
These declarations explain types reachable from the public exports above. They are source evidence, not supported package imports. Suffixed names are documentation identifiers that preserve distinct lexical bindings. Standard-library and third-party types (for example Promise, React and Zod) remain external boundaries; their implementation declarations are not expanded here.
Declaration source: packages/backend/dist/types/webhooks/types.d.ts#L59.
export interface WebhookPaymentData__dc4de7029bac {
subscriptionId?: string;
invoiceId?: string;
amount?: number | null;
currency?: string | null;
reason?: string;
[key: string]: unknown;
}