Перейти к документации
Python

Эндпоинты

`webhooks.list`, `list_all`, `iterate`, `get`, `create`, `update`, `delete`, `rotate_secret`, `test`, `get_delivery` и `replay_delivery`, а также журналы доставок и активности.

Все методы

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 является ЕДИНСТВЕННЫМ случаем, когда секрет возвращается, не считая rotate_secret. При чтении он никогда не выдаётся, поэтому сохраните его прежде всего остального. Опустите eventTypes, чтобы получить набор по умолчанию: все события email.*, кроме email.replied. email.replied, domain.*, suppression.*, file.* и form.* доходят до эндпоинта, только если он их называет.

У rotate_secret нет окна перекрытия. Старый секрет перестаёт работать немедленно, поэтому выкатывайте новый до ротации. Автоматически этот вызов никогда не повторяется: повтор выполнил бы ротацию второй раз и обесценил секрет, который вернула первая попытка.

На что можно подписаться

WEBHOOK_EVENTS экспортируется, чтобы вы могли отрисовать список. Все события относятся к **почтовому ящику**, а не к этому API: email.received срабатывает для почты, пришедшей в приложение, а email.sent срабатывает для письма, отправленного из композера. Подписка не то же самое, что наблюдение за собственным трафиком API.

file.uploaded срабатывает, когда файл помещают на страницу «Файлы», а file.deleted, когда файл удаляют. Их данные имеют тип FileEventData: fileId, filename, mimeType, sizeBytes, direction, to, threadId, messageId и uploadedAt или deletedAt. to это адрес, которому принадлежит файл, или null для файла, принадлежащего всему рабочему пространству.

Файловых событий нет в наборе по умолчанию, поэтому эндпоинт получает их, только если называет их в eventTypes. Эндпоинт, ограниченный некоторыми адресами, узнаёт только о файлах этих адресов, поэтому загрузка для всего рабочего пространства, с to равным null, ему не отправляется.

form.submitted срабатывает, когда кто-то подписывается через одну из ваших форм, а form.confirmed, когда ожидающая подписка вступает в аудитории, потому что человек открыл ссылку для подтверждения или потому что вы её одобрили. form.submitted несёт данные типа FormSubmittedEventData: formId, formName, submissionId, email, status, answers, audienceIds, sourceUrl и submittedAt. form.confirmed несёт данные типа FormConfirmedEventData: formId, formName, submissionId, email, audienceIds, via, равный link или approval, и confirmedAt.

Подписка через форму без двойного подтверждения отправляет form.submitted со status, равным added, и не отправляет form.confirmed, поэтому считайте эту пару моментом, когда человек вступает. Тот, кто подписывается снова до подтверждения, сохраняет тот же submissionId, а form.submitted приходит повторно, только если ответы изменились. Событий форм нет в наборе по умолчанию, а эндпоинт, ограниченный некоторыми адресами, никогда их не получает, потому что подписки принадлежат всему рабочему пространству.

Каждая из этих форм данных является TypedDict в openemail.types. Аннотируйте, например, проверенное событие как WebhookPayload[FileEventData], и средство проверки типов будет знать, что содержит event['data'].

Как убедиться, что это работает

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'])

responseCode, равный None, означает, что ответа не было вовсе (DNS, TLS, таймаут), и это другой факт, нежели ответ со значением 0. Каждая строка несёт attempt и maxAttempts, поэтому одно событие могут описывать несколько строк: одинаковый eventId в них обозначает событие, а номер попытки обозначает попытку. nextAttemptAt говорит, когда наступит автоматический повтор, следующий за строкой.

Повторная отправка

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'])

Доставка, которая продолжает проваливаться, пробуется до 8 раз: сразу, затем через 1 минуту, 5 минут, 30 минут, 2 часа, 5 часов, 10 часов и 10 часов, всего около 27 с половиной часов. Повторяется только сбой, который стоит повторять: нет ответа, 408, 425, 429 или 5xx. Повторная отправка снова шлёт сохранённое событие с тем же id, type, createdAt и data, так что получатель, отбрасывающий уже обработанные идентификаторы, принимает его за уже известное событие. Новая только подпись.

  • replay_delivery отправляет одно событие прямо сейчас и возвращает то, что ответил ваш сервер. Работает и для доставленной попытки и никогда не повторяется. Перед отправкой ещё не начавшиеся автоматические повторы этого события приостанавливаются: если повторная отправка доставлена, они остаются отменёнными, а если она не удалась, возобновляются по своему расписанию.
  • Если в этот момент уже отправляется автоматический повтор того же события, replay_delivery ничего не отправляет и отклоняется с 409 retry_in_progress, а пока ещё отправляется другая повторная отправка этого события, он отклоняется с 409 replay_in_progress, так что получатель никогда не получит две копии одновременно, даже от двух повторных отправок в один и тот же миг. Подождите несколько секунд и прочитайте get_delivery: этот повтор или эта повторная отправка может доставить событие. Повторная отправка идёт по одному событию: нет вызова, который заново отправляет все неудавшиеся доставки.
  • Кроме того, он отклоняет с 409 выключенный эндпоинт (webhook_disabled), событие, которое эндпоинт больше не слушает (event_not_subscribed) или больше не охватывает (event_out_of_scope), и попытку без сохранённого события (delivery_not_replayable). get_delivery заранее сообщает этот ответ в поле replayRefusal.

