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
Section titled “Base URL”https://api.avvio.xyz/business/api/v1Every path in this documentation is relative to it.
Environments
Section titled “Environments”| 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:
export AVVIO_API_KEY=avvio_test_… # or avvio_live_…How the sandbox behaves
Section titled “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). - Rates are fixed, so assertions on exact amounts keep passing. Real rates move (Go 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 in4567, 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
Section titled “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).
POST /payments/organizations/{orgId}/sandbox/fundx-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
Section titled “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 walks through it.
Test scenarios and suffixes
Section titled “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_; 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)
Section titled “The bank return (0003)”The payout completes, then fails with failureCode: returned_by_bank and
fundsReturned: true, 30 seconds later in the sandbox and days later in
production.
The funded payout (0006)
Section titled “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_: the transaction does not fund this payout |
…0002 |
409 FUNDING_: not mined yet; retry the same request |
| anything else | Accepted, and the payout settles |
| a hash already used | 409 FUNDING_; the error names the payout it funded |
The lost response (0007)
Section titled “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):
- Keep the same
Idempotency-Key. The same body under a new key gets409 DUPLICATE_REQUEST_DETECTED, naming the burned key, and nothing runs. - Poll
GET /orders?reference=<your reference>. Exactly one payout carries the reference; reconcile against it. - A same-key replay answers
409 PAYOUT_OUTCOME_UNKNOWNwithoriginalRequestIduntil support resolves the key. Keep therequestIdfrom 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).
Checkout outcomes by amount suffix
Section titled “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_ |
.01 |
Declined, card_declined |
checkout_ |
.02 |
pending then processing then paid, over 20s |
checkout_ only. processing fires no event |
.03 |
Paid, then fully refunded after 45s | paid, then checkout_ |
.04 |
Paid, then charged back after a minute | paid, then checkout_ |
.05 |
Declined, insufficient_funds |
checkout_ |
.06 |
Paid, then half refunded after 30s | paid, then checkout_; 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
Section titled “Production access”Live keys are issued once your organization is verified. Work through Go live first.
Versioning
Section titled “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
typevalues, payout states and corridors
Ignore response fields you do not recognize, and never throw on an unfamiliar
error type (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
Section titled “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
Section titled “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.
Was this page helpful?