---
title: "Endpoints"
description: "`webhooks.list`, `list_all`, `iterate`, `get`, `create`, `update`, `delete`, `rotate_secret`, `test`, `get_delivery` and `replay_delivery`, and the delivery and activity logs."
url: "https://openemail.uk/docs/python/webhooks/endpoints"
area: "Python"
category: "Webhooks"
---

# Endpoints

`webhooks.list`, `list_all`, `iterate`, `get`, `create`, `update`, `delete`, `rotate_secret`, `test`, `get_delivery` and `replay_delivery`, and the delivery and activity logs.

## Every method

**usage.py**

```
from acme.secrets import store

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

store(endpoint['secret'])

client.webhooks.list()
client.webhooks.get(endpoint['id'])
client.webhooks.update(endpoint['id'], {'enabled': False})
client.webhooks.test(endpoint['id'])
latest = client.webhooks.list_deliveries(endpoint['id'], limit=1)['items'][0]
client.webhooks.get_delivery(endpoint['id'], latest['id'])
client.webhooks.replay_delivery(endpoint['id'], latest['id'])
rotated = client.webhooks.rotate_secret(endpoint['id'])
store(rotated['secret'])
client.webhooks.delete(endpoint['id'])
```

`create` is the ONLY time the secret is returned, apart from `rotate_secret`. A read never echoes it, so store it before doing anything else. Omit `eventTypes` for the default set, every `email.*` event except `email.replied`. `email.replied`, `domain.*`, `suppression.*`, `file.*` and `form.*` reach an endpoint only when it names them.

> `rotate_secret` 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.

`file.uploaded` fires when a file is put on the Files page, and `file.deleted` when one is deleted. Their data is `FileEventData`: `fileId`, `filename`, `mimeType`, `sizeBytes`, `direction`, `to`, `threadId`, `messageId`, and `uploadedAt` or `deletedAt`. `to` is the address the file belongs to, or null for a file that belongs to the whole workspace.

> The file events are not in the default set, so an endpoint receives them only when it names them in `eventTypes`. An endpoint limited to some addresses hears only about the files of those addresses, so an upload for the whole workspace, with `to` null, is not sent to it.

`form.submitted` fires when someone signs up through one of your forms, and `form.confirmed` when a pending sign-up joins the audiences, because the person opened the confirmation link or because you approved it. `form.submitted` carries `FormSubmittedEventData`: `formId`, `formName`, `submissionId`, `email`, `status`, `answers`, `audienceIds`, `sourceUrl` and `submittedAt`. `form.confirmed` carries `FormConfirmedEventData`: `formId`, `formName`, `submissionId`, `email`, `audienceIds`, `via`, which is `link` or `approval`, and `confirmedAt`.

> A sign-up on a form without double opt-in sends `form.submitted` with `status` `added` and no `form.confirmed`, so treat that pair as the moment someone joins. Someone who signs up again before confirming keeps the same `submissionId`, and `form.submitted` fires again only when their answers changed. The form events are not in the default set, and an endpoint limited to some addresses never receives them, because sign-ups belong to the whole workspace.

> Each of these data shapes is a `TypedDict` in `openemail.types`. Annotate a verified event as `WebhookPayload[FileEventData]`, for example, and a type checker knows what `event['data']` holds.

## Proving it works

**webhook_test.py**

```
result = client.webhooks.test('whe_…')
delivery = result['delivery']

if delivery is not None:
    print(delivery['status'], delivery['responseCode'])

for d in client.webhooks.iterate_deliveries('whe_…'):
    print(d['eventType'], d['status'], d['responseCode'], d['error'])
```

> A `responseCode` of `None` 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 `eventId` across them is the event, and the attempt number is the try. `nextAttemptAt` says when the automatic retry after a row is due.

## Sending it again

**webhook_replay.py**

```
detail = client.webhooks.get_delivery('whe_…', 'whd_…')
print(detail['payload'], detail['responseBody'], detail['replayRefusal'])

replay = client.webhooks.replay_delivery('whe_…', 'whd_…')
print(replay['delivery']['status'], replay['delivery']['responseCode'])
```

A delivery that keeps failing is tried up to 8 times: as it happens, then after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours, about 27 and a half hours in all. Only a failure worth repeating is repeated: no answer, 408, 425, 429 or a 5xx. A replay sends the stored event again with the same `id`, `type`, `createdAt` and `data`, so a receiver that drops ids it has already handled treats it as the event it knows. Only the signature is new.

- `replay_delivery` sends one event now and returns what your server answered. It works on a delivered attempt too and is never retried. Before it sends, the automatic retries of that event that have not started are paused: they stay cancelled when the replay is delivered, and resume on their schedule when it fails.
- If an automatic retry of the same event is being sent at that moment, `replay_delivery` sends nothing and is refused with 409 `retry_in_progress`, and while another replay of it is still being sent it is refused with 409 `replay_in_progress`, so your receiver never gets two copies at once, even from two replays sent at the same instant. Wait a few seconds and read `get_delivery`, since that retry or replay may deliver it. Replay is one event at a time: no call sends every failed delivery again.
- It also refuses with 409 a switched-off endpoint (`webhook_disabled`), an event the endpoint no longer listens for (`event_not_subscribed`) or no longer covers (`event_out_of_scope`), and an attempt with no stored event (`delivery_not_replayable`). `get_delivery` reports that answer in advance as `replayRefusal`.

