Webhooks in, typed events out.
Relay types every payload, retries failed deliveries and replays any event.
curl --json @evt_1042.json https://relay.example.com/in/billing
Free up to 100,000 events a month.
{"…Raw webhookinvoice.paidTyped event503 ┄┄Retry, waiting
One stream for every provider
Every webhook lands here typed, with its delivery state beside it.
Four jobs, one stream
What Relay does between your providers and your endpoint.
Relay reads each payload and gives every field a type.
evt_1042.json
{
"type": "invoice.paid",
"id": "evt_1042",
"amount": 4800,
"currency": "GBP",
"paid_at": "2026-10-08T09:42:16Z"
}
invoice-paid.ts
// inferred from evt_1042
export type InvoicePaid = {
type: "invoice.paid";
id: string;
amount: number;
currency: "GBP";
paid_at: string;
};
When your endpoint is down, Relay holds evt_1042 and tries again: a 503, a timeout, then 200 OK.
- 503Service Unavailable in 2.1 s
Attempt 1, 09:42:18 - 10.0 sNo answer, timed out
Attempt 2, 09:42:48 - 200OK in 182 ms
Attempt 3, 09:43:48
Send any past event to your endpoint again, straight from the stream.
Replay
invoice.paidevt_1042- Event
evt_1042, invoice.paid- Destination
https://example.com/hooks/billing
Sends the stored payload as it arrived
09:58:14Replayedevt_1042200 OK164 ms09:31:02Replayedevt_0987200 OK201 ms08:12:40Replayedevt_0961200 OK176 ms
curl --json @evt_1042.json https://example.com/hooks/billing
One address for each provider. Every webhook lands in one stream, whoever sent it.
| Source | Posts to | Event types seen | Last event |
|---|---|---|---|
| shop.example.com | relay.example.com/in/shop | order.created order.shipped | 09:42:31 |
| billing.example.com | relay.example.com/in/billing | invoice.paid refund.issued | 09:42:18 |
| crm.example.com | relay.example.com/in/crm | customer.updated contact.merged | 09:41:55 |
| forms.example.com | relay.example.com/in/forms | form.submitted | 09:39:40 |
All four deliver to https://example.com/hooks
100,000 events a month, free.
Your handler gets a type
Answer 200 and the event is delivered. Anything else, and Relay tries again.
app/hooks/billing/route.tsYour endpoint
import type { InvoicePaid } from "./invoice-paid";
export async function POST(req: Request) {
const event = (await req.json()) as InvoicePaid;
console.log(event.id, event.amount, event.currency);
return new Response(null, { status: 200 });
}
evt_1042.