---
updatedAt: 2026-09-30T15:54:20.000Z
---

Fetch the complete documentation index at: https://docs.avvio.xyz/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Environments & sandbox

The key you send decides the environment. Base URL, paths and payloads are the
same in both, so the code you test is the code that runs.

## Base URL

```
https://api.avvio.xyz/business/api/v1
```

Every path in this documentation is relative to it.

## Environments

![Sandbox and production share the same API; only the key differs](/partner-assets/diagrams/sandbox-vs-production.svg)

|                             | Sandbox                                   | Production                                      |
| --------------------------- | ----------------------------------------- | ----------------------------------------------- |
| Key prefix                  | `avvio_test_…`                            | `avvio_live_…`                                  |
| Base URL and organization id | _the same_                               | _the same_                                      |
| Money                       | A virtual balance                         | A funded USD balance                            |
| Payment networks            | Simulated                                 | Real clearing houses and rails                  |
| Settlement                  | Seconds                                   | Hours to days, per corridor                     |
| Rates                       | Fixed and repeatable                      | Live, and they move between quoting and sending |
| Webhooks                    | Standard Webhooks, signed the same way; reach you through a public HTTPS tunnel (cloudflared, ngrok), not localhost | Standard Webhooks, HTTPS required |
| Card acceptor               | Simulated: no card, no charge, no processor | A real card acquirer                          |

There is no separate hostname and no environment flag. A test key addresses the
same organization id as your live key and resolves, server-side, to that
organization's sandbox. Switching is one variable:

```bash
export AVVIO_API_KEY=avvio_test_…                    # or avvio_live_…
```

## How the sandbox behaves

The sandbox runs the real API, with the same endpoints, schemas, validation,
corridor rules, error types and webhooks. It never touches a payment network.

- Payouts walk the live states, `pending → processing → completed`, and you may
  not see every state ([Track status & failures](/status/)).
- Rates are fixed, so assertions on exact amounts keep passing. Real rates move
  ([Go live](/going-live/)).
- Recipient validation is real, so a CLABE with a bad check digit is rejected.
  The one exception is the sample CLABE `012345678901234567`, accepted despite
  its check digit; it ends in `4567`, so it completes normally. Live keys
  accept no exceptions.
- Checkout webhooks are signed like live ones, carry `livemode: false`, and are
  opt-in, so name them when you register the endpoint.

## Sandbox balance

The sandbox balance is funded by an API call or from the dashboard, with no
transfer made. A live balance is funded by wire or USDC ([Fund your balance](/funding/)).

```bash
POST /payments/organizations/{orgId}/sandbox/fund
x-api-key: avvio_test_…
Idempotency-Key: <uuid>
Content-Type: application/json

{
  "amount": "10000.00"
}
```

There is no default balance, so you can test both a funded payout and the
`400 INSUFFICIENT_BALANCE` refusal. Read it back at `GET …/balance` and
`GET …/balance_transactions`. `amount` on `GET /balance` and `balance` on the
fund response are both the ledger's `available`, the figure `POST /payouts` is
held against.

## Sandbox webhook endpoints

A test key can register a receiver without the dashboard, at
`POST /payments/organizations/{orgId}/sandbox/webhook-endpoints` with a `url`
and optional `events`. Registration is not idempotent and takes no
`Idempotency-Key`. Omit `events` to receive the six payout types plus batch,
approval and endpoint events; checkout events are opt-in. Read what was sent
and what your server answered at `GET …/sandbox/webhook-endpoints/{endpointId}/deliveries`.
[Receive and verify webhooks](/recipes/receive-and-verify-webhooks/) walks through it.

## Test scenarios and suffixes

The last four digits of the recipient's account number choose the outcome, the
same way every time. Times are from create.

| Account suffix  | What happens                                   | Status path and timing                                  | failureCode           |
| --------------- | ---------------------------------------------- | ------------------------------------------------------- | --------------------- |
| _anything else_ | Completes normally                             | `processing` 2 s, `completed` 10 s                      | —                     |
| `0001`          | Fails at the network: invalid account          | `processing` 2 s, `failed` 10 s                         | `account_invalid`     |
| `0002`          | Settles slowly so you can watch it             | `processing` 10 s, `completed` 60 s                     | —                     |
| `0003`          | **Completes, then the bank returns it**        | `processing` 2 s, `completed` 10 s, `failed` 40 s       | `returned_by_bank`    |
| `0004`          | Compliance block; funds are **not** returned   | `processing` 2 s, `failed` 10 s                         | `compliance_rejected` |
| `0005`          | The quote expires before execution             | Rejected at create; no payout exists                    | —                     |
| `0006`          | Waits for your wallet funding                  | `pending` with `requiresFunding: true` until you confirm funding, then `processing` 2 s, `completed` 10 s | — |
| `0007`          | **Accepted, then the response is lost**        | The call fails `500 PAYOUT_OUTCOME_UNKNOWN`; the payout still goes `processing` 2 s, `completed` 10 s | —    |

You observe each change at the next ten-second sweep, so allow ten seconds
more. `processing` lasts eight seconds except on `0002`, which holds it for
fifty, so a sweep may skip it and `payout.processing` is not guaranteed.

### The bank return (`0003`)

![The 0003 bank-return flow: a payout completes, then the bank returns it and it fails](/partner-assets/diagrams/bank-return-0003.svg)

