Content

How to Connect Stripe to an AI Agent Without Double Refunds

There are at least eight different ways to create a refund in Stripe's API, and an agent given 'access to Stripe' will confidently pick one of them. Here is how to make sure it picks the one you meant, once.

IntegrationsOct 28, 2026

Key takeaways

  • -Stripe's API has several distinct refund operations covering charges, customer balances and terminal payments. They are not interchangeable.
  • -Amounts are in the smallest currency unit, so 500 means five dollars, and a model that reads a human amount literally refunds a hundredth of it.
  • -A retried write without an idempotency key is a second refund, not a second attempt.
  • -Allow the one operation you mean and gate it on amount, rather than granting a scope and hoping.
  • -Test keys and live keys look alike in a config file and do not in a bank statement.

Most integration guides treat payments like any other API. They are not like any other API, for one reason: everything an agent does wrong here is visible on somebody's bank statement.

So this page is about the four specific things that go wrong, in the order they tend to happen.

There is no single "refund" operation

This is the one that surprises people, and it is worth being concrete.

Stripe does not have one refund endpoint. Searching our catalog for "create a refund for a charge" returns several distinct operations, and the summaries are not synonyms:

OperationWhat it actually refunds
stripe.refund.create3A charge or payment intent
stripe.refund.create4A customer charge or payment intent
stripe.refund.create6A customer balance
stripe.refund.create8A card charge taken through a terminal

An agent handed "access to Stripe" and asked to refund something will choose one. It will choose confidently. If it refunds a customer balance when you meant a charge, the money moves in a way nobody expected and the ledger stops matching.

This is why "access to Stripe" is not a permission. It is a category containing operations with materially different consequences. A reviewable permission is one line long: this agent may call stripe.refund.create3 and nothing else.

npm install -g swytchcode
swy login
swy init --mode=sandbox

swy get stripe
swy discover "create a refund for a charge" --project stripe
swy info stripe.refund.create3          # read the real parameters before allowing it
swy add method stripe.refund.create3    # allow exactly this one
swy auth connect stripe

One ordering detail: swy discover searches the whole catalog, but swy info only works on a provider you have already fetched. Run swy get stripe first or info will tell you a real operation does not exist.

Only methods added with swy add method can run. If the model reaches for the terminal refund, or for a payout, the call fails at the runtime before a request reaches Stripe. Not a prompt it can argue with — an absent capability.

Amounts are in the smallest currency unit

Stripe takes 500 to mean five dollars, not five hundred.

Models read human text. A support message saying "refund her $500" and an amount field expecting 50000 is a one-hundredfold error waiting to happen in either direction, and the direction varies. Sometimes the agent refunds $5 and the customer complains again. Sometimes it refunds $50,000.

Two defences, and you want both.

Read the schema rather than guessing it. swy info stripe.refund.create3 tells you the field, its type and its units, which is the difference between an agent working from the actual contract and one working from a plausible memory of it.

Then put the threshold where it cannot be talked around:

{
  "id": "big-refunds",
  "target": ["stripe.refund.create3"],
  "when": { "field": "amount", "operator": ">", "value": 50000 },
  "action": {
    "type": "REQUIRES_APPROVAL",
    "message": "Refunds over 500.00 need approval"
  },
  "approval_timeout": "2h"
}

Note what this catches. It is not primarily there for a malicious instruction. It is there for the unit mistake, which is far more common, and it catches that one on the first occurrence rather than the fiftieth. A rule in version control, checked when the call runs — the argument is in a prompt is not a policy, the implementation in policy-as-code for AI agents, and the approval mechanics in human-in-the-loop approval for AI agents.

A retry is a second refund

This is the failure that looks like an attack at midnight and is not.

Agent platforms retry failed calls. A refund request that times out has very likely succeeded at Stripe's end and failed on the way back. If the retry is a brand new request, the customer gets refunded twice and your reconciliation breaks silently.

