{
  "question": "How do I stop two AI agents from paying the same vendor invoice twice?",
  "slug": "how-to-stop-two-ai-agents-from-paying-the-same-vendor-invoice-twice",
  "url": "https://www.rightshold.com/answers/how-to-stop-two-ai-agents-from-paying-the-same-vendor-invoice-twice",
  "short_answer": "An idempotency key stops the same payment being sent twice when one agent retries. It does not stop a second agent, or a second task, from paying the same vendor invoice as a new payment with its own key. That is the duplicate-bill problem: two processors each treat an invoice as unpaid and each pay it. Catch it before anyone authorizes spend, by claiming a normalized key of vendor id + invoice number + amount + currency in one shared register. Invoice numbers such as INV-001 collide across vendors, which is why the vendor id belongs in the key; amount and currency distinguish a corrected re-issue from a second payment of the same bill. If the key is already taken, refuse to pay and show the earlier claim. Stripe's Refunds API even lists duplicate as a reason you can attach when a second charge already went through. PCAOB AS 1105.11 defines completeness as the assertion that all transactions that should be presented are included, and AS 1105.10 tells the auditor to test the completeness of company-produced information: two unmarked payments of one invoice overstate expenses, and a claim recorded before authorize gives that population something to test. On Rightshold, record_action is keyed by client_event_id on your account's action stream: the same key and the same content come back as a duplicate of the first receipt; the same key with different content is rejected because that key is already bound to different event content. Both agents can attempt the claim with a deterministic client_event_id such as inv:<vendor>:<invoice>:<amount_minor>:<currency> before calling authorize_spend. authorize_spend does not deduplicate by reference — a second call still counts toward the cap — so the claim has to happen first. This page is about two agents (or two tasks) paying the same vendor bill. Stopping one agent from paying twice when it retries, binding a receipt to an idempotency key and reconciling before replay, reserving a shared spend pool, and stopping one agent from both creating and approving a payment are covered on separate pages.",
  "checklist": [
    "Write the normalization rules down before any agent pays: use your internal vendor id (not the display name), a single rule for trimming and casing invoice numbers, amounts as integer minor units, and currency as an ISO 4217 code.",
    "Build the claim key as vendor + invoice number + amount + currency. Keep it within 160 characters of letters, digits, periods, underscores, colons, slashes, at signs or hyphens, which is what client_event_id allows.",
    "Put the claim in one place every agent's payment path goes through: the same Rightshold account's action stream, or your own invoice register with an atomic insert on that key.",
    "Before authorize_spend, record the claim with record_action (action_type invoice.claimed, client_event_id set to the key) using a canonical body that does not name which agent asked, so two agents sending the same claim look identical. If the receipt comes back with duplicate: true, the invoice was already claimed — do not pay. If the call is rejected because the key is bound to different content, stop and inspect; do not mint a new key.",
    "Only if the claim is new, call authorize_spend and put the same key in reference so the spend receipt points at the claim. authorize_spend does not treat reference as a dedupe key; each new allow counts toward the daily cap.",
    "After the payment succeeds, record payment.sent under a new client_event_id (for example pay:<claim-key>) that names the claim, the spend receipt id and the provider payment id.",
    "If you allow partial payments against one invoice, drop amount from the uniqueness key and track the remaining balance in your register. When amount stays in the key, two identical full-amount payments of the same invoice are the duplicates you catch; two legitimate partials of different amounts will not collide.",
    "If a duplicate payment already went out, refund it on the rail (Stripe's Refunds API accepts reason=duplicate) and post a linked reversing entry for the extra payment — don't delete that record. How to record a void or reversal of an AI agent's payment on an append-only ledger covers that pair."
  ],
  "sections": [
    {
      "heading": "Why two agents paying one invoice is not a retry",
      "points": [
        "A retry is one intended payment that may be sent more than once (a timeout, a crashed worker, a framework replaying a step). The fix is one idempotency key created before the first attempt and reused unchanged, which is covered on the page about stopping an agent from paying twice when it retries, and on the page about binding a payment receipt to its trigger, policy and idempotency key before replay.",
        "A duplicate bill is a different intended payment for the same vendor invoice: a second agent, a second task, or a person and an agent each see the invoice as unpaid and each create their own payment with their own idempotency key. The payment provider happily charges twice, because each request looks new.",
        "That is why the check has to run on the invoice, not on the payment attempt, and why it has to run before authorize_spend. Two allows against the same invoice, each with a different reference, will both pass a spend cap that only looks at amount and merchant."
      ]
    },
    {
      "heading": "Dedupe on vendor, invoice number, amount and currency before authorize",
      "points": [
        "Vendor id first. Many vendors number invoices from 1 or INV-001, so invoice number alone collides. Use the identifier from your vendor master, normalized the same way every time (lowercase internal id, not 'ACME Inc.' vs 'Acme, Inc.').",
        "Invoice number next, under one written rule for spaces, dashes and case, so 'INV 4471' and 'inv-4471' don't slip past each other unless you meant them to be different.",
        "Amount in minor units and currency (ISO 4217) last. Together they separate a corrected re-issue at a new total from a second payment of the same bill. If you accept partial payments, take amount out of the uniqueness key and hold a remaining balance instead.",
        "The claim itself has to be atomic: one insert that fails if the key exists, not a read-then-write. On Rightshold, record_action with a deterministic client_event_id is that insert for agents that share the account's action stream. Two identical claim bodies come back as duplicate: true. A different body under the same key is rejected. Agents that record to different accounts or streams do not share that key space, so they still need your own register in front."
      ]
    },
    {
      "heading": "What Rightshold does and doesn't do for invoice deduplication",
      "points": [
        "record_action keys events by client_event_id on your account's private stream and will not bind that key to different content. authorize_spend checks one credential's spending policy and records a signed allow or deny; it does not look up other agents' payments or treat reference as unique. An allow counts toward the daily cap when it is recorded.",
        "Rightshold does not hold a vendor master, parse invoice PDFs, talk to your accounts-payable system or refund a duplicate charge. Normalization, the shared register (if agents don't share one stream) and the refund of a payment that already went out are steps you run. Not accounting or legal advice."
      ]
    }
  ],
  "checks": [],
  "ask_rightshold": {
    "start": "https://www.rightshold.com/start?ref=seo",
    "mcp_endpoint": "https://www.rightshold.com/mcp",
    "ask": "An agent wants to pay vendor acme-supplies invoice 4471 for 150.00 USD. The invoice claim key is free. Check this purchase against my spending policy and record the decision, with that invoice key as the reference.",
    "mcp_call": {
      "tool": "authorize_spend",
      "arguments": {
        "credential_id": "crd_YOUR_AGENT_CREDENTIAL",
        "amount_minor": 15000,
        "currency": "USD",
        "merchant_domain": "api.acme-supplies.example",
        "reference": "inv:acme-supplies:4471:15000:USD"
      }
    },
    "also": [
      "Needs your API key. authorize_spend records a signed allow or deny against the credential's policy and never moves money. It does not deduplicate by reference, so call it only after the invoice claim succeeds.",
      "Record the claim first with record_action: action_type invoice.claimed and client_event_id set to the same key (inv:<vendor>:<invoice>:<amount_minor>:<currency>). A duplicate: true receipt means another agent already claimed it; a conflict means the key is bound to different content.",
      "verify_spend_authorization is free and needs no key: confirm the receipt is genuine and an allow before the payment step pays.",
      "If a duplicate payment already went out, refund it with your provider (Stripe accepts reason=duplicate) and record a linked reversal with record_action under a new client_event_id."
    ],
    "scope": "Rightshold records invoice-claim events you send and signs spend allow/deny decisions per agent credential. It does not parse invoices, keep a vendor master, treat authorize_spend's reference as a unique invoice key, refund duplicate charges or connect to your accounts-payable system. Normalization, the shared claim and any refund are steps you run. Not accounting, tax or legal advice."
  },
  "sources": [
    {
      "title": "Stripe API: Create a refund (reason may be duplicate, fraudulent or requested_by_customer)",
      "url": "https://docs.stripe.com/api/refunds/create"
    },
    {
      "title": "Stripe Docs: Refund and cancel payments",
      "url": "https://docs.stripe.com/refunds"
    },
    {
      "title": "PCAOB: AS 1105, Audit Evidence (paragraph .10, accuracy and completeness of company-produced information; paragraph .11, completeness assertion)",
      "url": "https://pcaobus.org/oversight/standards/auditing-standards/details/AS1105"
    },
    {
      "title": "Operations records on Rightshold (stable client_event_id for retries; reusing it with different contents is rejected)",
      "url": "https://www.rightshold.com/operations"
    },
    {
      "title": "Agent spending policies on Rightshold",
      "url": "https://www.rightshold.com/agent-spending-policies"
    }
  ],
  "related": [
    {
      "title": "How to stop an AI agent from paying twice when it retries",
      "url": "https://www.rightshold.com/answers/how-to-stop-an-ai-agent-from-paying-twice-when-it-retries"
    },
    {
      "title": "How to bind an agent payment receipt to its trigger, policy and idempotency key, and reconcile before replay",
      "url": "https://www.rightshold.com/answers/how-to-bind-an-agent-payment-receipt-to-its-trigger-policy-and-idempotency-key-and-reconcile-before-replay"
    },
    {
      "title": "How to reserve against a shared team spend pool so concurrent AI agents cannot overspend it",
      "url": "https://www.rightshold.com/answers/how-to-reserve-against-a-shared-team-spend-pool-so-concurrent-ai-agents-cannot-overspend-it"
    },
    {
      "title": "How to enforce segregation of duties so an AI agent cannot both create and approve a payment",
      "url": "https://www.rightshold.com/answers/how-to-enforce-segregation-of-duties-so-an-ai-agent-cannot-both-create-and-approve-a-payment"
    },
    {
      "title": "How to record a void or reversal of an AI agent's payment on an append-only ledger",
      "url": "https://www.rightshold.com/answers/how-to-record-a-void-or-reversal-of-an-ai-agents-payment-on-an-append-only-ledger"
    }
  ],
  "computed_from": "Curated guidance; license verdicts computed from Rightshold's license profiles at render time. Not legal advice.",
  "updated": "2026-11-08"
}