Aller à la documentation
Python

Réessais et idempotence

Ce qui est réessayé, ce qui ne l'est délibérément pas, et pourquoi un envoi réessayé ne peut pas faire de doublon.

Envois

Le client attache un Idempotency-Key à chaque envoi (emails.send, emails.send_batch, templates.send et broadcasts.send), généré une fois par **appel** et réutilisé par les réessais de cet appel. L'API réserve cette clé avant de dispatcher quoi que ce soit : un réessai rejoue donc le message d'origine au lieu d'en envoyer un second, tandis que deux appels délibérés à send() envoient bien deux fois. Ce sont des intentions différentes, et elles le restent.

Passez votre propre idempotency_key pour étendre cette garantie au-delà d'un même processus : un job qui a planté puis s'est relancé rejoue ses envois au lieu de les répéter.

idempotency.py
invoice_id = 'inv_4192' client.emails.send(    {'from': sender, 'to': recipient, 'subject': subject, 'text': text},    idempotency_key=f'invoice:{invoice_id}',)

Dérivez-la de ce qui a rendu l'envoi nécessaire. Jamais d'une horloge. Réutiliser une clé avec un corps différent est refusé par idempotency_key_reuse plutôt que rejoué en silence.

Tout le reste

Toute lecture est réessayée. Une écriture ne l'est que lorsqu'une seconde requête identique ne peut rien signifier d'autre que la première ; un envoi entre dans ce cas parce que sa clé d'idempotence transforme une répétition en rejeu.

AppelRéessayéPourquoi
Toute lectureOuiRien ne change.
emails.send, emails.send_batch, templates.send et broadcasts.sendOuiUne clé d'idempotence transforme une répétition en rejeu.
emails.cancel, emails.reschedule, broadcasts.cancel, forms.publish, forms.pause, forms.resume, forms.approve_submission, account.accept_invitation et account.decline_invitationOuiUne simple affectation d'un état nommé.
threads.update, threads.trash et threads.restoreOuiUne affectation de libellés. L'appliquer deux fois revient à l'appliquer une fois.
threads.snooze, threads.unsnoozeOuiL'instant de réveil est dans le corps de la requête, il n'est pas déduit de l'heure d'arrivée.
labels.update, webhooks.update, settings.update, roles.update, members.update, domains.update, contacts.update, audiences.update, keys.update, emails.update, forms.update, branding.update, chats.rename, threads.update_note, domains.update_address, domains.update_address_forward, account.set_email_notification, account.set_push_muted, app_host.set et workspaces.set_activeOuiUne simple affectation de champs nommés.
members.grant_address, members.grant_domain, rules.reorder et threads.reorder_notesOuiL'octroi est un upsert, et l'ordre est énoncé en entier.
templates.publish, imports.startOuiPublier une tête déjà publiée, ou démarrer un import déjà démarré, renvoie l'élément inchangé.
templates.preview, templates.render, broadcasts.preview et rules.testOuiElles rendent, comptent ou évaluent, et n'écrivent rien.
domains.verify, app_host.verifyOuiUne vérification répétée ne change rien d'autre que l'heure de la vérification.
contacts.save, contacts.set_audiences, contacts.remove_photo, contacts.block, contacts.unblock, contacts.delete_many, keys.revoke, account.remove_photo, branding.remove_image, domains.remove_logo, domains.remove_logo_certificate, domains.remove_address_photo, app_host.delete, files.revoke_link, files.revoke_all_links et subscriptions.moveOuiChacun énonce le résultat final : un second appel laisse donc ce que le premier a laissé.
audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_address et senders.researchOuiUne répétition trouve le travail du premier appel déjà fait et le signale au lieu de le faire deux fois.
contacts.set_photo, imports.upload_chunk, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate et domains.set_address_photoOuiLes octets renvoyés remplacent ce que la première tentative a stocké.
drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.upload, templates.design et forms.designNonUn réessai laisse deux objets.
drafts.updateNonLisez l'id sur le résultat de chaque écriture plutôt que de réutiliser celui que vous avez envoyé.
drafts.delete, labels.delete, webhooks.delete, templates.delete, rules.delete, roles.delete, members.remove, members.revoke_address, temp_mail.delete et temp_mail.delete_messageNonUn réessai après une réponse perdue signale un échec pour un travail qui a réussi.
webhooks.rotate_secretNonUne seconde rotation invalide le secret que la première tentative a renvoyé.
webhooks.testNonCela enverrait une seconde livraison synthétique.
webhooks.replay_deliveryNonIl enverrait l'événement une seconde fois à votre destinataire.
emails.translateNonCela consomme des appels au modèle : un réessai après une requête restée sans réponse paie deux fois la même réponse.
emails.compose, emails.rewrite et emails.suggest_subjectNonChaque tentative consomme une action d'IA de plus et revient avec une réponse différente.
Tout autre appelNonEnvoyée une seule fois ; un échec est signalé plutôt que répété.

forms.update est réessayé même avec expectedUpdatedAt : une nouvelle tentative après une réponse perdue peut donc revenir en 409 version_conflict, parce que la première tentative est passée. Relisez le formulaire avant de réessayer.

client.raw.request réessaie un GET et envoie tout le reste une seule fois, sauf si vous passez repeatable=True.

Le backoff

  • Borné par max_retries sur le client, avec deux tentatives supplémentaires par défaut.
  • Uniquement après un échec réseau ou un 408, 500, 502, 503 ou 504. Un 429 n'est réessayé que s'il porte un Retry-After, et cette API n'en envoie pas : une limite de débit lève donc une exception immédiatement. Tout autre statut lève une exception sur-le-champ.
  • Exponentiel, d'une demi-seconde jusqu'à huit, avec du jitter, pour qu'un parc de machines ne se resynchronise pas au moment de la reprise.
  • Cadencé par Retry-After dans l'une ou l'autre de ses formes, delay-seconds et HTTP-date. Quand le serveur indique une attente, le client patiente exactement ce temps-là au lieu d'appliquer son backoff.
  • Un serveur qui demande plus d'une minute est compris comme disant au client de s'arrêter plutôt que d'attendre : l'erreur est donc levée avec retry_after_seconds dessus. Revenir plus tôt que demandé, ce n'est pas respecter la consigne.
  • timeout borne chaque tentative : un appel qui utilise ses deux nouvelles tentatives peut donc durer trois timeouts, plus les attentes entre eux.
  • L'annulation d'un appel d'AsyncOpenEmail n'est jamais suivie d'une nouvelle tentative. L'annulation se propage immédiatement, depuis la requête ou depuis l'attente qui précède la tentative suivante.