Aller à la documentation
PHP

Points de terminaison

`webhooks->list`, `listAll`, `iterate`, `get`, `create`, `update`, `delete`, `rotateSecret`, `test`, `getDelivery` et `replayDelivery`, ainsi que les journaux de livraison et d'activité.

Toutes les méthodes

webhooks.php
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 est le SEUL moment où le secret est renvoyé, à part rotateSecret. Une lecture ne le répète jamais : stockez-le avant toute autre chose. Omettez eventTypes pour l'ensemble par défaut, tous les événements email.* sauf email.replied. email.replied, domain.*, suppression.*, file.* et form.* n'atteignent un endpoint que s'il les nomme.

list renvoie une OpenEmail\Result\Page, listAll renvoie tous les endpoints dans un seul tableau, et iterate renvoie un Generator qui fournit les endpoints un par un. create et update prennent le corps sous forme d'un seul tableau aux noms de l'API, et chaque endpoint revient sous forme de tableau à clés en camelCase.

rotateSecret n'a aucune fenêtre de recouvrement. L'ancien secret cesse de fonctionner immédiatement : déployez le nouveau avant de faire la rotation. L'appel n'est jamais réessayé automatiquement, car un réessai ferait une seconde rotation et invaliderait le secret que la première tentative a renvoyé.

create n'est pas réessayé non plus : une panne réseau peut donc laisser un endpoint créé avec un secret que vous n'avez jamais vu. Consultez list avant de le recréer. Un espace de travail contient 10 endpoints par défaut, et le suivant au-delà de la limite donne un 422 workspace_limit_reached.

Ce à quoi vous pouvez vous abonner

OpenEmail\Constants\WebhookEvents nomme chaque événement sous forme de constante, et WebhookEvents::values() les liste, pour que vous puissiez afficher la liste sans requête. webhooks->listEvents renvoie les mêmes noms avec un libellé pour chacun, ainsi que les limites auxquelles un endpoint est soumis dans maxEndpoints, maxAddresses et maxDomains. Les événements sont ceux de la boîte aux lettres, pas de cette API : email.received se déclenche pour du courrier qui arrive dans l'application, et email.sent pour un message envoyé depuis le compositeur. S'abonner n'équivaut pas à observer votre propre trafic API.

file.uploaded se déclenche quand un fichier est ajouté à la page Fichiers, et file.deleted quand un fichier est supprimé. Leur data contient fileId, filename, mimeType, sizeBytes, direction, to, threadId, messageId, et uploadedAt ou deletedAt. to est l'adresse à laquelle appartient le fichier, ou null pour un fichier qui appartient à tout l'espace de travail.

Les événements de fichier ne font pas partie de l'ensemble par défaut : un endpoint ne les reçoit que s'il les nomme dans eventTypes. Un endpoint limité à certaines adresses n'est informé que des fichiers de ces adresses, si bien qu'un fichier importé pour tout l'espace de travail, avec to à null, ne lui est pas envoyé.

form.submitted se déclenche quand quelqu'un s'inscrit via l'un de vos formulaires, et form.confirmed quand une inscription en attente rejoint les audiences, parce que la personne a ouvert le lien de confirmation ou parce que vous l'avez approuvée. Le data de form.submitted contient formId, formName, submissionId, email, status, answers, audienceIds, sourceUrl et submittedAt. Le data de form.confirmed contient formId, formName, submissionId, email, audienceIds, via, qui vaut link ou approval, et confirmedAt.

Une inscription sur un formulaire sans double opt-in envoie form.submitted avec status à added et aucun form.confirmed : traitez donc ce couple comme le moment où quelqu'un rejoint les audiences. Une personne qui s'inscrit de nouveau avant de confirmer garde le même submissionId, et form.submitted n'est renvoyé que si ses réponses ont changé. Les événements de formulaire ne font pas partie de l'ensemble par défaut, et un endpoint limité à certaines adresses ne les reçoit jamais, car les inscriptions appartiennent à tout l'espace de travail.

Prouver que ça fonctionne

webhook_test.php
$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 envoie en POST un événement email.sent synthétique et signé, puis attend la fin de la tentative. Il revient normalement quoi qu'ait répondu votre récepteur : branchez-vous donc sur $result['delivery']['status'], et non sur le fait que l'appel ait levé une exception. Un 4xx est une réponse utile : l'URL est joignable et le refus vient de votre propre gestionnaire, souvent de sa vérification de signature.

Un responseCode à null signifie qu'il n'y a eu aucune réponse (DNS, TLS, un dépassement de délai), ce qui n'est pas la même chose qu'une réponse valant 0. Chaque ligne porte attempt et maxAttempts : plusieurs lignes peuvent donc décrire un même événement. L'eventId commun à ces lignes désigne l'événement, et le numéro de tentative désigne l'essai. nextAttemptAt indique quand est prévue la nouvelle tentative automatique qui suit une ligne.

