space.hooks.destinations

Create and change destinations, the URLs webhooks are sent to, and read their signing secrets.

A destination is a URL that webhooks are sent to, such as your order service. It says how to send them: the method, the authentication, how many a second, minute, or hour it can take, how many times to try each one, and how long to wait for an answer. Connections join sources to destinations.

Each webhook on its way to a destination is a delivery. It carries the body it was received with and the sender’s headers, then the destination’s authentication, then these headers:

HeaderHolds
webhook-idThe event’s ID, the same on every attempt and every time it is sent again
webhook-timestampWhen the attempt was sent, in seconds since 1 January 1970
webhook-signatureThe Standard Webhooks signature, made with the destination’s whsec_ signing secret
X-Hooks-Delivery-IdThe delivery’s ID
X-Hooks-AttemptWhich attempt this is, from 1

Check the signature with space.hooks.verify().

Any 2xx answer delivers the webhook. Anything else, a time-out, or no connection is tried again after 30 seconds, then after twice as long each time, up to an hour between attempts. A Retry-After on a 429 or 503 answer is honoured when it asks for a longer wait, up to an hour. Redirects are not followed. After the last attempt the delivery is failed, and you can send it again.

A destination that fails 3 attempts in a row shows as failing until an attempt succeeds. When one of its deliveries fails for good, everyone who can manage the project’s webhooks gets an email, at most once a day for each destination.

A project holds at most 100 destinations. Every call below can also fail with the codes shared by all of space, such as missing_project, forbidden, timeout, or network_error. See SpaceError.

space.hooks.destinations.create()

Creates a destination and a signing secret for it.

space.hooks.destinations.create(
  destination: DestinationInput,
  options?: {
    signal?: AbortSignal
    timeoutMs?: number
  }
): Promise<Destination>
destination.namestringRequired

1 to 100 characters after trimming, without line breaks or other control characters. Unique in the project.

destination.urlstringRequired

An https URL of at most 2,048 characters, without a # part or a user name and password. Put credentials in auth instead. A URL that leads to a private or reserved network address is refused when we send, and the attempt fails with blocked.

destination.urlSecretboolean

Hides the URL after saving, for a URL that holds a secret of its own. The destination’s url then reads null. false when you leave it out.

destination.methodstring

POST, PUT, or PATCH. Leave it out to send each webhook with the method it was received with.

destination.statestring

enabled or paused. enabled when you leave it out. While a destination is paused, its deliveries wait and are sent once it is enabled again.

destination.authDestinationAuth

How to authenticate to the URL. No authentication when you leave it out. kind picks one of these:

  • { kind: "none" } sends no authentication.
  • { kind: "bearer", token } sends Authorization: Bearer <token>.
  • { kind: "basic", username, password } sends HTTP basic authentication. username is 1 to 1,024 characters without colons. password is at most 1,024 characters.
  • { kind: "header", header, value } sends a header of your own, such as X-Api-Key. header must be a valid header name and cannot be one we set: any X-Hooks- header, the webhook- signing headers, Content-Type, Content-Length, User-Agent, Host, or a header that manages the connection.

token and value are 1 to 4,096 characters. They may hold tabs, but not line breaks or other control characters. None of these can be read back: the destination shows only kind, and header for the header kind.

destination.rateLimit{ count, period } | null

At most count sends each period. count is 1 to 100,000 and period is second, minute, or hour. Sends over the limit wait their turn; none are dropped. No limit when you leave it out or pass null.

destination.maxAttemptsinteger

How many times to try each delivery, 1 to 20. 6 when you leave it out.

destination.timeoutSecondsinteger

How long each attempt waits for an answer, 1 to 60 seconds. 30 when you leave it out.

options.signalAbortSignal

Cancels the call when it aborts. The promise then rejects with aborted.

options.timeoutMsnumber

The longest each request may take, in milliseconds. The client’s timeoutMs when you leave it out, 30,000 by default.

Returns a promise of the new destination. Read its signing secret with signingSecret(). create() is not sent again after a network failure or a timeout, because the destination may already exist.

