Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ browser.
swagger consumed here. When an endpoint's behavior is unclear, read the Go
handler rather than guessing.
- **Generated low-level client:** [`src/generated/api.ts`](./src/generated/api.ts)
— auto-generated from the swagger by `make openapi` (swagger-typescript-api),
— auto-generated from the swagger by `make openapi` (runs `npm run openapi`,
swagger-typescript-api),
post-processed with `@ts-nocheck`. Exposed to users as
`@webhookrelay/sdk/generated`. Do not hand-edit; it is regenerated.

Expand All @@ -54,13 +55,14 @@ src/
params.ts camelCase → snake_case param normalization.
resources/ One class per resource, hung off the client:
buckets.ts inputs.ts outputs.ts serviceConnections.ts functions.ts webhooks.ts
outbound.ts relay.outbound.* — consumers, event types, endpoints, messages
streaming/
poller.ts WebhookPoller — async-iterable pull delivery over /v1/events.
socket.ts WebhookSubscription — real-time push over /v1/socket.
websocket.ts Isomorphic WebSocket resolver + URL helper.
generated/api.ts Generated low-level client (do not edit).
test/ Vitest suite (mock fetch + fake WebSocket; no network).
examples/ Runnable usage examples.
examples/ Runnable usage examples (outbound.ts is also run by the tests).
swagger/swagger.yaml OpenAPI spec (input to `make openapi`).
scripts/ Build/codegen helpers (postprocess-generated.mjs).
```
Expand Down
8 changes: 1 addition & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@ swagger:
# SDK in src/ is the ergonomic surface; the generated client is the raw
# escape hatch, re-exported from "@webhookrelay/sdk/generated".
openapi: swagger
npx swagger-typescript-api generate \
-p ./swagger/swagger.yaml \
-o ./src/generated \
-n api.ts \
--extract-request-params \
--extract-response-body
node scripts/postprocess-generated.mjs
npm run openapi
@echo "regenerated src/generated/api.ts"

build:
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,37 @@ await relay.serviceConnections.listOutputs(bucket.id);
await relay.serviceConnections.list();
```

## Outbound webhooks — send webhooks to your customers

Register each customer as a consumer with the HTTPS endpoint they give you, then
publish events. Webhook Relay signs every delivery
([Standard Webhooks](https://www.standardwebhooks.com/)), retries failures durably
for up to 48 hours and keeps the delivery history. Outbound webhooks are in pilot:
the account needs the `outbound` feature.

```ts
// Once per customer.
await relay.outbound.eventTypes.upsert("invoice.paid", { description: "An invoice was paid" });
await relay.outbound.consumers.upsert("customer_42", { name: "Acme" });
const endpoint = await relay.outbound.endpoints.create("customer_42", {
url: "https://customer.example/webhooks",
eventTypes: ["invoice.paid"],
});
// endpoint.secret is the signing secret your customer verifies deliveries with.

// Every time the event happens.
const message = await relay.outbound.messages.publish(
{ consumer: "customer_42", eventType: "invoice.paid", payload: { invoice_id: "inv_123", amount: 4900 } },
{ idempotencyKey: "invoice-paid:inv_123" }, // retrying with the same key never publishes twice
);
```

Publishing is asynchronous: the result is the durable acceptance receipt.
`outbound.messages.get(id)` shows each endpoint's delivery and attempts;
`outbound.endpoints.retry`, `.recover` and `.replayMissing` re-send in the
background and return a recovery task (`outbound.recoveryTasks.get`). A runnable
version lives in [`examples/outbound.ts`](./examples/outbound.ts).

## Receiving webhooks

Three delivery modes, from most durable to lowest latency:
Expand Down
50 changes: 50 additions & 0 deletions examples/outbound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Send signed webhooks to your own customers with outbound webhooks.
*
* Run with:
* RELAY_API_KEY=sk-... CUSTOMER_WEBHOOK_URL=https://... npx tsx examples/outbound.ts
*
* test/outbound.test.ts runs these functions, and the Webhook Relay dashboard
* shows the marked region, so keep the two in step.
*/
// dashboard-snippet:start
import { WebhookRelay } from "@webhookrelay/sdk";

// Reads RELAY_API_KEY from the environment. Keep it server-side.
export const createClient = () => new WebhookRelay();

// Once per customer: register the HTTPS endpoint they give you.
export async function onboardCustomer(relay: WebhookRelay, customerURL: string) {
await relay.outbound.eventTypes.upsert("invoice.paid", { description: "An invoice was paid" });
await relay.outbound.consumers.upsert("customer_42", { name: "Acme" });
return relay.outbound.endpoints.create("customer_42", {
url: customerURL,
eventTypes: ["invoice.paid"],
});
}

// Every time the event happens. Retry with the same idempotency key.
export function publishInvoicePaid(relay: WebhookRelay) {
return relay.outbound.messages.publish(
{ consumer: "customer_42", eventType: "invoice.paid", payload: { invoice_id: "inv_123", amount: 4900 } },
{ idempotencyKey: "invoice-paid:inv_123" },
);
}
// dashboard-snippet:end

async function main() {
const customerURL = process.env.CUSTOMER_WEBHOOK_URL;
if (!customerURL) throw new Error("set CUSTOMER_WEBHOOK_URL");
const relay = createClient();
const endpoint = await onboardCustomer(relay, customerURL);
console.log("endpoint:", endpoint.id, "signing secret:", endpoint.secret);
const message = await publishInvoicePaid(relay);
console.log("message:", message.id, "->", message.endpoint_ids.length, "endpoint(s)");
}

if (import.meta.filename === process.argv[1]) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}
4 changes: 4 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { OutputsResource } from "./resources/outputs.js";
import { ServiceConnectionsResource } from "./resources/serviceConnections.js";
import { FunctionsResource } from "./resources/functions.js";
import { WebhooksResource } from "./resources/webhooks.js";
import { OutboundResource } from "./resources/outbound.js";

/**
* The Webhook Relay API client.
Expand Down Expand Up @@ -41,6 +42,8 @@ export class WebhookRelay {
readonly functions: FunctionsResource;
/** Read webhook history, poll, and subscribe over WebSocket. */
readonly webhooks: WebhooksResource;
/** Send signed webhooks to your own customers (pilot). */
readonly outbound: OutboundResource;

constructor(config: WebhookRelayConfig = {}) {
const resolved = resolveConfig(config);
Expand All @@ -51,6 +54,7 @@ export class WebhookRelay {
this.serviceConnections = new ServiceConnectionsResource(this.http);
this.functions = new FunctionsResource(this.http);
this.webhooks = new WebhooksResource(this.http, resolved);
this.outbound = new OutboundResource(this.http);
}

/**
Expand Down
Loading
Loading