The payout completes, then fails with `failureCode: returned_by_bank` and
`fundsReturned: true`, 30 seconds later in the sandbox and days later in
production.

> [!WARNING]
> Run `0003` before you go to production, and keep polling for at least a
> minute after `completed`. A ledger that treats `completed` as final breaks
> here.

### The funded payout (`0006`)

A `0006` payout waits until you fund it from your own wallet. It is also the
one payout you can cancel, with `POST …/payouts/{payoutId}/cancel`. It comes
back `canceled` with `fundsReturned: true`; it was never debited, so the
balance does not change and no ledger row is written. A funded `0006`, or any
other scenario, returns `PAYOUT_NOT_CANCELABLE`.

When you confirm funding, the last four digits of the transaction hash choose
the outcome:

| Hash ends           | You get                                                                                 |
| ------------------- | --------------------------------------------------------------------------------------- |
| `…0001`             | `400 FUNDING_TRANSACTION_INVALID`: the transaction does not fund this payout            |
| `…0002`             | `409 FUNDING_NOT_YET_VERIFIABLE`: not mined yet; retry the same request                 |
| anything else       | Accepted, and the payout settles                                                        |
| a hash already used | `409 FUNDING_TRANSACTION_ALREADY_USED`; the error names the payout it funded            |

### The lost response (`0007`)

The payout is sent and your balance debited, but the call fails
`500 PAYOUT_OUTCOME_UNKNOWN`. In production this is a timeout after the send
left. Rehearse the recovery ([Idempotency](/idempotency/)):

1. Keep the same `Idempotency-Key`. The same body under a new key gets
   `409 DUPLICATE_REQUEST_DETECTED`, naming the burned key, and nothing runs.
2. Poll `GET /orders?reference=<your reference>`. Exactly one payout carries
   the reference; reconcile against it.
3. A same-key replay answers `409 PAYOUT_OUTCOME_UNKNOWN` with
   `originalRequestId` until support resolves the key. Keep the `requestId`
   from the 500.

In a batch, a `0007` line lands in `requires_review` with `OUTCOME_UNKNOWN`,
never `create_failed`, and the batch still reaches `completed`
([Send a batch](/batch-payouts/)).

### Checkout outcomes by amount suffix

Pay a checkout link with `POST …/payments/simulate` or the link's test panel.
The last two digits of its total in minor units (the cents) pick the outcome.

| Total ends | Outcome | Event delivered |
| ---------- | ------- | --------------- |
| anything else | Paid immediately | `checkout_payment.paid` |
| `.01` | Declined, `card_declined` | `checkout_payment.failed` |
| `.02` | `pending` then `processing` then `paid`, over 20s | `checkout_payment.paid` only. `processing` fires no event |
| `.03` | Paid, then fully refunded after 45s | `paid`, then `checkout_payment.refunded` |
| `.04` | Paid, then **charged back** after a minute | `paid`, then `checkout_payment.reversed` |
| `.05` | Declined, `insufficient_funds` | `checkout_payment.failed` |
| `.06` | Paid, then half refunded after 30s | `paid`, then `checkout_payment.partially_refunded`; the status stays `paid` and `refundedBase` moves |

`GET /checkout/organizations/{orgId}/sandbox/scenarios` returns this table. Run
`.04` before you go live, as `0003` is for payouts. To force a chargeback at
once, call `POST /checkout/…/payments/{paymentId}/simulate` with
`{"action":"chargeback"}`.

Your sandbox organization is already verified and card-enabled. Live, card
onboarding follows business verification, so the verification `403` and the
onboarding `publishError` appear only there.

## Production access

Live keys are issued once your organization is verified. Work through
[Go live](/going-live/#the-checklist) first.

## Versioning

The version is in the path: **`v1`**. Backwards-compatible changes ship
continuously without a version change, so build for these:

- new endpoints
- new optional request parameters
- new fields in a response object
- new enum values, including error `type` values, payout states and corridors

Ignore response fields you do not recognize, and never throw on an unfamiliar
error `type` ([Errors](/errors/)). Breaking changes get a new version in the
path. Removing or renaming a field, changing a type, or making an optional
parameter required never happens to `v1` in place.

## Deprecation

If we retire a version:

- We write to your organization's registered technical contact.
- The migration window is **at least 90 days** from that notice.
- The retiring version keeps working, unchanged, for the whole window.
- A changelog entry accompanies the notice.

We will not shorten a window for a live partner. If a security issue forced a
faster change, a person would contact you before any request failed.

## Rate limits

The default is 100 requests per minute per API credential. High-volume payout
and reconciliation routes (`POST …/payouts`, the `GET …/orders` list,
`GET …/events`, `GET …/balance_transactions` and the batch reads) allow 600 per minute, and batch creation 30.
Reading one payout, `GET …/orders/{payoutId}`, is on the default 100, so follow
payouts with webhooks and the event feed, and poll a single payout no more than
about every 5 seconds. A
separate 2,000-per-minute ceiling per source IP always applies.
`GET /payments/organizations/{orgId}/policy` reports the buckets under
`rateLimits`.

Rate-limit headers are sent on the `429` only, never on a `200`. Treat its
`Retry-After`, in seconds, as authoritative; `X-RateLimit-Limit`,
`X-RateLimit-Remaining` and `X-RateLimit-Reset` describe the bucket you hit.
`RATE_LIMITED` is safe to retry after the backoff. If you need a higher
ceiling, ask before you build on it, not during a production payroll run.
