Repetições e idempotência
O que é repetido, o que deliberadamente não é, e porque é que um envio repetido não pode duplicar.
Envios
O cliente anexa uma Idempotency-Key a todos os envios (emails.send, emails.send_batch, templates.send e broadcasts.send), gerada uma vez por **chamada** como oe- seguido de um UUID aleatório, e reutilizada pelas repetições dessa chamada. A API reivindica essa chave antes de despachar seja o que for, por isso uma repetição reproduz a mensagem original em vez de enviar uma segunda, enquanto duas chamadas deliberadas a send continuam a enviar duas vezes. São intenções diferentes e assim se mantêm.
Passe a sua própria idempotency_key: para estender essa garantia entre processos, para que uma tarefa que tenha ido abaixo e voltado a correr reproduza os seus envios em vez de os repetir. Uma reprodução responde com replayed a true e com a mensagem guardada tal como está agora.
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]Derive-a do que tornou o envio necessário. Nunca de um relógio. Reutilizar uma chave com um corpo diferente é recusado com um 422 idempotency_key_reuse em vez de reproduzido silenciosamente. Uma chave tem de 1 a 255 caracteres entre letras, algarismos, _, ., : ou -, e qualquer outra coisa é um 400 invalid_idempotency_key.
Tudo o resto
Todos os GET são repetidos. Uma escrita só é repetida quando um segundo pedido idêntico não pode significar nada de diferente do primeiro, e um envio qualifica-se porque a sua chave de idempotência transforma uma repetição numa reprodução.
| Chamada | Repetida | Porquê |
|---|---|---|
| Todos os GET | Sim | Nada muda. |
| emails.send, emails.send_batch, templates.send, broadcasts.send | Sim | Uma chave de idempotência torna uma repetição numa reprodução. |
| emails.cancel, emails.reschedule, broadcasts.cancel, forms.pause, forms.resume, threads.restore | Sim | Uma definição pura de um estado nomeado. |
| threads.update, threads.trash | Sim | Uma definição de etiqueta. Aplicá-la duas vezes é aplicá-la uma vez. |
| threads.snooze, threads.unsnooze | Sim | O instante de despertar está no corpo, não é derivado da hora de chegada. |
| 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 | Sim | Uma definição pura de campos nomeados. |
| members.grant_address, members.grant_domain, rules.reorder, threads.reorder_notes | Sim | Uma concessão é um upsert, e uma ordem é indicada por inteiro. |
| templates.publish, forms.publish, imports.start | Sim | Publicar o que já está publicado, ou iniciar uma importação que já começou, devolve-o inalterado. |
| templates.preview, templates.render, broadcasts.preview, rules.test | Sim | Renderizam, contam ou avaliam, e não escrevem nada. |
| domains.verify, app_host.verify, senders.research | Sim | Uma verificação repetida não altera nada além da hora em que foi feita. |
| 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 | Sim | Cada uma indica o resultado final, por isso uma segunda chamada deixa o que a primeira deixou. |
| audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_address | Sim | Uma repetição encontra o trabalho da primeira chamada já feito e reporta-o em vez de o fazer duas vezes. |
| contacts.set_photo, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate, domains.set_address_photo, imports.upload_chunk | Sim | Os bytes enviados de novo substituem o que a primeira tentativa guardou. |
| drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.upload | Não | Uma repetição deixa dois objetos. |
| drafts.update | Não | Leia o id do resultado de cada escrita em vez de reutilizar o que enviou. |
| drafts.delete, labels.delete, webhooks.delete, templates.delete, rules.delete, roles.delete, members.remove, members.revoke_address, temp_mail.delete, temp_mail.delete_message | Não | Uma repetição depois de uma resposta perdida reporta falha para trabalho que teve sucesso. |
| webhooks.rotate_secret | Não | Uma segunda rotação invalida o segredo que a primeira tentativa devolveu. |
| webhooks.test | Não | Enviaria uma segunda entrega sintética. |
| webhooks.replay_delivery | Não | Enviaria o evento ao seu recetor uma segunda vez. |
| emails.translate, emails.compose, emails.rewrite, emails.suggest_subject | Não | Cada uma gasta chamadas ao modelo, por isso uma repetição após um pedido sem resposta compra a mesma resposta duas vezes. |
| security.begin_step_up, security.verify_step_up | Não | Uma repetição poderia enviar um segundo email ou gastar uma segunda tentativa do código. |
| Qualquer outra chamada que não seja um GET | Não | Enviada uma vez, e uma falha é reportada em vez de repetida. |
O backoff
- Limitado por
max_retries:no cliente, com duas tentativas extra por omissão.max_retries: 0desativa as repetições. - Apenas depois de uma falha de rede ou de um
408,500,502,503ou504. Um429só é repetido quando traz umRetry-After, e esta API não envia nenhum, por isso um limite de taxa lança exceção de imediato. Qualquer outro estado lança logo. - Exponencial, de meio segundo até oito, com jitter: cada espera é um ponto aleatório entre metade desse limite e o limite inteiro, para que uma frota não se ressincronize na recuperação.
- Ritmado pelo
Retry-Afterem qualquer uma das suas formas, delay-seconds e HTTP-date. Quando o servidor indica uma espera, o cliente espera exatamente esse tempo em vez de recuar progressivamente. - Um servidor a pedir mais de um minuto é tratado como estando a dizer ao cliente para parar e não para esperar, por isso o erro é lançado com
retry_after_secondsnele. Voltar mais cedo do que o pedido não é respeitá-lo. - Um timeout é uma falha de rede como qualquer outra, por isso uma chamada que é seguro repetir é tentada de novo depois de um, e
timeout:aplica-se de novo a cada tentativa. - As esperas são chamadas a
sleepdentro da chamada, por isso a thread que chama também espera, e a chamada só devolve ou lança uma exceção depois de terminada a sua última tentativa.