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** sous la forme oe- suivi d'un UUID aléatoire, 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 restent différentes.
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. Un rejeu répond avec replayed à true et le message stocké dans son état actuel.
invoice_id = "inv_2026_09_4192" sent = client.emails.send( from: "[email protected]", to: "[email protected]", subject: "Your September invoice", text: "Invoice attached.", idempotency_key: "invoice:#{invoice_id}") puts sent[:id], sent[:replayed]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é avec un 422 idempotency_key_reuse au lieu d'être rejoué en silence. Une clé fait de 1 à 255 caractères parmi les lettres, les chiffres, _, ., : ou -, et tout le reste donne un 400 invalid_idempotency_key.
Tout le reste
Tout GET est réessayé. Une écriture ne l'est que lorsqu'une seconde requête identique ne peut rien signifier d'autre que la première, et un envoi entre dans ce cas parce que sa clé d'idempotence transforme une répétition en rejeu.
| Appel | Réessayé | Pourquoi |
|---|---|---|
| Tout GET | Oui | Rien ne change. |
| emails.send, emails.send_batch, templates.send, broadcasts.send | Oui | Une clé d'idempotence transforme une répétition en rejeu. |
| emails.cancel, emails.reschedule, broadcasts.cancel, forms.pause, forms.resume, threads.restore | Oui | Une simple affectation d'un état nommé. |
| threads.update, threads.trash | 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. |
| emails.update, labels.update, webhooks.update, settings.update, roles.update, members.update, domains.update, domains.update_address, domains.update_address_forward, contacts.update, audiences.update, keys.update, forms.update, branding.update, threads.update_note, chats.rename, account.set_email_notification, account.set_push_muted, app_host.set, workspaces.set_active | Oui | Une simple affectation de champs nommés. |
| members.grant_address, members.grant_domain, rules.reorder, threads.reorder_notes | Oui | Un octroi est un upsert, et un ordre est énoncé en entier. |
| templates.publish, forms.publish, imports.start | Oui | Publier ce qui est déjà publié, ou démarrer un import déjà démarré, renvoie l'élément inchangé. |
| templates.preview, templates.render, broadcasts.preview, rules.test | Oui | Elles rendent, comptent ou évaluent, et n'écrivent rien. |
| domains.verify, app_host.verify, senders.research | Oui | Une vérification répétée ne change rien d'autre que l'heure à laquelle elle a été faite. |
| contacts.save, contacts.set_audiences, contacts.remove_photo, contacts.block, contacts.unblock, contacts.delete_many, keys.revoke, files.revoke_link, files.revoke_all_links, app_host.delete, account.remove_photo, branding.remove_image, domains.remove_logo, domains.remove_logo_certificate, domains.remove_address_photo, account.accept_invitation, account.decline_invitation, forms.approve_submission, 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 | 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, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate, domains.set_address_photo, imports.upload_chunk | 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 | 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, 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, emails.compose, emails.rewrite, emails.suggest_subject | Non | Chacune 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. |
| security.begin_step_up, security.verify_step_up | Non | Un réessai pourrait envoyer un deuxième e-mail ou consommer un deuxième essai sur le code. |
| Tout autre appel qui n'est pas un GET | Non | Envoyée une seule fois ; un échec est signalé plutôt que répété. |
Le backoff
- Borné par
max_retries:sur le client, avec deux tentatives supplémentaires par défaut.max_retries: 0désactive les réessais. - 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 erreur immédiatement. Tout autre statut lève une erreur sur-le-champ. - Exponentiel, d'une demi-seconde jusqu'à huit, avec gigue : chaque attente est un point aléatoire entre la moitié de ce plafond et sa totalité, pour qu'une flotte ne se resynchronise pas au moment du rétablissement.
- 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. - Un dépassement de délai est un échec réseau comme un autre : un appel qui peut être répété sans risque est donc retenté après un tel échec, et
timeout:s'applique de nouveau à chaque tentative. - Les attentes sont des appels à
sleepà l'intérieur de l'appel : le thread appelant attend donc aussi, et l'appel ne retourne ou ne lève une erreur qu'une fois sa dernière tentative terminée.