# Wallets & payments

Live purchases are paid from the payer's own wallet at
[Perflo](https://perflo.ai). The payer is whoever the intent buys for: your
account buying for itself, or one of your [customers](/docs/api#customers).
Perflo holds their money, not OpenIntents, and the payer decides how much
their agents may spend.

Test keys never pay, so they never need a wallet: a test intent stops at the
payment step with a receipt preview.

## How a purchase is paid

1. **The agent reaches checkout.** The cart is filled and the shop shows its
   total.
2. **OpenIntents asks the payer's wallet for that total.** Perflo checks it
   against the payer's limits and balance, then loads the payer's virtual
   Mastercard with exactly that amount.
3. **The card is typed into the shop's form.** OpenIntents reveals the card
   once and types it in itself. It is kept in memory for that run only,
   never stored, and never shown to the AI model, your code or your agent.
4. **The order is placed** and the intent completes with a receipt.

A run that ends without placing the order cancels the request at Perflo, so
the money goes back to the wallet. Wallets pay in US dollars. A shop that
charges in another currency fails the intent with `currency_unsupported`,
and nothing is charged, unless the service converts for that currency: the
wallet is then asked for the dollars the shop's total comes to, with some
headroom for the card network's rate, and what the shop does not take goes
back to the wallet when the order settles. The intent's `max_amount` and
receipt stay in the shop's currency.

## Setting up a wallet

A payer sets up their wallet once, on our page at
`https://openintents.io/wallet/…`, with Perflo's own sign-in framed in it.
They:

- **connect** their Perflo account, signing in or making one (the email and
  name you gave the customer are filled in already),
- **set limits**: the most one purchase, and one month, may cost,
- **add money**, or turn on Perflo's automatic top-ups from a saved card,
- **confirm who they are**, only when Perflo asks for it (for larger
  purchases, say).

The page runs only the steps left and skips the ones done. You don't need to
send anyone there ahead of time: the first live purchase that needs the wallet
pauses and asks for it (see [below](#when-a-purchase-waits)). To have a
customer ready before their first purchase, send them a
[wallet link](#wallet-links) during your own sign-up.

Your account's own wallet is on the [Wallets](/dashboard/wallets) page of the
dashboard, with each customer's status beside it. Customers also see theirs in
the [payer portal](/docs/api#portal-sessions).

## When a purchase waits

When the payer has a step to take first, the intent pauses with
`"status": "requires_payment"` and a `payment` object:

```json
{
  "id": "int_7f11b0",
  "status": "requires_payment",
  "payment": {
    "reason": "add_money",
    "payment_url": "https://openintents.io/api/pay/pay_7f11b0",
    "merchant": "TAP Air Portugal",
    "total": 38900,
    "fee": 0,
    "currency": "usd",
    "expires_at": "2026-10-01T09:00:00Z"
  }
}
```

Send `payment_url` to the payer: show it in your app, post it in a chat, or
let the [MCP tools](/docs/mcp) hand it to the user. It opens the wallet page
with only the step this purchase needs. Once it's done the intent goes back to
`queued` by itself, with the same cart, and on to `completed`.

| `reason` | What the payer does |
| --- | --- |
| `connect_wallet` | Connects their wallet, or signs in to it again. |
| `add_money` | Adds money: the wallet is short of `total`. |
| `identity_check` | Confirms who they are: the wallet needs it first. |
| `wallet_processing` | Nothing yet: the wallet is still taking the money (from a card they saved, say). The link shows how it is going. |
| `over_limit` | Raises the limit they set: the purchase is over it. |
| `requested` | Approves the purchase: you asked for a link first (`require_payment_link`). |

Other values may appear, so treat an unknown `reason` like any other: send
the link. Links expire after 30 minutes; an expired or
[cancelled](/docs/api#cancel-an-intent) intent is free. If Perflo declines the
payment, the intent fails with `payment_declined` and Perflo's own message,
and nothing is charged.

## Wallet links

Onboard a customer ahead of their first purchase:

<Endpoint method="POST" path="/v1/customers/{id}/wallet_links" />

| Parameter | Type | |
| --- | --- | --- |
| `amount` | integer | A purchase, in cents, to have the wallet ready for: the page then also asks for any money or limits it would need. |
| `return_url` | string | A public `https` URL the page offers a way back to. |
| `ttl_seconds` | integer | How long the link works, 60 to 604800. Default 86400 (a day). |

```json
{
  "id": "wl_7c2e91d04b8a3f56",
  "object": "wallet_link",
  "customer": "cus_8f2a",
  "amount": 5000,
  "url": "https://openintents.io/wallet/wl_7c2e91d04b8a3f56",
  "status": "open",
  "expires_at": "2026-10-02T08:12:00Z"
}
```

`url` is for the customer alone. `status` is `open` until the wallet needs
nothing more (`completed`) or the link runs out (`expired`). Once they
connect, endpoints that name `wallet.connected` get a
[webhook](/docs/webhooks#events).

<Endpoint method="GET" path="/v1/customers/{id}/wallet?amount=5000" />

```json
{
  "object": "wallet",
  "customer": "cus_8f2a",
  "status": "funded",
  "next_step": { "kind": "identity_check", "amount": null }
}
```

Where the customer stands, and the one step left before a purchase of
`amount` could be paid (without `amount`: before they are set up at all).
`status` goes `not_connected`, `pending`, `connected`, `limits_set`, `funded`,
`identity_checked`, or is `not_needed` for test keys. `next_step.kind` is
`connect`, `set_limits`, `add_money` or `identity_check`, and `next_step` is
`null` when nothing is left. It never shows their balance or limits: those
are the customer's own.

## Keeping spend in check

| Control | Where | Effect |
| --- | --- | --- |
| Wallet limits | set by the payer, in their wallet | A purchase over them waits with `reason: over_limit` until the payer raises them. |
| `max_amount` | per intent | The intent fails with `over_max_amount` rather than spend more. |
| `require_payment_link` | per intent | The purchase always waits for the payer's approval link. |
| [Customer](/docs/api#customers) `limits` | set by you, per customer | `per_intent` caps one purchase and `monthly` what the payer's wallet purchases commit in a UTC calendar month (each counts its cart's total). They are checked before anything is paid: a purchase over one fails with `limit_exceeded`, and nothing the payer does in their wallet lifts them. |

## The fee

For now, purchases paid from a wallet carry no fee: `fee` is `0` on the
payment request and on the receipt. See [Pricing](/docs/pricing).

## What Perflo gets

To pay, Perflo receives the payer's ID with us, their email and name when we
have them, and for each purchase the shop's name and domain, the amount and a
short description. We never store the card's details, and Perflo never sees
what is in the cart beyond that description.

<Callout type="warning">
  Card details never reach your code, your agent or the model. The card is
  revealed once, typed into the shop's payment form by OpenIntents, and kept
  in memory for that run only.
</Callout>
