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.
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.
| Appel | Réessayé | Pourquoi |
|---|---|---|
| Toute lecture | Oui | Rien ne change. |
| emails.send, emails.send_batch, templates.send et broadcasts.send | Oui | Une 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_invitation | Oui | Une simple affectation d'un état nommé. |
| threads.update, threads.trash et threads.restore | Oui | Une affectation de libellés. L'appliquer deux fois revient à l'appliquer une fois. |
| threads.snooze, threads.unsnooze | Oui | L'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_active | Oui | Une simple affectation de champs nommés. |
| members.grant_address, members.grant_domain, rules.reorder et threads.reorder_notes | Oui | L'octroi est un upsert, et l'ordre est énoncé en entier. |
| templates.publish, imports.start | Oui | Publier 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.test | Oui | Elles rendent, comptent ou évaluent, et n'écrivent rien. |
| domains.verify, app_host.verify | Oui | Une 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.move | Oui | Chacun é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.research | Oui | Une 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_photo | Oui | Les 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.design | Non | Un réessai laisse deux objets. |
| drafts.update | Non | Lisez 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_message | Non | Un réessai après une réponse perdue signale un échec pour un travail qui a réussi. |
| webhooks.rotate_secret | Non | Une seconde rotation invalide le secret que la première tentative a renvoyé. |
| webhooks.test | Non | Cela enverrait une seconde livraison synthétique. |
| webhooks.replay_delivery | Non | Il enverrait l'événement une seconde fois à votre destinataire. |
| emails.translate | Non | Cela 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_subject | Non | Chaque tentative consomme une action d'IA de plus et revient avec une réponse différente. |
| Tout autre appel | Non | Envoyé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_retriessur le client, avec deux tentatives supplémentaires par défaut. - Uniquement après un échec réseau ou un
408,500,502,503ou504. Un429n'est réessayé que s'il porte unRetry-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-Afterdans 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_secondsdessus. Revenir plus tôt que demandé, ce n'est pas respecter la consigne. timeoutborne 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'
AsyncOpenEmailn'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.