Sandbox
Test the Hodle API against testnet USDB with no real money.
The sandbox lets you integrate without moving real funds. It runs on Base Sepolia testnet against Avenia's own sandbox. Sandbox data is fully isolated from production.
No real money moves, and no route changes. The sandbox has no endpoints of its own: you call the same paths you will call in production, on the sandbox host, with a sandbox key. Point the same client at
https://api.hodle.com.brwith ahodle_live_key and it is the production integration — nothing to rewrite.
Base URL
https://sandbox-api.hodle.com.brGet a sandbox key
- Log into the Hodle admin.
- Open API Keys and create a new key with environment Sandbox.
- Copy the key. Sandbox keys are prefixed
hodle_test_; production keys arehodle_live_.
Authenticate every request with Authorization: Bearer hodle_test_.... A sandbox key only
works against the sandbox base URL; a production key is rejected (401) on sandbox routes.
Sandbox keys are stored in the isolated sandbox data plane and HMAC'd with a shared
sandbox secret, so a key created in the admin works against https://sandbox-api.hodle.com.br.
Keys created before this wiring must be recreated — an older sandbox key returns
401 Invalid API key or wrong environment on every route. If a freshly created hodle_test_
key still 401s, ask support to confirm it landed in the sandbox data plane before debugging
your client.
Three things have to agree for a request to be served as sandbox, and all three are checked on
every call: the host it arrived on, the key prefix, and the data plane the
application record lives in. A hodle_live_ key on the sandbox host is rejected, and so is a
hodle_test_ key on the production host — there is no configuration in your client that can
cross the planes by accident.
What the sandbox covers
Every endpoint below is the production path running the production handler. Only the upstream changes: Avenia's calls go to Avenia's own sandbox, and anything on-chain settles on Base Sepolia. There is no mirrored copy of a handler that could drift from the real one, which is why the shapes and the errors are the ones you will get in production.
| Surface | Sandbox host | Notes |
|---|---|---|
/api/subaccount (create, list, get) | Mirrored | Creates a real sub-account in the Avenia sandbox. |
/api/kyc, /api/kyc/{attemptId}, /api/kyc/document | Mirrored | Real Avenia sandbox attempts — see Simulating a KYC outcome. |
/api/kyb, /api/kyb/proof-of-address, /api/kyb/proof-of-financial-capacity | Mirrored | Including the GET attempt polls. |
/api/quote | Mirrored | Priced off the Avenia sandbox book, not the live one. |
/api/wallet/create | Mirrored | Creates a real Safe on Base Sepolia. The response carries chainId: 84532 — read it, not network: the wallet keeps the network you asked for, but in sandbox it is materialised on Base Sepolia, which is the only chain the sandbox settles on. |
/api/wallet, /api/wallet/get, /api/wallet/keys | Mirrored | Wallet is provisioned on first use — see Wallets in sandbox. |
/api/wallet/payout | Mirrored | Debit settles on-chain on Base Sepolia; the PIX leg is simulated. Response carries X-Hodle-Sandbox: simulated. |
/api/wallet/transfer | Mirrored | Real Base Sepolia transfer to the address you name. |
/api/deposit/asset | Mirrored | Unpayable BR Code in, real test token out. Settle it with the webhook. |
/api/lightning/invoice | Mirrored | Returns a bolt11-shaped string that is not payable — see below. |
/avenia/webhook | Mirrored | The settlement trigger — see Settling a payment. |
/api/withdraw/pix | 501 | Deprecated; no sandbox settlement. |
Settlement tokens
A sandbox payout moves a real test token in a real Base Sepolia transaction, so you get a hash you can open on BaseScan. Only the PIX leg is simulated — the Brazilian payment system has no testnet.
| You send | Sandbox settles in | Decimals | Mirrors |
|---|---|---|---|
asset: "BRLA" | BRLH 0x4ec5f42ee07d5d1cfe431ec7059df4df556d44a8 | 18 | BRLA, BRL-pegged 1:1 |
asset: "USDC" or "USDT" | USDH 0x1c7af3c850c1d1d8d97d40db0acdd0195c6323f7 | 6 | USDC/USDT, converted at the FX rate |
Decimals and the peg match production on purpose: amount maths that is right here is right there. Both tokens are worthless, mint is permissionless, and your sandbox wallet is topped up automatically — you never need a faucet. Spending more than you hold still fails, so the insufficient-balance path stays testable.
What is still imaginary. The incoming PIX and the Lightning payment never happen — there is no testnet for the Brazilian payment system, and the invoice above is unpayable. Delivery and debit legs are real transactions on Base Sepolia. Before going live, run the PIX legs once against
https://api.hodle.com.brwith ahodle_live_key, de-risked with small amounts (value: 10is R$ 0.10), your own kill switch, and a lowperTxLimitBrl.
Simulating a KYC outcome
Pass sandboxReject: true in POST /api/kyc to drive the attempt to REJECTED; omit it to
get APPROVED. The flag is Avenia's own, forwarded untouched, so the rejection you get is a
real rejection from a real KYC engine. Poll GET /api/kyc/{attemptId} exactly as you would in
production.
Rates
Quotes are priced off the Avenia sandbox book, not the live one, so the numbers are real-shaped but not real. Do not assert on exact values in sandbox.
Wallets in sandbox
Production requires the account holder to establish a wallet PIN during onboarding, and the API deliberately cannot mint one — that is what keeps the wallet non-custodial. Sandbox has no onboarding, so it provisions the wallet for you on first use.
| Sandbox | |
|---|---|
| PIN | 123456 — pass it as walletPin |
| Provisioned by | The first call to /api/wallet/keys or /api/wallet/get |
| Network | Base Sepolia (84532) |
Six digits because that is the platform's format. Every sandbox wallet shares this PIN, so it protects nothing and is safe to publish.
The key material is real: it goes through the same path as production, so
/api/wallet/keys returns a genuine protectedSymmetricKey and you exercise the real
handshake rather than a stub that would pass here and fail in production.
Settling a payment
A sandbox charge cannot be paid: the BR Code from /api/deposit/asset carries a sandbox marker
instead of a live PSP payload, so no banking app can pay it — deliberately, so nobody pays a
test charge with real money.
You settle it the way production does: by the webhook. In production the PSP posts the ticket
event to /avenia/webhook when the PIX lands; in sandbox you post it, because no PIX will
ever land. Same route, same payload, same handler.
curl --request POST \
--url https://sandbox-api.hodle.com.br/avenia/webhook \
--header "Authorization: Bearer $HODLE_SANDBOX_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"event": {
"subscription": "TICKET",
"data": {
"type": "DEPOSIT-SUCCESS",
"ticket": { "id": "<the trackId from /api/deposit/asset>" }
}
}
}'Your sandbox key is required here, and that is the one difference from the call Avenia makes. The route accepts two proofs of who you are:
- Avenia's signature, which it puts on every webhook including the ones from its sandbox. You cannot produce it.
- Your sandbox API key, which stands in for it. It says which charges you may settle: your
own, and only those — a
trackIdbelonging to another integrator matches nothing.
Neither one and you get 401. The signature is verified on every environment, sandbox
included; the key does not switch it off, it is simply the other accepted proof.
A key-authenticated request can only settle. It never reaches the handlers that credit deposits or complete KYC, so nobody can drive those on your data — or you on theirs.
Only DEPOSIT-SUCCESS and TICKET-COMPLETE settle. A DELIVERY-FAILED cannot be used to mark
a charge paid.
Idempotent: settling twice delivers once, including two requests racing each other. The
delivery leg is a real Base Sepolia transaction, so the asset genuinely lands at the address
you asked for. If that transaction fails, the charge goes to FAILED and stays there — create
a new one with /api/deposit/asset rather than retrying the settle.
You control the timing, which is the point: you exercise the PENDING to COMPLETED
transition and your own webhook handling — against the very payload shape you will parse in
production — instead of waiting on a rail that will never fire.
Lightning invoices are not payable
/api/lightning/invoice returns a bolt11-shaped string so you can wire up parsing and the
amount fields, but no wallet will settle it — there is no channel, no preimage, and the
checksum does not validate. Drive settlement through the webhook above.
When a call fails with a missing fake
An endpoint whose upstream is neither the Avenia sandbox nor Base Sepolia fails with a message naming the host and path rather than silently succeeding. If you hit one, it is a gap on our side — send us the error and we will map it.