Endpoints
Manage the receivers your application sends to — create, update, filter, rotate secrets.
Endpoints are the receiver-of-record for fanout. A send() reaches every endpoint whose types filter matches the event.
Lifecycle
// Create
const ep = await postel.outbound.endpoints.create({
url: "https://customer.example.com/webhooks",
types: ["order.*", "shipment.delivered"],
tenantId: "cust_42",
metadata: { displayName: "ACME Co — Production" },
});
// Update
await postel.outbound.endpoints.update(ep.id, {
types: ["order.*"],
});
// Disable (delivery stops; row remains for audit)
await postel.outbound.endpoints.disable(ep.id);
// Delete (delivery stops; row removed unless purgeAttempts: false)
await postel.outbound.endpoints.delete(ep.id, { purgeAttempts: true });
// List / get — list returns a bounded page, newest-first
const page = await postel.outbound.endpoints.list({ tenantId: "cust_42", limit: 50 });
page.items; // Endpoint[]
page.nextCursor; // an opaque string, or null on the last page
const one = await postel.outbound.endpoints.get(ep.id);list is paginated with the same envelope as every other list read ({ limit?, cursor? } in, { items, nextCursor } out; default limit 100). Walk every page by feeding nextCursor back in as cursor — the same loop shown for tenants.
Every endpoint operation accepts an optional tx so endpoint state changes can join the same DB transaction as your business write.
What reads return
create, update, get, and list return the full endpoint — every serializable field you passed in (types, channels, filter, retryPolicy, plain-record headers, metadata, allowHttp, maxInflight, http, circuitBreaker, autoDisable) plus id, url, state, createdAt, and updatedAt.
Function-shaped options stay off the read shape — they are code rather than data, so they run at dispatch time but can't round-trip through a read, and the shape is identical whichever storage adapter you use:
filterFnandtransformare absent from the returned endpoint.headersgiven as a callback, and aCustom(...)retry policy (whosecomputeis a function), read back asnull.ExponentialBackoffandLinearBackoffpolicies round-trip unchanged.- The returned
httpcarries your stored overrides minus thefetchkey. signingis never echoed back because a strategy can carry key material.
Filtering
The types filter accepts string globs:
| Pattern | Matches |
|---|---|
"order.created" | Exactly order.created |
"order.*" | order.created, order.refunded, order.shipped |
"*.created" | order.created, user.created, … |
"*" | Everything |
For "does this data field equal this value" checks, pass a structural filter — { dataPath, equals }, or an array of clauses ANDed together. dataPath is a dot-separated path into the event's data; unlike a predicate function, this is plain JSON: it round-trips through get/list, survives a restart on real storage, and is safe to expose over the admin HTTP API.
await postel.outbound.endpoints.create({
url: "https://internal.example.com/audit",
types: ["*"],
filter: { dataPath: "plan", equals: "enterprise" },
});
// Multiple clauses are ANDed:
await postel.outbound.endpoints.create({
url: "https://eu-gold.example.com/webhooks",
types: ["*"],
filter: [
{ dataPath: "region", equals: "eu" },
{ dataPath: "tier", equals: "gold" },
],
});For anything a structural clause can't express, drop to filterFn — a predicate (event) => boolean receiving a typed { type, data, channels?, timestamp? } envelope:
await postel.outbound.endpoints.create({
url: "https://internal.example.com/audit",
types: ["*"],
filterFn: (event) =>
event.type === "user.signup" && (event.data as { plan?: string }).plan === "enterprise",
});filterFn is code, not data: it runs at dispatch time, in-process, and — like transform — is held in a process-local registry on non-memory storage, so it does not survive a restart unless you re-register it (via create/update) and does not cross the admin HTTP API. Prefer the structural filter unless you genuinely need arbitrary logic. Both filter and filterFn apply together, ANDed with types/channels: filter is evaluated first, then filterFn.
Transformation (per-endpoint payload shape)
Some endpoints want a subset of the data, or a flattened shape. Pass transform:
await postel.outbound.endpoints.create({
url: "https://legacy-customer.example.com/webhooks",
types: ["order.*"],
transform: (event) => ({
type: event.type,
id: event.data.id,
// Drop everything else.
}),
});The transformed payload is what gets signed and sent. The original payload remains in the outbox row for replay (in its original form).
Signing secrets
Creating an endpoint provisions its initial primary signing secret automatically, from the resolved signing strategy — the per-endpoint signing option, else the outbound signing default, else HMAC (v1):
// HMAC (v1) by default — a symmetric secret is minted and stored.
const hmacEp = await postel.outbound.endpoints.create({
url: "https://customer.example.com/webhooks",
types: ["order.*"],
});
// Ed25519 (v1a) — a keypair is minted and the public key is published via
// JWKS immediately, with no rotation required.
const edEp = await postel.outbound.endpoints.create({
url: "https://partner.example.com/webhooks",
types: ["order.*"],
signing: Ed25519V1a(),
});
const jwks = await postel.outbound.keys.publicJwks(); // already includes edEp's public keyIf you manage signing material yourself — a KMS-backed flow, or your own secret store — pass provisionSecret: false so create writes no secret:
await postel.outbound.endpoints.create({
url: "https://customer.example.com/webhooks",
signing: Ed25519V1a(),
provisionSecret: false, // you provision / manage the secret out of band
});Rotation
Rotate the signing secret with rotateSecret:
await postel.outbound.endpoints.rotateSecret(ep.id, {
keepPreviousFor: "7d", // overlap window
});During the overlap, both the previous and current secrets are valid signers. After keepPreviousFor elapses, the previous secret is purged. The mirror image of the receiver-side multi-secret window.
Headers
Static or dynamic headers on outgoing requests:
await postel.outbound.endpoints.create({
url: "https://customer.example.com/webhooks",
types: ["*"],
headers: {
"x-tenant-id": "cust_42",
"x-environment": config.environment,
},
});
// Or dynamic:
await postel.outbound.endpoints.create({
url: "...",
types: ["*"],
headers: ({ message }) => ({
"x-correlation-id": (message as { correlationId: string }).correlationId,
}),
});Per-endpoint overrides
The full set of outbound config defaults — retryPolicy, circuitBreaker, autoDisable, http — can be overridden per endpoint. See Retries & backoff.
Read next
send()and the outbox — howsendchooses which endpoints to enqueue.- Replay — re-emitting historical messages to a specific endpoint.