Payment Idempotency for AI Agents: Prevent Duplicate Execution
Prevent duplicate AI agent payments with canonical request digests, atomic idempotency reservations, uncertain-state reconciliation and retry tests.

Payment idempotency for AI agents means that retries of the same intended transaction produce one protected operation. It requires a stable business key, canonical request digest, atomic reservation and provider reconciliation. A replay nonce prevents credential reuse; it does not answer whether a timed-out payment may be submitted again.
The agentic payments guide covers the full control path. Idempotency becomes its own engineering problem once two workers race or a provider accepts a request and drops the response.
Reserve intent before calling the provider
type PaymentReservation = {
tenantId: string;
idempotencyKey: string;
requestDigest: string;
state: 'reserved' | 'submitted' | 'accepted' | 'rejected' | 'uncertain';
providerReference?: string;
};
async function reservePayment(input: PaymentInput) {
const prior = await store.insertIfAbsent({
tenantId: input.tenantId,
idempotencyKey: input.idempotencyKey,
requestDigest: digest(input),
state: 'reserved',
});
if (prior.requestDigest !== digest(input)) throw new Error('KEY_REUSED_FOR_DIFFERENT_REQUEST');
return prior;
}
The insert and comparison must be atomic. Include tenant identity in the key. If the same key arrives with different amount, currency, supplier or destination, reject it instead of returning the earlier result.
Distinguish replay and retry
| Mechanism | Protects against | Typical key |
|---|---|---|
| proof replay store | reuse of signed credential or proof | key, audience, nonce |
| authority usage counter | exceeding delegated uses | grant ID and use number |
| domain idempotency | duplicate business operation | tenant and transaction intent |
| provider idempotency | duplicate provider submission | provider-scoped request key |
Do not collapse these stores. Their lifetimes and recovery behavior differ.
Treat timeouts as unknown
After sending the provider request, persist submitted before interpreting the response. If the connection drops, mark uncertain and query by the provider idempotency key or external reference. Never ask the model whether it thinks the payment probably went through.
Test a provider that accepts the payment and loses the response, two workers racing before dispatch, a database failover after reservation, and a retry after process restart. The acceptance criterion is one provider-side payment and a recoverable local state.
Choose and retain the key carefully
The idempotency key should represent business intent, not one HTTP attempt. Generate it before the first dispatch and keep it stable through agent replanning, process restarts and human review. If a changed request is genuinely a new transaction, issue a new key and preserve the link to the earlier abandoned or rejected intent.
Retain the reservation longer than the provider can accept, settle or report the transaction. Expiring it after a short API timeout recreates the duplicate risk. The exact period depends on the rail and business process, so document it with payments and records owners.
Do not return an old successful response when the caller presents the same key with a different digest. That hides a programming or substitution error. Return a conflict and require the caller to inspect the existing transaction.
RFC 9110 defines HTTP method semantics, including idempotent methods, but a payment operation's business idempotency still depends on the application and provider contract.
The reconciliation state machine specifies late outcomes. Accounts payable without duplicate payments applies the pattern to invoices. The OATI Receipt path covers evidence concepts in developer preview.
Idempotency reduces duplicate execution; it does not guarantee exactly-once delivery across every system. Document provider semantics and independently review the payment integration.
Idempotency design questions
Who creates the business key?
Create it in a trusted transaction service when the payment intent becomes structured, before the first provider call. Do not let a language model freely regenerate it on each plan. Tie it to tenant and business operation, expose it to the agent as an opaque value and retain it through retries and reconciliation.
What if the provider has no idempotency support?
Use an internal atomic reservation and query the provider or system of record before retry. Some rails expose a client reference that can support reconciliation even without strict idempotency. If the provider cannot determine whether a timed-out request executed, the workflow may require manual resolution rather than automated resubmission.
Should rejected payments keep the key?
Preserve the record and provider semantics. A definitive rejection may allow a corrected request under a new business key, linked to the old one. Reusing the same key for changed amount or destination is ambiguous and should be rejected. Distinguish validation rejection before dispatch from provider rejection after submission.
How are concurrent agents handled?
Both must reserve against the same tenant and business key in a shared atomic store. A process-local mutex is insufficient across replicas. Return the existing transaction state to the losing caller without starting another action. Test crashes between reservation, submission and state persistence.
What should happen to an uncertain reservation?
Keep it active until reconciliation proves a terminal result or an accountable procedure resolves it. Releasing it on timeout can permit a duplicate. Track age and next query time, and escalate when the rail's normal observation window expires.
How is idempotency audited?
Record key, request digest, reservation transitions, provider reference and every retry without sensitive credential data. Sample duplicate attempts and confirm one provider operation. Monitor key conflicts because they can indicate a caller bug, tenant-key omission or attempted substitution.
Make restart behavior part of the acceptance test. Reserve a transaction, stop the worker after the provider receives the request, then start a different replica with no process memory. It should recover the same key and request digest, inspect durable state and reconcile before attempting anything new. Repeat with the crash one instruction earlier, before dispatch. These two cases look nearly identical in an application log but require different recovery. The durable reservation and provider observation must tell them apart.
Monitor reservations that never reach submission as well as uncertain provider calls. They may indicate worker failure, dead letters or a transaction that should release capacity. Recovery rules need to distinguish them without reusing the key for changed intent.
Report these states separately. A growing pre-dispatch backlog needs a different response from payments awaiting provider confirmation, even though both can appear as incomplete transactions.