Aller à la documentation
Python

Points de terminaison

`webhooks.list`, `list_all`, `iterate`, `get`, `create`, `update`, `delete`, `rotate_secret`, `test`, `get_delivery` et `replay_delivery`, ainsi que les journaux de livraison et d'activité.

Toutes les méthodes

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 est le SEUL moment où le secret est renvoyé, à part rotate_secret. 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.

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

Ce à quoi vous pouvez vous abonner

WEBHOOK_EVENTS est exporté pour que vous puissiez afficher la liste. 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é. Leurs données sont FileEventData : 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. form.submitted porte FormSubmittedEventData : formId, formName, submissionId, email, status, answers, audienceIds, sourceUrl et submittedAt. form.confirmed porte FormConfirmedEventData : 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.

Chacune de ces formes de données est un TypedDict dans openemail.types. Annotez par exemple un événement vérifié comme WebhookPayload[FileEventData], et un vérificateur de types sait ce que contient event['data'].

Prouver que ça fonctionne

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

Un responseCode à None 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.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'])

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.

  • replay_delivery 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à, replay_delivery n'envoie rien et est refusé avec un 409 retry_in_progress, et tant qu'un autre rejeu de cet événement est encore en cours d'envoi, il est refusé avec un 409 replay_in_progress, si bien que votre destinataire ne reçoit jamais deux copies à la fois, même de deux rejeux envoyés au même instant. Attendez quelques secondes et lisez get_delivery, car cette tentative ou ce rejeu peut le livrer. Le rejeu se fait un événement à la fois : aucun appel ne renvoie toutes les livraisons en échec.
  • Il refuse aussi avec un 409 un point de terminaison désactivé (webhook_disabled), un événement que le point de terminaison n'écoute plus (event_not_subscribed) ou ne couvre plus (event_out_of_scope), et une tentative sans événement stocké (delivery_not_replayable). get_delivery annonce cette réponse à l'avance sous la forme de replayRefusal.

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

Chaque refus lève OpenEmailApiError avec status à 409, is_conflict à vrai et la raison dans code, l'une des valeurs de WEBHOOK_REPLAY_ERROR_CODES.

Paramètres : webhooks.create

urlstrobligatoire
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`/`.internal`, ni une IP littérale de loopback, privée, CGNAT ou link-local. Il s'agit d'une requête côté serveur vers une adresse que vous fournissez : ces cas donnent donc un 422 sur `url` ; la vérification lit le nom d'hôte tel qu'il est écrit et ne résout jamais le DNS. Ce qui est stocké, c'est la sérialisation par l'analyseur d'URL de ce que vous avez envoyé : `https://acme.com` se relit donc `https://acme.com/`.
eventTypeslist[WebhookEvent]
Les événements qui atteignent cet endpoint : n'importe lequel des noms de `WEBHOOK_EVENTS`. `POST /webhooks` plafonne le tableau au nombre d'événements existants : un de plus donne un 422 sur `eventTypes` ; `PATCH` 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 cela signifie tous les événements `email.*` sauf `email.replied`, soit quatorze aujourd'hui, et jamais les familles domaine, suppression ou fichier. 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, à la faveur d'une mise en production, une forme qu'elle n'a jamais vue.
descriptionstr
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.

Réponse : CreatedWebhookResource

objectLiteral['webhook']
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.
idstr
L'identifiant de l'endpoint : `whe_` suivi de 24 caractères hexadécimaux. Tous les autres appels de webhook le prennent : `get`, `update`, `delete`, `rotate_secret`, `test`, `list_deliveries`, `list_all_deliveries`, `iterate_deliveries`, `get_delivery` et `replay_delivery`.
urlstr
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.
descriptionstr | None
Le libellé que vous lui avez donné, ou null si vous n'en avez pas donné. Un `update` qui envoie un null explicite le remet à null.
eventTypeslist[WebhookEvent] | ['*']
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 `WebhookCreate` n'a pas d'`enabled` et que seul `WebhookPatch` en a un.
lastDeliveryAtstr | None
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 `list_deliveries` vous dit comment cela s'est passé. Null jusqu'à la première tentative, donc toujours null sur `create`.
createdAtstr
Horodatage ISO 8601 de l'enregistrement de l'endpoint. `list` renvoie les endpoints du plus récent au plus ancien selon ce champ.
secretstr
La clé HMAC-SHA-256 qui signe le `X-OpenEmail-Signature` de chaque livraison : `whsec_` suivi de 32 octets aléatoires en base64url, et c'est elle que vous passez à `verify_webhook_signature`. Renvoyée par `create` et `rotate_secret`, et par rien d'autre. Une lecture ne la répète jamais : stockez-la maintenant ; un secret perdu ne peut être remplacé que par `rotate_secret`, qui invalide l'ancien immédiatement.

Filtrer les journaux

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 lit un point de terminaison et list_workspace_deliveries tous, ou ceux que nomme endpoint_ids=, et les deux acceptent status=, since= et until=, les filtres de l'onglet Livraisons de la console. list_activity et list_workspace_activity lisent le journal d'audit : qui a créé, modifié, activé ou désactivé, renouvelé, testé, rejoué ou supprimé quoi. Chacune a un list_all_… et un iterate_… à côté, et chaque ligne du journal de l'espace de travail porte endpointId.

since= et until= acceptent un datetime ou une chaîne ISO 8601. Un datetime naïf est lu comme une heure locale puis converti en UTC : passez donc un objet avisé, comme ci-dessus.

Référence