Stripe supports idempotency keys for exactly this. The question to ask of whatever executes your agent's calls is narrow and specific: does a retry carry the same idempotency key as the original attempt, or does it generate a new one? A great many agent stacks do the second thing without documenting it.

If the answer is "I do not know," that is the thing to find out before the agent touches money. The full version is in idempotency explained, and the broader retry problem in logging every API call an agent makes.

Test keys and live keys look identical in a config file

They differ by a few characters in the middle of a long string, which is not a difference a tired person notices at 6pm.

The structural fix is to not have the key in your project at all. With a brokered setup the credential lives in ~/.swytchcode/credentials.db, outside the repository, selected by the mode you initialised with:

swy init --mode=sandbox     # set once, at init

Mode is set at init and lives in .swytchcode/tooling.json. It is not an environment variable the agent can read or a flag someone passes by hand, which removes the class of accident where a local script runs against live. The reasoning is in credential brokering for AI agents.

And because the credential is never in the model's context, a prompt injection that asks the agent to reveal its Stripe key gets nothing. It is not being withheld; it is not there.

What a review will ask

Four questions, and they take a minute each.

Which exact operations can it call? The answer should be a list you can read aloud.

What is the largest single amount it can move without a human? If the answer is "unlimited," that is the finding.

Does a retry move money twice? Test it. Do not reason about it.

Can you reconstruct what it did?

swy audit network   # what ran, with status and timing
swy audit policy    # what was denied or held, and by which rule

The refused and held calls matter as much as the successful ones here, and they are exactly what an agent's own chat history omits.

The wider checklist, for the assistant rather than just the payment path, is in a security checklist before connecting any AI assistant to company data.

FAQ

How do I connect Stripe to an AI agent?

Fetch the provider, find the specific operation you need, read its schema, allow that one operation, and connect the credential outside your project. Concretely: swy get stripe, swy discover "create a refund for a charge" --project stripe, swy info stripe.refund.create3, swy add method stripe.refund.create3, swy auth connect stripe. Granting broader Stripe access instead means the agent can pick operations you never considered.

Is it safe to let an AI agent issue refunds?

With two things in place, yes. An allow list containing only the refund operation you mean, and a rule that holds anything above a threshold for a human. Without those, the risk is not mainly a malicious instruction, it is a unit mistake or the wrong refund operation being chosen confidently.

Why did my AI agent refund the wrong amount?

Almost always because Stripe amounts are in the smallest currency unit and the model read a human figure literally. 500 is five dollars. Read the schema with swy info rather than assuming, and set an amount threshold so the first occurrence is caught rather than the fiftieth.

How do I stop duplicate charges or refunds from retries?

Make sure retries reuse the original idempotency key rather than generating a new one. Stripe supports this directly; many agent stacks do not use it consistently. A timed-out request has usually succeeded on Stripe's side, so a fresh retry is a second real refund rather than a second attempt.

Can an AI agent see my Stripe secret key?

Not in a brokered setup. The credential stays in a local store outside your project and is injected when a call executes, so it never enters the model's context and a prompt injection asking for it finds nothing. If your key is in a .env file or a prompt, then yes, and that is the first thing to change.

How do I keep an agent on Stripe test mode?

Set the mode once at swy init, where it is recorded in .swytchcode/tooling.json rather than in an environment variable the agent or a stray script can change. That removes the accident where local work runs against live keys because the two look nearly identical in a config file.

What should I log for an agent that touches payments?

The operation as a stable identifier, who it acted for, the amount, the policy decision with the rule version, the status, and whether the response validated. Crucially, keep the denied and held calls in the same place as the successful ones, because those are the rows you will want when somebody asks what happened.

Setup commands come from the Swytchcode CLI docs, and the refund operation identifiers above were read from the live catalog. Stripe's own behaviour around minor units and idempotency keys is described from Stripe's API documentation rather than linked.

More content