Reintentos e idempotencia
Qué se reintenta, qué no se reintenta a propósito y por qué un envío reintentado no puede duplicarse.
Envíos
El cliente adjunta un Idempotency-Key a cada envío (emails.send, emails.send_batch, templates.send y broadcasts.send), generado una vez por **llamada** como oe- seguido de un UUID aleatorio, y reutilizado por los reintentos de esa llamada. La API reserva esa clave antes de despachar nada, así que un reintento reproduce el mensaje original en lugar de enviar uno segundo, mientras que dos llamadas deliberadas a send siguen enviando dos veces. Son intenciones distintas y siguen siendo distintas.
Pasa tu propio idempotency_key: para extender esa garantía entre procesos, de modo que un trabajo que se cayó y volvió a ejecutarse reproduzca sus envíos en lugar de repetirlos. Una reproducción responde con replayed a true y el mensaje almacenado tal como está ahora.
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]Dedúcela de aquello que hizo necesario el envío. Nunca de un reloj. Reutilizar una clave con un cuerpo distinto se rechaza con un 422 idempotency_key_reuse en lugar de reproducirse en silencio. Una clave tiene de 1 a 255 caracteres entre letras, dígitos, _, ., : o -, y cualquier otra cosa da un 400 invalid_idempotency_key.
Todo lo demás
Todo GET se reintenta. Una escritura solo se reintenta cuando una segunda solicitud idéntica no puede significar nada distinto de la primera, y un envío cumple esa condición porque su clave de idempotencia convierte una repetición en una reproducción.
| Llamada | Se reintenta | Por qué |
|---|---|---|
| Todo GET | Sí | No cambia nada. |
| emails.send, emails.send_batch, templates.send, broadcasts.send | Sí | Una clave de idempotencia convierte una repetición en una reproducción. |
| emails.cancel, emails.reschedule, broadcasts.cancel, forms.pause, forms.resume, threads.restore | Sí | Una asignación pura de un estado con nombre. |
| threads.update, threads.trash | Sí | Una asignación de etiquetas. Aplicarla dos veces equivale a aplicarla una vez. |
| threads.snooze, threads.unsnooze | Sí | El instante de reactivación viaja en el cuerpo; no se deriva de la hora de llegada. |
| 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 | Sí | Una asignación pura de campos con nombre. |
| members.grant_address, members.grant_domain, rules.reorder, threads.reorder_notes | Sí | Una concesión es un upsert, y un orden se indica por completo. |
| templates.publish, forms.publish, imports.start | Sí | Publicar lo que ya está publicado, o iniciar una importación que ya ha empezado, lo devuelve sin cambios. |
| templates.preview, templates.render, broadcasts.preview, rules.test | Sí | Renderizan, cuentan o evalúan, y no escriben nada. |
| domains.verify, app_host.verify, senders.research | Sí | Una comprobación repetida no cambia nada salvo el momento en que se hizo. |
| 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 | Sí | Cada una declara el resultado final, así que una segunda llamada deja lo que dejó la primera. |
| audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_address | Sí | Una repetición encuentra ya hecho el trabajo de la primera llamada y lo informa en lugar de hacerlo dos veces. |
| contacts.set_photo, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate, domains.set_address_photo, imports.upload_chunk | Sí | Los bytes enviados de nuevo reemplazan lo que guardó el primer intento. |
| drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.upload | No | Un reintento deja dos objetos. |
| drafts.update | No | Lee el id del resultado de cada escritura en lugar de reutilizar el que enviaste. |
| drafts.delete, labels.delete, webhooks.delete, templates.delete, rules.delete, roles.delete, members.remove, members.revoke_address, temp_mail.delete, temp_mail.delete_message | No | Un reintento tras una respuesta perdida informa de un fallo en un trabajo que sí se completó. |
| webhooks.rotate_secret | No | Una segunda rotación invalida el secreto que devolvió el primer intento. |
| webhooks.test | No | Enviaría una segunda entrega sintética. |
| webhooks.replay_delivery | No | Enviaría el evento a tu receptor por segunda vez. |
| emails.translate, emails.compose, emails.rewrite, emails.suggest_subject | No | Cada una consume llamadas al modelo, así que un reintento tras una solicitud sin respuesta paga dos veces la misma respuesta. |
| security.begin_step_up, security.verify_step_up | No | Un reintento podría enviar un segundo correo o gastar un segundo intento del código. |
| Cualquier otra llamada que no sea un GET | No | Se envía una sola vez, y un fallo se informa en lugar de repetirse. |
El backoff
- Limitado por
max_retries:en el cliente, con dos intentos adicionales por defecto.max_retries: 0desactiva los reintentos. - Solo tras un fallo de red o un
408,500,502,503o504. Un429se reintenta únicamente cuando lleva unRetry-After, y esta API no lo envía, así que un límite de tasa lanza un error de inmediato. Cualquier otro status lanza al instante. - Exponencial, desde medio segundo hasta ocho, con jitter: cada espera es un punto aleatorio entre la mitad de ese tope y el tope entero, para que una flota no se resincronice al recuperarse el servicio.
- Regulado por
Retry-Afteren cualquiera de sus formas, delay-seconds y HTTP-date. Cuando el servidor indica una espera, el cliente espera exactamente ese tiempo en lugar de aplicar backoff. - Si el servidor pide más de un minuto, se interpreta como una orden de detenerse y no de esperar, así que el error se lanza con
retry_after_secondsincluido. Volver antes de lo que pidió no es respetarlo. - Un tiempo de espera agotado es un fallo de red como cualquier otro, así que una llamada que se puede repetir sin riesgo se vuelve a intentar tras uno, y
timeout:se aplica de nuevo a cada intento. - Las esperas son llamadas a
sleepdentro de la llamada, así que el hilo que llama también espera, y la llamada solo devuelve o lanza una vez terminado su último intento.