lupin.Open console
Email

Prevent duplicate agent emails when a request times out

Persist a send identity before submission and separate definitive failures from uncertain provider outcomes.

The dangerous gap is after submission

An HTTP timeout does not tell you whether an email provider accepted a message. The server may have completed the action while the response was lost, or it may have stopped before contacting the provider. Retrying with a new identity can create a second message in the first case. Build your application around one durable attempt for one approved communication. Save its sender inbox, recipients, content revision and operation key before the network request. A process restart should restore that same attempt rather than generate a fresh random identity.

Use the correct deduplication mechanism

For POST /v0/emails/inboxes/:inbox_id/emails/send, provide an Idempotency-Key header. MCP sends, replies, reply-all, forwards and draft sends require an explicit idempotency_key argument. For resource creation such as drafts and inboxes, client_id serves a separate creation-deduplication purpose. Registration has its own stronger secret rules and must not reuse a mail-send key. Keep these identities in typed records so a general retry helper does not accidentally attach a registration recovery secret to ordinary messages or assume every create endpoint uses the same header.

Freeze the authorized request

A retry must preserve the same action and payload. If the user changes recipients or content, treat that as a new proposal with a new approval and attempt identity, after resolving any earlier uncertain send. Do not depend on every endpoint detecting a changed body under an old key in exactly the same way. Your application should enforce the invariant first. Keep an immutable digest or stored snapshot so a worker cannot reinterpret send the update after fetching a newer draft and silently send different material under the old approval.

Classify failures before deciding

Validation errors and permission denials require fixing the request or obtaining authority; they are not transient delivery problems. mail_rejected means the provider rejected the request before accepting delivery and the spending reservation was released. Unknown outcomes must remain unresolved until operation evidence or provider reconciliation establishes what happened. A recorded accepted message can recover local state without repeating the external action. Retain pending reservations and the original key where the system reports uncertainty instead of clearing state merely to make another request pass.

Reconcile before declaring success

Store the returned message ID and known state, then show the human what is actually established. email.sent means provider acceptance, not that a recipient read the email. If no conclusive result exists, say the outcome is uncertain and stop issuing new sends for that action. Your next implementation step is a local fault-injection test that loses the response after an accepted send and restarts the worker. Assert that the same attempt is recovered, no second provider call occurs and the user-facing status never overstates the available evidence.

Keep a local action ledger

Record authorized_at, attempt identity, payload digest and observed outcome in your own application state. These fields answer different questions from a message label: what was approved, which execution it authorized and what evidence returned. Do not delete the pending record after a timeout just to unclog the queue; that removes the information needed for duplicate-safe recovery.

Sources & verification

Reviewed against checked-in Lupin API handlers and documentation. Example requests were not executed against live providers.

Prepared with AI assistance by Lupin editorial. How we research and maintain these guides.

Put it into practice

Give your agent the right tools.

Review the API, workspace requirements and permissions before enabling a workflow.

Explore email Register an agent