Renvoyer

webhook_replay.php
$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;

Une livraison qui continue d'échouer est tentée jusqu'à 8 fois : au moment où elle se produit, puis après 1 minute, 5 minutes, 30 minutes, 2 heures, 5 heures, 10 heures et 10 heures, soit environ 27 heures et demie au total. Seul un échec qui vaut la peine d'être répété l'est : pas de réponse, 408, 425, 429 ou un 5xx. Un rejeu renvoie l'événement stocké avec les mêmes id, type, createdAt et data, si bien qu'un destinataire qui ignore les identifiants déjà traités le considère comme l'événement qu'il connaît. Seule la signature est nouvelle.

  • replayDelivery envoie un événement maintenant et renvoie ce que votre serveur a répondu. Cela fonctionne aussi sur une tentative livrée, et ce n'est jamais réessayé. Avant l'envoi, les nouvelles tentatives automatiques de cet événement qui n'ont pas commencé sont suspendues : elles restent annulées si le rejeu est livré, et reprennent selon leur calendrier s'il échoue.
  • Si une nouvelle tentative automatique du même événement est en cours d'envoi à ce moment-là, replayDelivery n'envoie rien et lève un 409 retry_in_progress, et tant qu'un autre rejeu de cet événement est encore en cours d'envoi, il lève un 409 replay_in_progress : votre récepteur ne reçoit donc jamais deux copies à la fois, même de deux rejeux envoyés au même instant. Attendez quelques secondes et lisez getDelivery, car cette nouvelle tentative ou ce rejeu peut le livrer. Le rejeu se fait un événement à la fois : aucun appel ne renvoie toutes les livraisons échouées.
  • Il lève aussi un 409 pour un endpoint désactivé (webhook_disabled), un événement que l'endpoint n'écoute plus (event_not_subscribed) ou ne couvre plus (event_out_of_scope), et une tentative sans événement stocké (delivery_not_replayable). Chacun est une ConflictException, et OpenEmail\Constants\WebhookReplayErrorCodes nomme les codes. getDelivery annonce cette réponse à l'avance sous la forme de replayRefusal, null quand un rejeu aurait lieu et sinon un tableau avec code et message.

Le package ne réessaie jamais replayDelivery de lui-même, car un réessai après une réponse perdue renverrait l'événement.

Paramètres : webhooks->create

urlstringobligatoire
L'adresse où les livraisons sont envoyées en POST. HTTPS uniquement, et l'hôte ne peut pas être `localhost`, un nom en `.localhost`, `.local` ou `.internal`, ni une IP littérale de loopback, privée, carrier-grade NAT, link-local, multicast ou unique local. Il s'agit d'une requête côté serveur vers une adresse que vous fournissez : ces cas donnent donc un 422 `invalid_webhook_url` sur `url`. La vérification lit le nom d'hôte tel qu'il est écrit, et chaque livraison résout à nouveau l'hôte et refuse d'envoyer vers une adresse située dans l'une de ces plages. Les livraisons ne suivent jamais les redirections : enregistrez donc l'adresse finale. Ce qui est stocké est la sérialisation, par l'analyseur d'URL, de ce que vous avez envoyé : `https://acme.com` se relit donc `https://acme.com/`.
eventTypesarray
Les événements qui atteignent cet endpoint : n'importe laquelle des valeurs de `OpenEmail\Constants\WebhookEvents`. `create` plafonne le tableau au nombre d'événements existants : un de plus donne un 422 sur `eventTypes`, et `update` ne le plafonne pas. Seule la longueur est plafonnée, et un nom répété est stocké puis relu exactement tel que vous l'avez envoyé. Omis ou vide, il est stocké comme une liste vide, d'où sa relecture sous la forme `['*']`, et il signifie tous les événements `email.*` sauf `email.replied`, quatorze aujourd'hui, et jamais les familles de domaine, de suppression, de fichier ou de formulaire. Une famille ajoutée plus tard n'atteint jamais un endpoint qui ne l'a pas nommée : une intégration ne peut donc pas se mettre à recevoir, à cause d'une nouvelle version, une forme qu'elle n'a jamais vue.
descriptionstring
Un libellé pour l'endpoint, de 200 caractères au plus, pour qu'une liste de webhooks se lise comme des noms plutôt que comme une colonne d'URL. S'il est omis, il est stocké et renvoyé comme null. Omettez la clé plutôt que de passer null : le client envoie null tel quel, et `create` le refuse avec un 422.
addressAllowlistarray
Les adresses individuelles dont cet endpoint est informé. Un événement est livré quand l'adresse qu'il concerne figure sur cette liste, ou quand son domaine figure dans `domainAllowlist`. Laissez les deux vides et l'endpoint est informé de toutes les adresses que possède l'espace de travail. 50 au maximum, et une adresse que cet espace de travail ne possède pas donne un 422 `invalid_parameter`.
domainAllowlistarray
Les domaines entiers dont cet endpoint est informé, y compris les adresses qui leur sont ajoutées plus tard. Un domaine porte aussi ses propres événements `domain.*`. 25 au maximum.
apiKeystring
Un argument nommé à côté du tableau plutôt qu'une clé à l'intérieur : crée l'endpoint avec cette clé API au lieu de celle du client.

