> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tallyforagents.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Make payment retries safe.

Tally supports idempotency keys on `POST /v1/payments` so retries can't accidentally double-spend. The semantics are simple: same key + same (account, mode) returns the original payment, without re-submitting anything to the chain.

## The header... is a body field

Tally takes the idempotency key in the request body, not in an `Idempotency-Key` header (unlike Stripe). This matches the SDK's `PaymentCreateInput` shape:

```json theme={null}
POST /v1/payments
{
  "agent_id": "research-bot",
  "wallet": "0x7a3f...",
  "to": "0xC0fee...",
  "amount_usdc": "4.50",
  "idempotency_key": "invoice-2026-04-117"
}
```

The key is up to 64 characters and scoped to `(account, mode)`. The same key can be used in test mode and live mode independently, and across different accounts globally.

## Semantics

When `payments.create` (or `POST /v1/payments`) is called with an `idempotency_key`:

1. Tally looks up an existing transaction in this (account, mode) with the same key.
2. If one exists, it returns that record. **The body is not revalidated** — even if your retry passed a different `to` or `amount_usdc`, you get the original payment back as-written the first time.
3. If no record exists, the payment proceeds normally. Tally also forwards the key to Privy as `privy-idempotency-key` so the underlying signing call is itself idempotent on Privy's side.

In rare races where two requests with the same key arrive simultaneously, the database's unique-constraint catches the loser, and the loser fetches and returns the winner's record. Effectively serialized; you'll never see two on-chain transactions from one idempotency key.

## Choosing keys

Use deterministic strings derived from your own data:

* An invoice id: `invoice-2026-04-117`
* A workflow run id: `wf-run-7c4f-step-3`
* A `(user_id, operation)` tuple: `user-882-summary-paid-2026-05-18`

**Avoid:**

* Timestamps (`tx-1715990400`) — they change on retry, defeating the purpose.
* Fresh UUIDs (`tx-${randomUUID()}`) — same problem.
* The agent id alone (`research-bot`) — too coarse; you'll deduplicate unrelated payments.

A good rule: the key should be a deterministic hash of the *intent*. If two requests represent the same intent, they should use the same key.

## When you don't pass a key

`idempotency_key` is optional. If you omit it:

* Each retry is treated as a fresh request.
* A network blip after the chain accepts the transaction but before the response reaches you can lead to a duplicate payment when you retry.

For agent payments — where the same "this user owes vendor X \$5" intent may be retried by your worker — always pass a key.

## When idempotency is the wrong answer

Idempotency keys are scoped to one (account, mode) and one key per intent. Patterns where they're insufficient:

* **Splitting a payment into installments.** Use distinct keys per installment (`invoice-2026-04-117-installment-1`, ...). Don't reuse the parent key.
* **A payment that legitimately replays a previous one.** Example: a user resubscribes, generating a *new* payment that happens to be identical to last month's. Use a fresh key — this isn't a retry, it's a new operation.

If you find yourself wanting to reuse a key for a different intent, that's a sign the key derivation is too coarse.

## In the SDK

The TypeScript SDK passes `idempotency_key` through to the body — no special handling required. See [SDK payments](/sdk/payments) for the type.