SDK никогда не повторяет replay_delivery сам, потому что повтор после потерянного ответа отправил бы событие ещё раз.

Каждый отказ выбрасывает OpenEmailApiError со status 409, истинным is_conflict и причиной в code, одним из значений WEBHOOK_REPLAY_ERROR_CODES.

Параметры: webhooks.create

urlstrобязательно
Куда доставки отправляются методом POST. Только HTTPS, и хостом не может быть `localhost`, имя в `.localhost`/`.local`/`.internal` или IP-литерал петлевого, частного, CGNAT- либо link-local-диапазона. Это серверный запрос по адресу, который даёте вы, поэтому такие значения дают 422 по `url`; проверка читает имя хоста как написано и никогда не разрешает DNS. Хранится сериализация того, что вы отправили, выполненная парсером URL, поэтому `https://acme.com` читается обратно как `https://acme.com/`.
eventTypeslist[WebhookEvent]
Какие события доходят до этого эндпоинта: любые имена из `WEBHOOK_EVENTS`. `POST /webhooks` ограничивает массив числом существующих событий, поэтому на одно больше даёт 422 по `eventTypes`; `PATCH` такого ограничения не накладывает. Ограничена только длина, а повторяющееся имя сохраняется и читается обратно ровно так, как вы его отправили. Пропущенное или пустое значение сохраняется как пустой список, потому оно и читается обратно как `['*']`, и означает все события `email.*`, кроме `email.replied`, то есть четырнадцать на сегодня, и никогда не семейства доменов, подавлений или файлов. Семейство, добавленное позже, никогда не доходит до эндпоинта, который его не назвал, поэтому интеграция не может начать получать форму, которой она никогда не видела, просто из-за релиза.
descriptionstr
Подпись для эндпоинта, не длиннее 200 символов, чтобы список вебхуков читался как имена, а не как колонка URL. Если не указана, сохраняется и возвращается как null.

Ответ: CreatedWebhookResource

objectLiteral['webhook']
Всегда `'webhook'`: тот же дискриминатор, который возвращает обычное чтение, потому что секрет представляет собой один дополнительный ключ в обычной форме, а не отдельный тип объекта. Присутствует ли `secret`, определяется тем, какой метод вы вызвали, а не этим полем.
idstr
Идентификатор эндпоинта: `whe_`, за которым следуют 24 шестнадцатеричных символа. Его принимают все прочие вызовы вебхуков: `get`, `update`, `delete`, `rotate_secret`, `test`, `list_deliveries`, `list_all_deliveries`, `iterate_deliveries`, `get_delivery` и `replay_delivery`.
urlstr
Эндпоинт в сохранённом виде, прошедший проверки на HTTPS и на запрещённые хосты. Это разобранный и заново сериализованный URL, поэтому сравнивайте с этим значением, а не со строкой, которую вы отправили.
descriptionstr | None
Подпись, которую вы ему дали, или null, если не давали. `update` с явным null возвращает её в null.
eventTypeslist[WebhookEvent] | ['*']
Подписанные события или `['*']`, когда эндпоинт не назвал ни одного. В виде `['*']` при чтении отображается пустой сохранённый список; отправить его обратно нельзя, и он обозначает четырнадцать событий о письмах, а не весь каталог. `create` и `update` принимают только буквальные имена событий.
enabledbool
Предпринимаются ли попытки доставки; выключенный эндпоинт пропускается при рассылке событий и сохраняет свой секрет и историю доставок. Здесь всегда true, поскольку в `WebhookCreate` нет `enabled`, а есть он только в `WebhookPatch`.
lastDeliveryAtstr | None
Отметка времени ISO 8601 последней ПОПЫТКИ доставки, а не последнего успеха. Она проставляется и после неудавшегося POST, поэтому она говорит, что эндпоинт пробовали, а как всё прошло, говорит `list_deliveries`. Null до первой попытки, а значит, всегда null при `create`.
createdAtstr
Отметка времени ISO 8601 того, когда эндпоинт был зарегистрирован. `list` возвращает эндпоинты от новых к старым по этому полю.
secretstr
Ключ HMAC-SHA-256, которым подписывается `X-OpenEmail-Signature` каждой доставки: `whsec_`, за которым следуют 32 случайных байта в base64url; именно его вы передаёте в `verify_webhook_signature`. Возвращается `create` и `rotate_secret` и больше ничем. При чтении он никогда не выдаётся, поэтому сохраните его сразу; потерянный секрет можно только заменить через `rotate_secret`, который немедленно обесценивает старый.

Фильтры журналов

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 читает один эндпоинт, а list_workspace_deliveries все или те, что названы в endpoint_ids=, и оба принимают status=, since= и until=, фильтры вкладки «Доставки» в консоли. list_activity и list_workspace_activity читают журнал аудита: кто что создал, изменил, выключил или включил, ротировал, проверил, переотправил или удалил. У каждого рядом есть list_all_… и iterate_…, и каждая строка журнала рабочего пространства несёт endpointId.

since= и until= принимают datetime или строку ISO 8601. Наивный datetime читается как местное время и переводится в UTC, поэтому передавайте объект с часовым поясом, как выше.

Справочник