CodeWhen
validation_failedA field breaks a rule above, or is not listed. issues names each field, such as auth.header
hooks_name_takenAnother destination in the project has that name
hooks_limit_reachedThe project already holds 100 destinations
create-destination.ts
import { space } from "digitospace"

const destination = await space.hooks.destinations.create({
  name: "Orders, Dar es Salaam",
  url: "https://orders.example.co.tz/webhooks/payments",
  method: "POST",
  auth: { kind: "bearer", token: process.env.ORDERS_TOKEN ?? "" },
  rateLimit: { count: 10, period: "second" },
  maxAttempts: 10,
  timeoutSeconds: 15,
})

console.log(destination.id, destination.auth.kind)

space.hooks.destinations.get()

Reads one destination.

space.hooks.destinations.get(
  destination: string,
  options?: {
    signal?: AbortSignal
    timeoutMs?: number
  }
): Promise<Destination>
destinationstringRequired

The destination’s ID, 26 lowercase letters and digits.

options.signalAbortSignal

Cancels the call when it aborts.

options.timeoutMsnumber

The longest each request may take, in milliseconds.

Returns a promise of the destination.

CodeWhen
validation_failedThe ID is not 26 lowercase letters and digits. Nothing is sent
not_foundThe project has no destination with that ID
get-destination.ts
import { space } from "digitospace"

const destination = await space.hooks.destinations.get("01m3m5gpbttxs7xnhk0jgyt4wa")

if (destination.failing) {
  console.log(`Failing since ${destination.failingSince}`)
}

space.hooks.destinations.list()

Lists the project’s destinations, newest first.

space.hooks.destinations.list(options?: {
  signal?: AbortSignal
  timeoutMs?: number
}): Paged<Destination>
options.signalAbortSignal

Cancels the call when it aborts.

options.timeoutMsnumber

The longest each request may take, in milliseconds.

Returns a Paged<Destination> at once, without a request. It reads the destinations 200 at a time as you go: loop over it with for await, collect every destination with .all(), or read one page with .page(cursor), which resolves to { items, nextCursor }.

failing-destinations.ts
import { space } from "digitospace"

const destinations = await space.hooks.destinations.list().all()

for (const destination of destinations.filter((found) => found.failing)) {
  console.log(destination.name, destination.failuresInRow)
}

space.hooks.destinations.update()

Changes some of a destination’s fields and keeps the rest. It reads the destination first, then saves it whole with your changes, so two updates at the same time can undo each other.

space.hooks.destinations.update(
  destination: string,
  change: Partial<DestinationInput>,
  options?: {
    signal?: AbortSignal
    timeoutMs?: number
  }
): Promise<Destination>
destinationstringRequired

The destination’s ID.

changePartial<DestinationInput>Required

The fields to change, with the same rules as create(). A field you leave out keeps its value:

  • A hidden URL stays hidden and keeps working unless you pass a new url or urlSecret: false. To show a hidden URL again, pass urlSecret: false with the url.
  • The saved authentication is kept unless you pass auth. Pass { kind: "none" } to remove it.
  • rateLimit: null removes the rate limit.
options.signalAbortSignal

Cancels the call when it aborts.

options.timeoutMsnumber

The longest each request may take, in milliseconds.

Returns a promise of the changed destination.

CodeWhen
validation_failedA field breaks a rule. issues names each field
not_foundThe project has no destination with that ID
hooks_name_takenAnother destination in the project has that name
update-destination.ts
import { space } from "digitospace"

await space.hooks.destinations.update("01m3m5gpbttxs7xnhk0jgyt4wa", {
  timeoutSeconds: 30,
  rateLimit: { count: 300, period: "minute" },
})

space.hooks.destinations.pause()

Pauses a destination. New deliveries are still made for it, and every delivery waits until you resume it. Events are kept for 30 days, so a delivery still waiting after that is deleted with its event.

space.hooks.destinations.pause(
  destination: string,
  options?: {
    signal?: AbortSignal
    timeoutMs?: number
  }
): Promise<Destination>
destinationstringRequired

