Authentication
API keys, the Bearer header, environments baked into the key, and the four scopes that gate every endpoint.
Every request except GET /health authenticates with an API key, sent as a
Bearer token. The key carries everything Acute needs: which organization you are,
which environment you are in, and what you are allowed to do.
curl https://sandbox.api.acute.network/v1/wallets \
-H "Authorization: Bearer acuinf_test_aV6Rn2pQk8wXyZ3mB1cD4eF7"A key is a prefix that names the environment, followed by an opaque secret:
acuinf_test_<secret> ← sandbox
acuinf_live_<secret> ← productionThe secret is 24 random bytes, base64url-encoded. The environment is part of the
key: there is no separate "mode" flag to forget. A test key only works on the
sandbox host (https://sandbox.api.acute.network/v1); a live key only works on
the live host (https://api.acute.network/v1). They are not interchangeable: send a
key to the host for the other environment and it is rejected with 401
(API_KEY_ENVIRONMENT_MISMATCH), so a key minted for one environment cannot be
pointed at the other.
The secret is shown once
Acute stores only a hash of your key, never the secret. When you mint a key in the Console, copy it immediately: it is displayed exactly once. Lose it and you rotate, you do not recover.
Send the full key in the Authorization header on every authenticated call:
-H "Authorization: Bearer acuinf_test_..."A malformed, unknown, or revoked key returns 401 authentication_error with
the granular code API_KEY_INVALID (a missing header is API_KEY_MISSING). A
well-formed key sent to the host for the other environment (a live key on the
sandbox host, or a test key on the live host) returns 401
authentication_error with API_KEY_ENVIRONMENT_MISMATCH. The envelope never
leaks an attacker's secret, just the reason the key did not authenticate.
{
"success": false,
"statusCode": 401,
"error": {
"type": "authentication_error",
"code": "API_KEY_INVALID",
"message": "Invalid API key",
"details": {}
},
"meta": { "requestId": "req_a17c4e92bd05f3681c0a7e44" }
}A key carries one or more of four scopes. Each endpoint requires exactly one;
if your key lacks it, you get 403 authorization_error
(API_KEY_SCOPE_FORBIDDEN). Grant a key only the scopes it needs.
| Scope | Grants |
|---|---|
wallet | create/list/get wallets, KYC, balance, and withdraw |
payment | create/list/get payments, refund, and the in-app pay action |
transfer | wallet → wallet transfers |
payout | batch disbursement |
Withdraw rides the wallet scope
POST /wallets/:id/withdraw is gated by wallet, not a scope of its own; it is a wallet action.
POST /wallets/:id/transfer needs transfer, and POST /wallets/:id/pay needs payment. The
scope → endpoint map on the reference overview is exhaustive.
| Endpoint | Scope |
|---|---|
POST /wallets, GET /wallets, GET /wallets/:id | wallet |
POST /wallets/:id/kyc, GET /wallets/:id/balance | wallet |
POST /wallets/:id/withdraw | wallet |
POST /wallets/:id/transfer | transfer |
POST /wallets/:id/pay | payment |
POST /payments, GET /payments, GET /payments/:id | payment |
POST /payments/:id/refund | payment |
POST /payouts, GET /payouts, GET /payouts/:id | payout |
GET /health | none |
Rotation mints a replacement carrying the same environment and scopes, then revokes the old one, so you can deploy the new secret before retiring the old. The new secret is returned once, exactly like creation. Revocation is immediate and idempotent; a revoked key authenticates nothing.
Two keys, two jobs
Keep test and live keys in separate secrets and separate config. A mix-up is loud (a live
key pointed at the sandbox host is rejected with API_KEY_ENVIRONMENT_MISMATCH, and vice versa),
but the cleanest defence is to never let them share a variable name.