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
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ works when one of these is set:
| --- | --- |
| `RELAY_API_KEY` | Account API key (`sk-...`) |
| `RELAY_KEY` + `RELAY_SECRET` | Classic access token pair |
| `RELAY_BASE_URL` | API address, when not `https://my.webhookrelay.com` (self-hosted or local development) |

Other options: `baseUrl`, `timeoutMs`, `fetch`, `headers`, `userAgent`.

Expand Down Expand Up @@ -237,8 +238,22 @@ const message = await relay.outbound.messages.publish(
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).
background and return a recovery task (`outbound.recoveryTasks.get`).

Watch endpoint health across all your customers. Endpoints carry `stats`
(attempts and failures over the last 24 hours) when listed or read:

```ts
const { endpoints, stats } = await relay.outbound.health();
// endpoints: { active, failing, paused, disabled } counts; stats: { attempts, failures }

// Failing endpoints come longest failing first. Filter by `consumer` too;
// `limit` is 1-100 (default 50), page with `offset`.
const failing = await relay.outbound.endpoints.listAll({ state: "failing", limit: 20 });
for (const e of failing) console.log(e.consumer, e.url, e.failing_since, e.stats?.failures);
```

A runnable version lives in [`examples/outbound.ts`](./examples/outbound.ts).

## Receiving webhooks

Expand Down
14 changes: 14 additions & 0 deletions examples/outbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ export function publishInvoicePaid(relay: WebhookRelay) {
}
// dashboard-snippet:end

// Find customers whose endpoints are failing, longest failing first.
export async function reportFailingEndpoints(relay: WebhookRelay) {
const health = await relay.outbound.health();
if (health.endpoints.failing === 0) return [];
const failing = await relay.outbound.endpoints.listAll({ state: "failing", limit: 20 });
return failing.map((e) => ({
consumer: e.consumer,
url: e.url,
failingSince: e.failing_since,
failures24h: e.stats?.failures ?? 0,
}));
}

async function main() {
const customerURL = process.env.CUSTOMER_WEBHOOK_URL;
if (!customerURL) throw new Error("set CUSTOMER_WEBHOOK_URL");
Expand All @@ -40,6 +53,7 @@ async function main() {
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)");
console.log("failing endpoints:", await reportFailingEndpoints(relay));
}