The destination’s ID.

options.signalAbortSignal

Cancels the call when it aborts.

options.timeoutMsnumber

The longest each request may take, in milliseconds.

The same as update(destination, { state: "paused" }), with the same errors. Returns a promise of the paused destination.

pause-destination.ts
import { space } from "digitospace"

const destination = await space.hooks.destinations.pause("01m3m5gpbttxs7xnhk0jgyt4wa")

console.log(destination.state)

space.hooks.destinations.resume()

Enables a paused destination. Its waiting deliveries are sent, within its rate limit.

space.hooks.destinations.resume(
  destination: string,
  options?: {
    signal?: AbortSignal
    timeoutMs?: number
  }
): Promise<Destination>
destinationstringRequired

The destination’s ID.

options.signalAbortSignal

Cancels the call when it aborts.

options.timeoutMsnumber

The longest each request may take, in milliseconds.

The same as update(destination, { state: "enabled" }), with the same errors. Returns a promise of the enabled destination.

resume-destination.ts
import { space } from "digitospace"

await space.hooks.destinations.resume("01m3m5gpbttxs7xnhk0jgyt4wa")

space.hooks.destinations.signingSecret()

Reads the secret the destination’s deliveries are signed with. It starts with whsec_. Keep it on the server that receives the deliveries, and pass it to space.hooks.verify().

space.hooks.destinations.signingSecret(
  destination: string,
  options?: {
    signal?: AbortSignal
    timeoutMs?: number
  }
): Promise<string>
destinationstringRequired

The destination’s ID.

options.signalAbortSignal

Cancels the call when it aborts.

options.timeoutMsnumber

The longest each request may take, in milliseconds.

Returns a promise of the secret.

CodeWhen
validation_failedThe ID is not 26 lowercase letters and digits. Nothing is sent
not_foundThe project has no destination with that ID
forbiddenThe API key may not change the project’s webhooks, which reading a secret needs
signing-secret.ts
import { space } from "digitospace"

const secret = await space.hooks.destinations.signingSecret("01m3m5gpbttxs7xnhk0jgyt4wa")

console.log(`Set WEBHOOK_SECRET to ${secret} on the orders server`)

space.hooks.destinations.resetSigningSecret()

Makes a new signing secret and returns it. Deliveries are signed with the new secret from the next attempt on, and the old one stops matching at once, so update the receiving server straight away.

space.hooks.destinations.resetSigningSecret(
  destination: string,
  options?: {
    signal?: AbortSignal
    timeoutMs?: number
  }
): Promise<string>
destinationstringRequired

The destination’s ID.

options.signalAbortSignal

Cancels the call when it aborts.

options.timeoutMsnumber

The longest each request may take, in milliseconds.

Returns a promise of the new secret. It is not sent again after a network failure or a timeout, because the secret may already have changed. Read it with signingSecret() if the call fails that way.

CodeWhen
validation_failedThe ID is not 26 lowercase letters and digits. Nothing is sent
not_foundThe project has no destination with that ID
reset-signing-secret.ts
import { space } from "digitospace"

const secret = await space.hooks.destinations.resetSigningSecret(
  "01m3m5gpbttxs7xnhk0jgyt4wa"
)

console.log(secret.startsWith("whsec_"))

space.hooks.destinations.delete()

Deletes a destination, with its connections and every delivery to it. Deliveries still waiting are not sent. This cannot be undone.

space.hooks.destinations.delete(
  destination: string,
  options?: {
    signal?: AbortSignal
    timeoutMs?: number
  }
): Promise<void>
destinationstringRequired

The destination’s ID.

options.signalAbortSignal

Cancels the call when it aborts.

options.timeoutMsnumber

The longest each request may take, in milliseconds.

Returns a promise that resolves when the destination is gone.

CodeWhen
validation_failedThe ID is not 26 lowercase letters and digits. Nothing is sent
not_foundThe project has no destination with that ID
delete-destination.ts
import { space } from "digitospace"

await space.hooks.destinations.delete("01m3m5gpbttxs7xnhk0jgyt4wa")

Last updated