From d276b2094050333472db88aae32f3429d81e5e96 Mon Sep 17 00:00:00 2001 From: Karolis Date: Sun, 25 Oct 2026 19:10:15 +0400 Subject: [PATCH] Import an existing signing secret when creating an outbound endpoint endpoints.create accepts an optional secret (whsec_ + base64 of a 24-64 byte key, e.g. exported from Svix), so migrated consumers keep verifying with the key they already have. The API has accepted it since backend PR #1093. --- README.md | 2 ++ src/resources/outbound.ts | 6 ++++-- src/types.ts | 10 ++++++++++ test/outbound.test.ts | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b485229..869b106 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,8 @@ const endpoint = await relay.outbound.endpoints.create("customer_42", { eventTypes: ["invoice.paid"], }); // endpoint.secret is the signing secret your customer verifies deliveries with. +// Migrating from Svix or another Standard Webhooks sender? Pass `secret` with the +// endpoint's existing whsec_ secret so your customer keeps verifying with it. // Every time the event happens. const message = await relay.outbound.messages.publish( diff --git a/src/resources/outbound.ts b/src/resources/outbound.ts index e21bb50..c031a44 100644 --- a/src/resources/outbound.ts +++ b/src/resources/outbound.ts @@ -7,6 +7,7 @@ import type { OutboundConsumer, OutboundDelivery, OutboundEndpoint, + OutboundEndpointCreateParams, OutboundEndpointParams, OutboundEndpointWithSecret, OutboundEventType, @@ -140,7 +141,8 @@ export class OutboundEndpointsResource { /** * Register an HTTPS endpoint for a consumer. The result carries the signing - * secret your customer verifies deliveries with. + * secret your customer verifies deliveries with: a new one, or `secret` when + * importing an existing Standard Webhooks secret (e.g. from Svix). * * ```ts * const endpoint = await relay.outbound.endpoints.create("customer_42", { @@ -149,7 +151,7 @@ export class OutboundEndpointsResource { * }); * ``` */ - create(consumerId: string, params: OutboundEndpointParams): Promise { + create(consumerId: string, params: OutboundEndpointCreateParams): Promise { return this.http.unwrap( this.http.api.v1.outboundConsumersEndpointsCreate( segment(consumerId), diff --git a/src/types.ts b/src/types.ts index 27c579f..b66ed6b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -562,6 +562,16 @@ export interface OutboundEndpointParams { functionId?: string; } +/** Endpoint configuration for `endpoints.create`, with an optional secret to import. */ +export interface OutboundEndpointCreateParams extends OutboundEndpointParams { + /** + * Existing Standard Webhooks signing secret to import (`whsec_` + base64 of a + * 24-64 byte key, e.g. exported from Svix) so the consumer keeps verifying + * with the key it already has. Omit it to generate a new secret. + */ + secret?: string; +} + export interface PublishOutboundMessageParams { consumer: string; event_type?: string; diff --git a/test/outbound.test.ts b/test/outbound.test.ts index 8f999c1..e62fd11 100644 --- a/test/outbound.test.ts +++ b/test/outbound.test.ts @@ -50,6 +50,20 @@ describe("outbound", () => { expect(calls[1]?.body).toEqual({ url: "https://example.com/v2", event_types: ["a"] }); }); + it("imports an existing signing secret on create", async () => { + const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"; + const { relay, calls } = createClient(() => ({ json: { id: "endpoint-1", secret } })); + + const endpoint = await relay.outbound.endpoints.create("customer_42", { + url: "https://example.com/hook", + eventTypes: ["invoice.paid"], + secret, + }); + + expect(endpoint.secret).toBe(secret); + expect(calls[0]?.body).toEqual({ url: "https://example.com/hook", event_types: ["invoice.paid"], secret }); + }); + it("pauses, resumes and handles signing secrets", async () => { const { relay, calls } = createClient((url) => url.includes("/secret/") ? { json: { secret: "whsec_new" } } : { json: { id: "endpoint-1", state: "paused" } },