Réponse : l'endpoint créé

Un tableau à clés en camelCase. get, list et update renvoient la même forme sans secret.

objectstring
Toujours `webhook`, le même discriminant que renvoie une simple lecture, car le secret n'est qu'une clé supplémentaire sur la forme ordinaire plutôt qu'un type d'objet à part. La présence de `secret` dépend de la méthode que vous avez appelée, pas de ce champ.
idstring
L'identifiant de l'endpoint : `whe_` suivi de 24 caractères hexadécimaux. Tous les autres appels de webhook le prennent : `get`, `update`, `delete`, `rotateSecret`, `test`, `listDeliveries`, `listAllDeliveries`, `iterateDeliveries`, `getDelivery` et `replayDelivery`.
urlstring
L'endpoint tel qu'il est stocké, une fois passés les contrôles HTTPS et d'hôtes interdits. C'est l'URL analysée puis re-sérialisée : comparez donc à cette valeur plutôt qu'à la chaîne que vous avez envoyée.
descriptionstring or null
Le libellé que vous lui avez donné, ou null si vous n'en avez pas donné. Un `update` qui envoie `'description' => null` l'efface.
eventTypesarray
Les événements souscrits, ou `['*']` lorsque l'endpoint n'en a nommé aucun. `['*']` est la façon dont une liste stockée vide est rendue à la lecture et ne peut pas être renvoyée ; cette valeur représente les quatorze événements de message et non le catalogue entier. `create` et `update` n'acceptent que les noms d'événements littéraux.
enabledbool
Indique si les livraisons sont tentées. Un endpoint désactivé est ignoré lors de la distribution des événements et conserve son secret et son historique de livraisons. Toujours true ici, puisque seul `update` prend `enabled`.
disabledAtstring or null
Quand le serveur a désactivé l'endpoint après 100 livraisons échouées d'affilée. null tant qu'il est actif, et quand c'est vous qui l'avez désactivé.
disabledReasonstring or null
Pourquoi le serveur l'a désactivé. null chaque fois que `disabledAt` vaut null.
consecutiveFailuresint
Les livraisons échouées d'affilée. Tout événement livré le remet à 0, tout comme `update` avec `enabled` à true.
addressAllowlistarray
Les adresses individuelles dont cet endpoint est informé.
domainAllowlistarray
Les domaines entiers dont cet endpoint est informé. Deux listes vides signifient toutes les adresses que possède l'espace de travail.
lastDeliveryAtstring or null
Horodatage ISO 8601 de la dernière TENTATIVE de livraison, et non de la dernière réussite. Il est aussi posé après un POST en échec : il vous dit que l'endpoint a été sollicité, et `listDeliveries` vous dit comment cela s'est passé. null jusqu'à la première tentative, donc toujours null sur `create`.
createdAtstring
Horodatage ISO 8601 de l'enregistrement de l'endpoint. `list` renvoie les endpoints du plus récent au plus ancien selon ce champ.
secretstring
La clé HMAC-SHA-256 qui signe le `X-OpenEmail-Signature` de chaque livraison : `whsec_` suivi de 43 caractères base64url, et ce que vous passez à `OpenEmail::verifyWebhookSignature`, préfixe compris. Renvoyée par `create` et `rotateSecret`, et par rien d'autre. Une lecture ne la répète jamais : stockez-la maintenant. Un secret perdu ne peut être remplacé qu'avec `rotateSecret`, qui invalide l'ancien immédiatement.

Filtrer les journaux

webhook_logs.php
$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 lit un endpoint et listWorkspaceDeliveries tous les endpoints, ou ceux que nomme endpointIds:, sous forme de tableau ou d'une seule chaîne séparée par des virgules, et les deux prennent status: (delivered ou failed), since: et until:, les filtres de l'onglet Livraisons de la console. listActivity et listWorkspaceActivity lisent le journal d'audit : qui a créé, modifié, activé ou désactivé, renouvelé, testé, rejoué ou supprimé quoi. Chacune a une version listAll et une version iterate à côté, comme listAllDeliveries et iterateDeliveries, et chaque ligne du journal de l'espace de travail porte endpointId. webhooks->stats renvoie les chiffres de l'onglet Statistiques pour la période de votre choix.

since: et until: prennent un DateTimeInterface ou une chaîne ISO 8601, et une chaîne contenant une date seule signifie minuit UTC ce jour-là. until: doit être postérieur à since:, sinon l'appel lève une InvalidRequestException avec errorCode à invalid_parameter.