---
title: "Endpoints"
description: "`webhooks.list`, `get`, `create`, `update`, `delete`, `rotateSecret`, `test` and `listDeliveries`."
url: "https://openemail.uk/docs/sdk/webhooks/endpoints"
area: "SDK"
category: "Webhooks"
---

# Endpoints

`webhooks.list`, `get`, `create`, `update`, `delete`, `rotateSecret`, `test` and `listDeliveries`.

## Every method

**usage.ts**

```
const endpoint = await openemail.webhooks.create({
  url: 'https://acme.com/hooks/mail',
  eventTypes: ['email.sent', 'email.bounced'],
  description: 'Billing service',
})

await store(endpoint.secret)

await openemail.webhooks.list()
await openemail.webhooks.get(endpoint.id)
await openemail.webhooks.update(endpoint.id, { enabled: false })
await openemail.webhooks.test(endpoint.id)
const rotated = await openemail.webhooks.rotateSecret(endpoint.id)
await openemail.webhooks.delete(endpoint.id)
```

`create` is the ONLY time the secret is returned, apart from `rotateSecret`. A read never echoes it, so store it before doing anything else. Omit `eventTypes` to receive every event, later ones included.

> `rotateSecret` has no overlap window. The old secret stops working immediately, so deploy the new one before you rotate. It is never retried automatically: a retry would rotate a second time and invalidate the secret the first attempt returned.

## What you can subscribe to

`WEBHOOK_EVENTS` is exported so you can render the list. Events are events of the **mailbox**, not of this API: `email.received` fires for mail that arrives in the app, and `email.sent` fires for a message the composer sent. Subscribing is not the same as watching your own API traffic.

## Proving it works

**webhook-test.ts**

```
const result = await openemail.webhooks.test('whe_…')
console.log(result.delivery?.status, result.delivery?.responseCode)

const deliveries = await openemail.webhooks.listDeliveries('whe_…')
for (const d of deliveries) console.log(d.eventType, d.status, d.responseCode, d.error)
```

> A `responseCode` of `null` means there was no response at all (DNS, TLS, a timeout), which is a different fact from a response that said 0. Each row carries `attempt` and `maxAttempts`, so several rows can describe one event: the same `payload.id` across them is the event, and the attempt number is the try.

## Parameters: webhooks.create

- `url` (string, required): Where deliveries are POSTed. HTTPS only, and the host may not be `localhost`, a `.localhost`/`.local`/`.internal` name, or a loopback, private, CGNAT or link-local IP literal. This is a server-side fetch to an address you supply, so those are a 422 on `url`; the check reads the hostname as written and never resolves DNS. What is stored is the URL parser's serialisation of what you sent, so `https://acme.com` reads back as `https://acme.com/`.
- `eventTypes` (WebhookEvent[]): Which events reach this endpoint: any of the names in `WEBHOOK_EVENTS`. `POST /webhooks` caps the array at the number of events that exist, so one more than that is a 422 on `eventTypes`; `PATCH` does not cap it. Only the length is capped, and a repeated name is stored and read back exactly as you sent it. Omitted or empty is stored as an empty list, which is why it reads back as `['*']`, and it means every `email.*` event except `email.replied`, fourteen today, and never the domain or suppression families. A family added later never reaches an endpoint that did not name it, so an integration cannot start receiving a shape it has never seen because of a release.
- `description` (string): A label for the endpoint, at most 200 characters, so a list of webhooks reads as names rather than a column of URLs. Omitted, it is stored and returned as null.

## Response: CreatedWebhookResource

- `object` ('webhook'): Always `'webhook'`, the same discriminator a plain read returns, because the secret is one extra key on the ordinary shape rather than an object type of its own. Whether `secret` is present is decided by which method you called, not by this field.
- `id` (string): The endpoint's identifier: `whe_` followed by 24 hex characters. Every other webhook call takes it: `get`, `update`, `delete`, `rotateSecret`, `test` and `listDeliveries`.
- `url` (string): The endpoint as stored, having passed the HTTPS and blocked-host checks. It is the parsed URL re-serialised, so compare against this value rather than against the string you sent.
- `description` (string | null): The label you gave it, or null if you gave none. An `update` that sends an explicit null clears it back to null.
- `eventTypes` (WebhookEvent[] | ['*']): The subscribed events, or `['*']` when the endpoint named none. `['*']` is how an empty stored list is rendered on read and cannot be sent back, and it stands for the thirteen message events rather than the whole catalogue. `create` and `update` accept only the literal event names.
- `enabled` (boolean): Whether deliveries are attempted; a disabled endpoint is skipped when events are dispatched and keeps its secret and its delivery history. Always true here, since `WebhookCreate` has no `enabled` and only `WebhookPatch` does.
- `lastDeliveryAt` (string | null): ISO 8601 timestamp of the last delivery ATTEMPT, not the last success. It is stamped after a failed POST too, so it tells you the endpoint was tried and `listDeliveries` tells you how it went. Null until the first attempt, and so always null on `create`.
- `createdAt` (string): ISO 8601 timestamp of when the endpoint was registered. `list` returns endpoints newest first by this field.
- `secret` (string): The HMAC-SHA-256 key that signs each delivery's `X-OpenEmail-Signature`: `whsec_` followed by 32 random bytes in base64url, and what you hand to `verifyWebhookSignature`. Returned by `create` and `rotateSecret` and by nothing else. A read never echoes it, so store it now; a lost secret can only be replaced with `rotateSecret`, which invalidates the old one immediately.