> The SDK never retries `replay_delivery` on its own, because a retry after a lost response would send the event again.

> Each refusal raises `OpenEmailApiError` with `status` 409, `is_conflict` true and the reason as `code`, one of the values in `WEBHOOK_REPLAY_ERROR_CODES`.

## Parameters: webhooks.create

- `url` (str, 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` (list[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, suppression or file 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` (str): 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` (Literal['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` (str): The endpoint's identifier: `whe_` followed by 24 hex characters. Every other webhook call takes it: `get`, `update`, `delete`, `rotate_secret`, `test`, `list_deliveries`, `list_all_deliveries`, `iterate_deliveries`, `get_delivery` and `replay_delivery`.
- `url` (str): 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` (str | None): The label you gave it, or null if you gave none. An `update` that sends an explicit null clears it back to null.
- `eventTypes` (list[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 fourteen message events rather than the whole catalogue. `create` and `update` accept only the literal event names.
- `enabled` (bool): 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` (str | None): 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 `list_deliveries` tells you how it went. Null until the first attempt, and so always null on `create`.
- `createdAt` (str): ISO 8601 timestamp of when the endpoint was registered. `list` returns endpoints newest first by this field.
- `secret` (str): 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 `verify_webhook_signature`. Returned by `create` and `rotate_secret` and by nothing else. A read never echoes it, so store it now; a lost secret can only be replaced with `rotate_secret`, which invalidates the old one immediately.

## Filtering the logs

**webhook_logs.py**

```
from datetime import datetime, timedelta, timezone

failed = client.webhooks.list_workspace_deliveries(
    status='failed',
    since=datetime.now(timezone.utc) - timedelta(days=1),
)
print(len(failed['items']))

history = client.webhooks.list_activity('whe_…')
print([(change['type'], change['actor']['label'] if change['actor'] else None) for change in history['items']])
```

`list_deliveries` reads one endpoint and `list_workspace_deliveries` every endpoint, or the ones `endpoint_ids=` names, and both take `status=`, `since=` and `until=`, the filters of the console’s Deliveries tab. `list_activity` and `list_workspace_activity` read the audit log: who created, changed, switched, rotated, tested, replayed or removed what. Each has a `list_all_…` and an `iterate_…` beside it, and every row of the workspace log carries `endpointId`.

> `since=` and `until=` take a `datetime` or an ISO 8601 string. A naive `datetime` is read as local time and converted to UTC, so pass an aware one, as above.

## Reference

- [`webhooks.list()`](https://openemail.uk/docs/python/reference/webhooks#list): full reference
- [`webhooks.list_all()`](https://openemail.uk/docs/python/reference/webhooks#listAll): full reference
- [`webhooks.iterate()`](https://openemail.uk/docs/python/reference/webhooks#iterate): full reference
- [`webhooks.get()`](https://openemail.uk/docs/python/reference/webhooks#get): full reference
- [`webhooks.create()`](https://openemail.uk/docs/python/reference/webhooks#create): full reference
- [`webhooks.update()`](https://openemail.uk/docs/python/reference/webhooks#update): full reference
- [`webhooks.delete()`](https://openemail.uk/docs/python/reference/webhooks#delete): full reference
- [`webhooks.rotate_secret()`](https://openemail.uk/docs/python/reference/webhooks#rotateSecret): full reference
- [`webhooks.test()`](https://openemail.uk/docs/python/reference/webhooks#test): full reference
- [`webhooks.list_deliveries()`](https://openemail.uk/docs/python/reference/webhooks#listDeliveries): full reference
- [`webhooks.list_all_deliveries()`](https://openemail.uk/docs/python/reference/webhooks#listAllDeliveries): full reference
- [`webhooks.iterate_deliveries()`](https://openemail.uk/docs/python/reference/webhooks#iterateDeliveries): full reference
- [`webhooks.get_delivery()`](https://openemail.uk/docs/python/reference/webhooks#getDelivery): full reference
- [`webhooks.replay_delivery()`](https://openemail.uk/docs/python/reference/webhooks#replayDelivery): full reference
- [`webhooks.list_workspace_deliveries()`](https://openemail.uk/docs/python/reference/webhooks#listWorkspaceDeliveries): full reference
- [`webhooks.list_all_workspace_deliveries()`](https://openemail.uk/docs/python/reference/webhooks#listAllWorkspaceDeliveries): full reference
- [`webhooks.iterate_workspace_deliveries()`](https://openemail.uk/docs/python/reference/webhooks#iterateWorkspaceDeliveries): full reference
- [`webhooks.list_activity()`](https://openemail.uk/docs/python/reference/webhooks#listActivity): full reference
- [`webhooks.list_all_activity()`](https://openemail.uk/docs/python/reference/webhooks#listAllActivity): full reference
- [`webhooks.iterate_activity()`](https://openemail.uk/docs/python/reference/webhooks#iterateActivity): full reference
- [`webhooks.list_workspace_activity()`](https://openemail.uk/docs/python/reference/webhooks#listWorkspaceActivity): full reference
- [`webhooks.list_all_workspace_activity()`](https://openemail.uk/docs/python/reference/webhooks#listAllWorkspaceActivity): full reference
- [`webhooks.iterate_workspace_activity()`](https://openemail.uk/docs/python/reference/webhooks#iterateWorkspaceActivity): full reference
