# Wallet

The wallet is the payment source for every order. It has separate USD, EUR and GBP balances. The
API never auto-converts a wallet balance to cover an order in another currency.

```http
GET /v1/wallet
```

Required scope: `wallet:read`.

## Balances

```json
{
  "currencyMode": "native",
  "wallet": [
    { "currency": "USD", "postedMinor": 10000, "heldMinor": 0, "availableMinor": 10000 },
    { "currency": "EUR", "postedMinor": 0, "heldMinor": 0, "availableMinor": 0 },
    { "currency": "GBP", "postedMinor": 0, "heldMinor": 0, "availableMinor": 0 }
  ]
}
```

| Field | Meaning |
| --- | --- |
| `postedMinor` | Credited funds, including completed funding and refunds, minus completed debits. |
| `heldMinor` | Funds reserved by an operation. Current wallet orders debit immediately, so this is normally 0. |
| `availableMinor` | `postedMinor - heldMinor`. The order check uses this value. |

Amounts are integer minor units. `currencyMode: native` means balances are held in their own
currency; no conversion is applied at read time.

## Paying for an order

An order is paid from the wallet in the quote currency. There are three possible outcomes:

1. **Enough available credit**: the purchase is created, the wallet is locked, the debit and ledger
   entry are written in the same transaction, and the order returns `completed` or
   `processing`.
2. **Not enough credit**: the API returns `402 insufficient_funds`. No purchase, child order or
   debit is created.
3. **Price or stock moved**: the API returns `409 price_changed` or `409 out_of_stock`. No
   purchase or debit is created.

The target contract changes case 2 to `402 payment_required` with a `funding_url`. That
shape is **Planned** and not returned by every deployment yet. Handle both `insufficient_funds`
and `payment_required`.

## Fund the wallet

```http
POST /v1/wallet/funding
Idempotency-Key: fund-2026-001
```

Required scope: `wallet:fund`.

```json
{
  "amountMinor": 10000,
  "currency": "USD"
}
```

`amountMinor` must be an integer from 1 to 1,000,000. `currency` must be USD, EUR or GBP.
The response is a hosted Stripe Checkout URL and a payment operation id.

```json
{
  "paymentOperationId": "88888888-8888-4888-8888-888888888888",
  "checkoutUrl": "https://checkout.stripe.com/c/pay/REDACTED",
  "amountMinor": 10000,
  "currency": "USD"
}
```

The wallet is not credited when this response is returned. Credit is issued only after the signed
Stripe webhook confirms a paid session, exactly once per operation.

## Gating

Stripe funding is behind a deployment flag and a configured Stripe account. When it is disabled:

| Status | Code | Meaning |
| ---: | --- | --- |
| 501 | `capability_unavailable` | Funding is deliberately disabled. |
| 503 | `payment_pending` | Stripe is not configured. |
| 409 | `idempotency_conflict` | That funding key is already paid or already used. |

Sandbox credit is added by RoamWorks staff. A live partner receives written wallet terms before
funding is enabled. Direct card purchase and saved payment methods are not available through the
partner API.

## Funding rules

- Use a new `Idempotency-Key` per funding intent. Reuse it only for a retry of that intent.
- Do not create a second funding session for the same intent with a new key.
- Do not treat `checkoutUrl` as proof of payment. Poll the wallet or wait for the webhook.
- The API and the dashboard share the wallet. A dashboard top-up and an API top-up both appear in
  the same balances and ledger.
- The funding hook does not accept card data and never returns card data. The card form is hosted
  by Stripe.
- Treat `checkoutUrl` as a sensitive, short-lived URL. Do not post it in a public channel or
  send it to an unrelated service.

## Sandbox

Sandbox wallets are isolated from live. Use a sandbox key and the test-mode funding path when it
is enabled, or ask RoamWorks support for test credit. Sandbox balances and upstream fulfilment are
not real money or real inventory.

## Related

- [Orders](/docs/orders) for the debit path.
- [Webhooks](/docs/webhooks) for funding and order events.
- [Errors](/docs/errors) for 402 and 409 handling.
