Skip to content

webhooks

This page is about managing endpoints. For verifying and handling deliveries, see Receiving webhooks.

Delivery is at-least-once: an endpoint you create here can receive the same event more than once, and your handler must dedupe on event.id. The at-least-once contract explains why and shows the pattern.

const endpoint = await boomin.webhooks.endpoints.create({
url: "https://your-app.com/webhooks/boomin",
description: "Production",
enabledEvents: ["distribution.live", "payout.settled"],
});
console.log(endpoint.id, endpoint.secret);
// we_... whsec_...
MethodRouteScope
endpoints.create(params, options)POST /webhook_endpointswebhooks:write
endpoints.list(params, options)GET /webhook_endpointswebhooks:read
endpoints.retrieve(id, options)GET /webhook_endpoints/{id}webhooks:read
endpoints.update(id, params, options)POST /webhook_endpoints/{id}webhooks:write
endpoints.rotateSecret(id, params, options)POST /webhook_endpoints/{id}/rotate_secretwebhooks:write
endpoints.del(id, options)DELETE /webhook_endpoints/{id}webhooks:write

Endpoints are organization-scoped, not brand-scoped: one endpoint receives every brand’s events in the org, and each event carries its own subject.

On the wire, webhook endpoints are the one resource that is not returned bare — the raw HTTP responses of create, retrieve, update, and rotate_secret answer:

{ "webhook_endpoint": { "id": "we_...", "object": "webhook_endpoint", ... } }

The SDK unwraps that envelope for you — every endpoints.* method resolves to the bare endpoint object. list is a normal { object: "list", data, hasMore } envelope of unwrapped endpoint objects, and del answers a bare { id, object, deleted: true }.

const endpoint = await boomin.webhooks.endpoints.retrieve("we_...");
const { data } = await boomin.webhooks.endpoints.list();

As the SDK returns it (raw HTTP uses snake_case: enabled_events, created_at):

{
"id": "we_...",
"object": "webhook_endpoint",
"url": "https://your-app.com/webhooks/boomin",
"description": "Production",
"enabledEvents": ["distribution.live", "payout.settled"],
"status": "enabled",
"secret": "whsec_...",
"rotatedAt": null,
"livemode": true,
"createdAt": "2026-08-01T00:00:00.000Z",
"updatedAt": "2026-08-01T00:00:00.000Z"
}
await boomin.webhooks.endpoints.create({
url: "https://your-app.com/webhooks/boomin", // required
description: "Production", // optional, ≤ 500 chars
enabledEvents: ["distribution.live"], // optional, ≤ 100 entries
});

Answers 201.

URL rules. https is required. Plain http is accepted only for loopback (localhost, 127.0.0.1, [::1]) so local development works. Anything else is invalid_request (400).

Subscriptions. Every entry in enabledEvents must come from the public event vocabulary; an unknown type is invalid_event_type (400) naming the offenders. An empty or omitted enabledEvents subscribes the endpoint to all public types.

No backfill. A new endpoint receives events appended at or after its creation. To cover the gap, page events.list by seq.

await boomin.webhooks.endpoints.update("we_...", {
enabledEvents: ["distribution.live", "distribution.failed", "payout.settled"],
status: "disabled",
});

Every field is optional; omitted fields are left alone. status accepts enabled or disabled — disabling stops delivery without destroying the endpoint or its secret, which is what you want during an incident.

A disabled endpoint exhausts any in-flight deliveries immediately rather than retrying them.

const endpoint = await boomin.webhooks.endpoints.rotateSecret("we_...");
console.log(endpoint.secret); // the NEW secret, shown once

Installs a fresh whsec_... and keeps the previous secret honored for a 24-hour overlap window. During the window every delivery carries two v1 signature entries — current first, previous second — so a receiver holding either verifies.

Full zero-downtime procedure: Receiving webhooks.

await boomin.webhooks.endpoints.del("we_...");
// { id: "we_...", object: "webhook_endpoint", deleted: true }

Hard deletion — the endpoint is gone, along with its secrets. DELETE honors Idempotency-Key like every other mutation, and the idempotency cache is consulted before the row lookup, so a replayed delete returns the cached response instead of 404ing on the endpoint it already removed.

Prefer update({ status: "disabled" }) when you might want the endpoint back.