if (import.meta.filename === process.argv[1]) {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@webhookrelay/sdk",
"version": "0.2.0",
"version": "0.3.0",
"description": "Official TypeScript/JavaScript SDK for the Webhook Relay API — manage buckets, inputs, outputs, service connections and JavaScript transformation functions, poll webhooks and subscribe to them in real time over WebSocket.",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
5 changes: 3 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ function readEnv(name: string): string | undefined {
* 1. `apiKey` (or a `secret` that looks like an `sk-` key) → Bearer auth.
* 2. `key` + `secret` classic access token → HTTP Basic auth.
*
* Environment fallbacks (Node): `RELAY_API_KEY`, then `RELAY_KEY` + `RELAY_SECRET`.
* Environment fallbacks (Node): `RELAY_API_KEY`, then `RELAY_KEY` + `RELAY_SECRET`,
* and `RELAY_BASE_URL` for the API address (self-hosted or local development).
*/
export function resolveConfig(config: WebhookRelayConfig = {}): ResolvedConfig {
const apiKey = config.apiKey ?? readEnv("RELAY_API_KEY");
Expand Down Expand Up @@ -105,7 +106,7 @@ export function resolveConfig(config: WebhookRelayConfig = {}): ResolvedConfig {
);
}

const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
const baseUrl = (config.baseUrl ?? readEnv("RELAY_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, "");

return {
baseUrl,
Expand Down
128 changes: 125 additions & 3 deletions src/generated/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,8 @@ export interface ApiEndpointWithSecret {
rate?: number;
secret?: string;
state?: "active" | "failing" | "paused" | "disabled";
/** Stats are the endpoint's attempts over OutboundStatsWindow; read-only. */
stats?: StructsOutboundEndpointStats;
/** seconds; zero uses OutboundDefaultTimeout */
timeout?: number;
updated_at?: string;
Expand Down Expand Up @@ -907,6 +909,16 @@ export interface ApiOrgSamlSSORequest {
signature_method?: string;
}

export interface ApiOutboundAccess {
enabled?: boolean;
request?: ApiOutboundAccessRequest;
}

export interface ApiOutboundAccessRequest {
requested_at?: string;
ticket_id?: string;
}

export interface ApiOutboundErrorResponse {
error?: string;
}
Expand All @@ -921,6 +933,11 @@ export interface ApiOutboundMessageDetail {
event_type?: string;
id?: string;
payload?: object;
/**
* UnavailableEndpointIDs lists the endpoints whose delivery could not be
* read in time; the message detail is otherwise complete.
*/
unavailable_endpoint_ids?: string[];
}

export interface ApiOutboundRecoveryBody {
Expand Down Expand Up @@ -2464,12 +2481,19 @@ export interface StructsOutboundEndpoint {
previous_secret_expires_at?: string;
rate?: number;
state?: "active" | "failing" | "paused" | "disabled";
/** Stats are the endpoint's attempts over OutboundStatsWindow; read-only. */
stats?: StructsOutboundEndpointStats;
/** seconds; zero uses OutboundDefaultTimeout */
timeout?: number;
updated_at?: string;
url?: string;
}

export interface StructsOutboundEndpointStats {
attempts?: number;
failures?: number;
}

export interface StructsOutboundEventType {
created_at?: string;
deprecated?: boolean;
Expand All @@ -2479,6 +2503,13 @@ export interface StructsOutboundEventType {
updated_at?: string;
}

export interface StructsOutboundHealth {
endpoints?: Record<string, number>;
/** Since is the start of the window the stats cover. */
since?: string;
stats?: StructsOutboundEndpointStats;
}

export interface StructsOutboundMessage {
consumer?: string;
created_at?: string;
Expand Down Expand Up @@ -6873,6 +6904,42 @@ export class Api<
...params,
}),

/**
* @description Outbound webhooks are a pilot. Reports whether they are enabled for the account and, while they are not, the open request to enable them.
*
* @tags outbound
* @name OutboundAccessList
* @summary Get outbound webhooks access
* @request GET:/v1/outbound/access
* @secure
*/
outboundAccessList: (params: RequestParams = {}) =>
this.request<ApiOutboundAccess, ApiOutboundErrorResponse>({
path: `/v1/outbound/access`,
method: "GET",
secure: true,
format: "json",
...params,
}),

/**
* @description Asks support to enable outbound webhooks by opening a support ticket. Asking again while that ticket is open returns it instead of opening another; nothing is opened when outbound webhooks are already enabled.
*
* @tags outbound
* @name OutboundAccessRequestCreate
* @summary Request outbound webhooks
* @request POST:/v1/outbound/access/request
* @secure
*/
outboundAccessRequestCreate: (params: RequestParams = {}) =>
this.request<ApiOutboundAccess, ApiOutboundErrorResponse>({
path: `/v1/outbound/access/request`,
method: "POST",
secure: true,
format: "json",
...params,
}),

/**
* @description Lists the producer's customers that receive outbound webhooks. Pilot: requires the outbound feature flag.
*
Expand All @@ -6892,7 +6959,7 @@ export class Api<
}),

/**
* @description Newest first. Signing secrets are not included.
* @description Newest first, with each endpoint's attempts and failures over the last 24 hours. Signing secrets are not included.
*
* @tags outbound
* @name OutboundConsumersEndpointsList
Expand Down Expand Up @@ -6996,6 +7063,37 @@ export class Api<
...params,
}),

/**
* @description Filter by state to find failing or disabled endpoints among all of the account's consumers. Failing endpoints come longest failing first, others newest first. Each endpoint carries its attempts and failures over the last 24 hours. Signing secrets are not included.
*
* @tags outbound
* @name OutboundEndpointsList
* @summary List outbound endpoints across consumers
* @request GET:/v1/outbound/endpoints
* @secure
*/
outboundEndpointsList: (
query?: {
/** Only endpoints in this state */
state?: "active" | "failing" | "paused" | "disabled";
/** Only this consumer's endpoints */
consumer?: string;
/** Page size, at most 100 */
limit?: number;
/** Page offset */
offset?: number;
},
params: RequestParams = {},
) =>
this.request<StructsOutboundEndpoint[], ApiOutboundErrorResponse>({
path: `/v1/outbound/endpoints`,
method: "GET",
query: query,
secure: true,
format: "json",
...params,
}),

/**
* @description Delivery history is kept.
*
Expand Down Expand Up @@ -7072,6 +7170,12 @@ export class Api<
limit?: number;
/** Page offset */
offset?: number;
/** Only deliveries in this status */
status?: "sent" | "failed" | "stalled" | "received" | "rejected";
/** Only deliveries of this event type */
event_type?: string;
/** Only the delivery of this message */
message_id?: string;
},
params: RequestParams = {},
) =>
Expand Down Expand Up @@ -7169,7 +7273,7 @@ export class Api<
}),

/**
* @description Re-sends message_id to the endpoint in the background and returns the recovery task.
* @description Re-sends message_id to the endpoint in the background and returns the recovery task. A delivery waiting for its next scheduled retry is re-sent now; one whose first attempt is still queued is refused.
*
* @tags outbound
* @name OutboundEndpointsRetryCreate
Expand Down Expand Up @@ -7317,6 +7421,24 @@ export class Api<
...params,
}),

/**
* @description Counts the account's endpoints by state and their delivery attempts and failures over the last 24 hours, to the hour. Attempts are counted within about 15 seconds.
*
* @tags outbound
* @name OutboundHealthList
* @summary Get outbound endpoint health
* @request GET:/v1/outbound/health
* @secure
*/
outboundHealthList: (params: RequestParams = {}) =>
this.request<StructsOutboundHealth, ApiOutboundErrorResponse>({
path: `/v1/outbound/health`,
method: "GET",
secure: true,
format: "json",
...params,
}),

/**
* @description Newest first, without payloads.
*
Expand Down Expand Up @@ -7349,7 +7471,7 @@ export class Api<
}),

/**
* @description Accepts a message for signed, asynchronous delivery to every endpoint of the consumer subscribed to its event type. Reusing an Idempotency-Key with the same message returns the original receipt.
* @description Accepts a message for signed, asynchronous delivery to every endpoint of the consumer subscribed to its event type. An event type not yet in the catalog is added on first publish; deprecated event types are rejected. Reusing an Idempotency-Key with the same message returns the original receipt.
*
* @tags outbound
* @name OutboundMessagesCreate
Expand Down
Loading
Loading