Эндпоинты
`webhooks->list`, `listAll`, `iterate`, `get`, `create`, `update`, `delete`, `rotateSecret`, `test`, `getDelivery` и `replayDelivery`, а также журналы доставок и активности.
Все методы
use OpenEmail\Constants\WebhookEvents; $endpoint = $client->webhooks->create([ 'url' => 'https://acme.com/hooks/mail', 'eventTypes' => [WebhookEvents::EMAIL_SENT, WebhookEvents::EMAIL_BOUNCED], 'description' => 'Billing service',]); file_put_contents('.openemail-webhook-secret', $endpoint['secret']); $client->webhooks->list();$client->webhooks->get($endpoint['id']);$client->webhooks->update($endpoint['id'], ['enabled' => false]);$client->webhooks->test($endpoint['id']); foreach ($client->webhooks->listDeliveries($endpoint['id'], limit: 1) as $latest) { $client->webhooks->getDelivery($endpoint['id'], $latest['id']); $client->webhooks->replayDelivery($endpoint['id'], $latest['id']);} $rotated = $client->webhooks->rotateSecret($endpoint['id']);file_put_contents('.openemail-webhook-secret', $rotated['secret']); $client->webhooks->delete($endpoint['id']);create является ЕДИНСТВЕННЫМ случаем, когда секрет возвращается, не считая rotateSecret. При чтении он никогда не выдаётся, поэтому сохраните его прежде всего остального. Опустите eventTypes, чтобы получить набор по умолчанию: все события email.*, кроме email.replied. email.replied, domain.*, suppression.*, file.* и form.* доходят до эндпоинта, только если он их называет.
list возвращает одну OpenEmail\Result\Page, listAll возвращает все эндпоинты одним массивом, а iterate возвращает Generator, который выдаёт эндпоинты по одному. create и update принимают тело одним массивом с именами из API, и каждый эндпоинт возвращается как массив с ключами в camelCase.
У rotateSecret нет окна перекрытия. Старый секрет перестаёт работать немедленно, поэтому выкатывайте новый до ротации. Автоматически этот вызов никогда не повторяется: повтор выполнил бы ротацию второй раз и обесценил секрет, который вернула первая попытка.
create тоже не повторяется, поэтому сетевой сбой может оставить созданный эндпоинт с секретом, которого вы так и не увидели. Проверьте list, прежде чем создавать его снова. По умолчанию рабочее пространство вмещает 10 эндпоинтов, а следующий сверх лимита даёт 422 workspace_limit_reached.
На что можно подписаться
OpenEmail\Constants\WebhookEvents задаёт каждое событие константой, а WebhookEvents::values() их перечисляет, поэтому список можно отрисовать без запроса. webhooks->listEvents возвращает те же имена с подписью для каждого, а также пределы для эндпоинта в maxEndpoints, maxAddresses и maxDomains. События являются событиями почтового ящика, а не этого API: email.received срабатывает для почты, пришедшей в приложение, а email.sent для сообщения, отправленного из композера. Подписка не равнозначна наблюдению за собственным API-трафиком.
file.uploaded срабатывает, когда файл помещают на страницу «Файлы», а file.deleted, когда его удаляют. Их data содержит fileId, filename, mimeType, sizeBytes, direction, to, threadId, messageId и uploadedAt или deletedAt. to является адресом, которому принадлежит файл, или null для файла, принадлежащего всему рабочему пространству.
Файловых событий нет в наборе по умолчанию, поэтому эндпоинт получает их, только если называет их в eventTypes. Эндпоинт, ограниченный некоторыми адресами, узнаёт только о файлах этих адресов, поэтому загрузка для всего рабочего пространства, с to равным null, ему не отправляется.
form.submitted срабатывает, когда кто-то подписывается через одну из ваших форм, а form.confirmed, когда ожидающая подписка вступает в аудитории, потому что человек открыл ссылку подтверждения или потому что вы её одобрили. data у form.submitted содержит formId, formName, submissionId, email, status, answers, audienceIds, sourceUrl и submittedAt. data у form.confirmed содержит formId, formName, submissionId, email, audienceIds, via (равный link или approval) и confirmedAt.
Подписка через форму без двойного подтверждения отправляет form.submitted со status, равным added, и не отправляет form.confirmed, поэтому считайте эту пару моментом, когда человек вступает. Тот, кто подписывается снова до подтверждения, сохраняет тот же submissionId, а form.submitted приходит повторно, только если ответы изменились. Событий форм нет в наборе по умолчанию, а эндпоинт, ограниченный некоторыми адресами, никогда их не получает, потому что подписки принадлежат всему рабочему пространству.
Как убедиться, что это работает
$result = $client->webhooks->test('whe_3f9c2a7b1e4d8f60a5c7b92d');echo $result['delivery']['status'], ' ', $result['delivery']['responseCode'] ?? 'no response', PHP_EOL; foreach ($client->webhooks->iterateDeliveries('whe_3f9c2a7b1e4d8f60a5c7b92d') as $delivery) { echo $delivery['eventType'], ' ', $delivery['status'], ' ', $delivery['responseCode'] ?? '-', ' ', $delivery['error'] ?? '', PHP_EOL;}test отправляет подписанное синтетическое событие email.sent и ждёт завершения попытки. Он возвращается штатно, что бы ни ответил ваш приёмник, поэтому ветвитесь по $result['delivery']['status'], а не по тому, выбросил ли вызов исключение. 4xx является полезным ответом: URL доступен, а отказ пришёл от вашего собственного обработчика, часто от его проверки подписи.
responseCode, равный null, означает, что ответа не было вовсе (DNS, TLS, таймаут), а это иной факт, чем ответ, сообщивший 0. Каждая строка несёт attempt и maxAttempts, поэтому несколько строк могут описывать одно событие: общий eventId обозначает событие, а номер попытки обозначает попытку. nextAttemptAt сообщает, когда назначен автоматический повтор после этой строки.
Повторная отправка
$detail = $client->webhooks->getDelivery('whe_3f9c2a7b1e4d8f60a5c7b92d', 'whd_8c1e4a7f2b9d3e6a0c5f1b28');echo json_encode($detail['payload'], JSON_THROW_ON_ERROR), PHP_EOL;echo $detail['responseBody'] ?? 'no answer', ' ', $detail['replayRefusal']['code'] ?? 'replayable', PHP_EOL; $replay = $client->webhooks->replayDelivery('whe_3f9c2a7b1e4d8f60a5c7b92d', 'whd_8c1e4a7f2b9d3e6a0c5f1b28');echo $replay['delivery']['status'], ' ', $replay['delivery']['responseCode'] ?? 'no response', PHP_EOL;Доставка, которая продолжает проваливаться, пробуется до 8 раз: сразу, затем через 1 минуту, 5 минут, 30 минут, 2 часа, 5 часов, 10 часов и 10 часов, всего около 27 с половиной часов. Повторяется только сбой, который стоит повторять: нет ответа, 408, 425, 429 или 5xx. Повторная отправка снова шлёт сохранённое событие с тем же id, type, createdAt и data, так что получатель, отбрасывающий уже обработанные идентификаторы, принимает его за уже известное событие. Новая только подпись.
replayDeliveryотправляет одно событие сейчас и возвращает ответ вашего сервера. Работает и с доставленной попыткой и никогда не повторяется. Перед отправкой ещё не начавшиеся автоматические повторы этого события приостанавливаются: если повторная отправка доставлена, они остаются отменёнными, а если она не удалась, возобновляются по своему расписанию.- Если в этот момент отправляется автоматический повтор того же события,
replayDeliveryничего не отправляет и выбрасывает 409retry_in_progress, а пока ещё отправляется другая повторная отправка этого события, выбрасывает 409replay_in_progress, поэтому ваш приёмник никогда не получит две копии одновременно, даже от двух повторных отправок, запущенных в один и тот же момент. Подождите несколько секунд и прочитайтеgetDelivery, поскольку тот повтор или та повторная отправка могут доставить событие. Повторная отправка работает по одному событию: нет вызова, который заново отправляет все неудачные доставки. - Он также выбрасывает 409 для выключенного эндпоинта (
webhook_disabled), для события, которое эндпоинт больше не слушает (event_not_subscribed) или больше не охватывает (event_out_of_scope), и для попытки без сохранённого события (delivery_not_replayable). Каждый из них являетсяConflictException, аOpenEmail\Constants\WebhookReplayErrorCodesперечисляет коды.getDeliveryзаранее сообщает такой ответ какreplayRefusal: null, если повторная отправка пройдёт, а иначе массив сcodeиmessage.
Пакет никогда сам не повторяет replayDelivery, потому что повтор после потерянного ответа отправил бы событие ещё раз.
Параметры: webhooks->create
urlstringобязательно- Куда отправляются доставки методом POST. Только HTTPS, и хост не может быть `localhost`, именем на `.localhost`, `.local` или `.internal` либо IP-литералом из диапазонов loopback, частной сети, NAT операторского уровня, link-local, multicast или уникальных локальных адресов. Это серверный запрос на указанный вами адрес, поэтому такие значения дают 422 `invalid_webhook_url` для `url`. Проверка читает имя хоста в написанном виде, а каждая доставка заново разрешает хост и отказывается отправлять на адрес из любого из этих диапазонов. Доставки никогда не следуют перенаправлениям, поэтому регистрируйте конечный адрес. Сохраняется сериализация парсера URL от того, что вы отправили, поэтому `https://acme.com` читается обратно как `https://acme.com/`.
eventTypesarray- Какие события доходят до этого эндпоинта: любые значения из `OpenEmail\Constants\WebhookEvents`. `create` ограничивает массив числом существующих событий, поэтому на одно больше даёт 422 для `eventTypes`, а `update` его не ограничивает. Ограничивается только длина, а повторяющееся имя сохраняется и читается обратно ровно так, как вы его отправили. Если параметр не указан или пуст, он сохраняется как пустой список, поэтому читается обратно как `['*']`, и это означает все события `email.*`, кроме `email.replied` (сейчас их четырнадцать), и никогда семейства доменов, подавлений, файлов или форм. Семейство, добавленное позже, никогда не доходит до эндпоинта, который его не назвал, поэтому интеграция не может из-за нового выпуска начать получать данные в форме, которой никогда не видела.
descriptionstring- Подпись эндпоинта, не более 200 символов, чтобы список вебхуков читался как имена, а не как столбец URL. Если не указана, сохраняется и возвращается как null. Не передавайте null, а просто не указывайте ключ: клиент отправляет null как есть, и `create` отклоняет его с 422.
addressAllowlistarray- Отдельные адреса, о которых узнаёт этот эндпоинт. Событие доставляется, когда адрес, к которому оно относится, есть в этом списке или когда его домен есть в `domainAllowlist`. Оставьте оба пустыми, и эндпоинт будет узнавать обо всех адресах рабочего пространства. Не более 50, а адрес, который не принадлежит этому рабочему пространству, даёт 422 `invalid_parameter`.
domainAllowlistarray- Целые домены, о которых узнаёт этот эндпоинт, включая адреса, добавленные в них позже. Домен также несёт собственные события `domain.*`. Не более 25.
apiKeystring- Именованный аргумент рядом с массивом, а не ключ внутри него: создаёт эндпоинт с этим API-ключом вместо ключа клиента.
Ответ: созданный эндпоинт
Массив с ключами в camelCase. get, list и update возвращают ту же форму без secret.
objectstring- Всегда `webhook`, тот же дискриминатор, что возвращает обычное чтение, потому что секрет является одним дополнительным ключом в обычной форме, а не отдельным типом объекта. Присутствует ли `secret`, определяется вызванным методом, а не этим полем.
idstring- Идентификатор эндпоинта: `whe_` и 24 шестнадцатеричных символа. Его принимает любой другой вызов для вебхуков: `get`, `update`, `delete`, `rotateSecret`, `test`, `listDeliveries`, `listAllDeliveries`, `iterateDeliveries`, `getDelivery` и `replayDelivery`.
urlstring- Эндпоинт в сохранённом виде, прошедший проверки на HTTPS и запрещённые хосты. Это разобранный и заново сериализованный URL, поэтому сравнивайте с этим значением, а не со строкой, которую вы отправили.
descriptionstring or null- Подпись, которую вы дали, или null, если не дали. `update`, отправляющий `'description' => null`, её очищает.
eventTypesarray- Подписанные события или `['*']`, когда эндпоинт не назвал ни одного. В виде `['*']` при чтении отображается пустой сохранённый список; отправить его обратно нельзя, и он обозначает четырнадцать событий о письмах, а не весь каталог. `create` и `update` принимают только буквальные имена событий.
enabledbool- Выполняются ли попытки доставки. Отключённый эндпоинт пропускается при рассылке событий и сохраняет свой секрет и историю доставок. Здесь всегда true, поскольку `enabled` принимает только `update`.
disabledAtstring or null- Когда сервер отключил эндпоинт после 100 неудачных доставок подряд. null, пока он включён, а также если вы отключили его сами.
disabledReasonstring or null- Почему сервер его отключил. null всякий раз, когда `disabledAt` равно null.
consecutiveFailuresint- Неудачные доставки подряд. Любое доставленное событие сбрасывает счётчик в 0, как и `update` с `enabled`, равным true.
addressAllowlistarray- Отдельные адреса, о которых узнаёт этот эндпоинт.
domainAllowlistarray- Целые домены, о которых узнаёт этот эндпоинт. Оба пустых списка означают все адреса рабочего пространства.
lastDeliveryAtstring or null- Метка времени ISO 8601 последней ПОПЫТКИ доставки, а не последнего успеха. Ставится и после неудачного POST, поэтому говорит о том, что к эндпоинту обращались, а `listDeliveries` говорит, чем это закончилось. null до первой попытки, поэтому в `create` всегда null.
createdAtstring- Отметка времени ISO 8601 того, когда эндпоинт был зарегистрирован. `list` возвращает эндпоинты от новых к старым по этому полю.
secretstring- Ключ HMAC-SHA-256, которым подписывается `X-OpenEmail-Signature` каждой доставки: `whsec_` и 43 символа base64url, и именно его вы передаёте в `OpenEmail::verifyWebhookSignature` вместе с префиксом. Возвращается `create` и `rotateSecret` и больше ничем. Чтение никогда его не возвращает, поэтому сохраните его сейчас. Потерянный секрет можно только заменить через `rotateSecret`, который немедленно делает старый недействительным.
Фильтры журналов
$failed = $client->webhooks->listWorkspaceDeliveries(status: 'failed', since: new \DateTimeImmutable('-1 day')); foreach ($failed as $delivery) { echo $delivery['endpointId'], ' ', $delivery['eventType'], ' ', $delivery['responseCode'] ?? '-', PHP_EOL;} $history = $client->webhooks->listActivity('whe_3f9c2a7b1e4d8f60a5c7b92d'); foreach ($history as $change) { echo $change['type'], ' ', $change['actor']['label'] ?? 'OpenEmail', PHP_EOL;}listDeliveries читает один эндпоинт, а listWorkspaceDeliveries все эндпоинты или те, что называет endpointIds:, в виде массива или одной строки через запятую, и оба принимают status: (delivered или failed), since: и until:, фильтры вкладки «Доставки» в консоли. listActivity и listWorkspaceActivity читают журнал аудита: кто что создал, изменил, переключил, ротировал, протестировал, отправил повторно или удалил. У каждого есть версии listAll и iterate, например listAllDeliveries и iterateDeliveries, а каждая строка журнала рабочего пространства несёт endpointId. webhooks->stats возвращает числа, стоящие за вкладкой «Аналитика», за выбранное вами окно.
since: и until: принимают DateTimeInterface или строку ISO 8601, а строка с голой датой означает полночь UTC в этот день. until: должен быть позже since:, иначе вызов выбрасывает InvalidRequestException, у которого errorCode равно invalid_parameter.