@philiprehberger/webhook-relay-client
v0.1.0
Published
TypeScript SDK + signature verifier for the Webhook Relay API
Downloads
124
Readme
@philiprehberger/webhook-relay-client
TypeScript SDK + HMAC verifier for the Webhook Relay API. Dependency-free, dual ESM + CJS build, Node 18+.
Installation
npm install @philiprehberger/webhook-relay-clientVerify an incoming webhook (receiver side)
import { verifySignature } from "@philiprehberger/webhook-relay-client";
const ok = verifySignature(
process.env.WEBHOOK_SECRET!, // whsec_...
rawRequestBody, // exact bytes received
request.headers["x-webhook-signature"], // t=...,v1=...
);
if (!ok) {
return new Response("Bad signature", { status: 400 });
}The body must be the exact bytes received. JSON.stringify(JSON.parse(...)) will reorder keys / change whitespace and break the signature.
The signature format matches Stripe and Svix (t=<ts>,v1=<hex> over "{ts}.{body}" with HMAC-SHA256), so the same verifier accepts signatures from any sender using that convention.
Send an event (sender side)
import { WebhookRelayClient } from "@philiprehberger/webhook-relay-client";
const relay = new WebhookRelayClient({
apiKey: process.env.WEBHOOK_RELAY_KEY!, // whk_live_... / whk_test_... / whk_sandbox_...
});
const event = await relay.ingest({
type: "order.created",
payload: { orderId: 42 },
idempotencyKey: "order-42-created",
});
console.log(event.id, event.deliveries_summary);The client throws WebhookRelayError on 4xx/5xx with the RFC 7807 problem payload preserved:
import { WebhookRelayError } from "@philiprehberger/webhook-relay-client";
try {
await relay.ingest({ type: "", payload: {} });
} catch (err) {
if (err instanceof WebhookRelayError) {
console.log(err.status, err.title, err.detail);
}
}Subscriptions, deliveries, and the rest
const sub = await relay.createSubscription({
url: "https://my-app.example.com/webhooks",
eventFilter: "order.*",
name: "orders inbound",
});
console.log(sub.id, sub.signing_secret); // store the secret now — shown only once
const page = await relay.listDeliveries({ status: "failed" });
await relay.pauseSubscription(sub.id);
await relay.resumeSubscription(sub.id);
const rotated = await relay.rotateSubscriptionSecret(sub.id);For endpoints the typed surface doesn't cover (manual retry, dead-letters, webhook test probe, SSE stream) drop down to request<T>():
await relay.request<{ id: string }>("POST", `/v1/deliveries/${id}/retry`);Compatible senders
Use signBody to stand up a compatible sender for testing, or to verify your verifier matches the wire format:
import { signBody } from "@philiprehberger/webhook-relay-client";
const { header } = signBody("whsec_shared", rawBody);
fetch(receiverUrl, {
method: "POST",
headers: { "X-Webhook-Signature": header },
body: rawBody,
});Pointing at a different host
const relay = new WebhookRelayClient({
apiKey,
baseUrl: "https://relay.staging.internal",
});Links
- API: https://api.webhook-relay.dcsuniverse.com
- Docs: https://webhook-relay.dcsuniverse.com
- OpenAPI spec: https://webhook-relay.dcsuniverse.com/openapi.yaml
- Source: https://github.com/philiprehberger/ts-webhook-relay-client
License
MIT
