Skip to content

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.

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

Every path in this documentation is relative to it.

SANDBOX PRODUCTION Test key avvio_test_* Engine Simulated Simulated rails No real funds Live key avvio_live_* Engine Production Banking rails Real funds
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_…

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 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.

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/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.

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.

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.

pending Reserved processing Dispatched completed Funds sent failed returned_by_bank Bank return, days later

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

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 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):

  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).

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.

Live keys are issued once your organization is verified. Work through Go live first.

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). 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.

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.

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?