API Reference
Every endpoint is under /organizations/{tenant}/ on the wallet API host (https://{FQDN}). All requests require the X-API-KEY header. All POSTs require an Idempotency-Key header.
Wallets
| Method | Path | Description |
|---|---|---|
GET | /wallets | List your tenant's wallets |
POST | /wallets | Create a wallet (owner_type, owner_ref, name) |
GET | /wallets/{id} | Fetch a wallet |
GET | /wallets/{id}/balance | Per-asset balances (?asset=USD optional) |
GET | /wallets/{id}/entries | Ledger entries — paginated (50 per page), filterable by asset, journal_kind, and date range |
Wallets created through the API always have kind: operational. Other account kinds (hold sub-accounts, suspense, external source) are system-managed and appear only in ledger entries.
Money Movement
| Method | Path | Description |
|---|---|---|
POST | /deposits | Record an external source and split it across wallets atomically |
POST | /holds | Preauthorize (place a hold) |
GET | /holds/{id} | Fetch a hold plus its capture history |
POST | /holds/{id}/capture | Capture part or all of a hold into a destination account |
POST | /holds/{id}/void | Void the remaining (uncaptured) portion of a hold |
POST | /captures/{captureJournalUuid}/refund | Refund against a specific capture (partial allowed) |
POST | /transfers | Same-asset transfer between wallets |
POST | /fx-conversions | Cross-asset conversion with explicit rate and rate_source |
Journal kinds: deposit, preauth, capture, void, refund, transfer, fx_conversion.
Hold statuses: open, partially_captured, captured, voided. (expired is reserved; expiration is not currently auto-enforced — void expired holds explicitly.)
Deposit source types: wire, ach_debit, card, stablecoin_deposit.
Amounts & Assets
- All amounts are JSON strings, decimal with up to 8 places. Response
amountfields are also strings — never parse them as JSON numbers (precision loss). - Supported assets:
USD,USDC,EUR,MXN. - Balances update atomically with the journal write — a successful response means the balance reflects the operation.
Error Handling
All errors return a JSON body:
{
"error": "ERROR_CODE",
"message": "Human-readable explanation."
}
| HTTP | error | When it happens |
|---|---|---|
| 400 | MISSING_IDEMPOTENCY_KEY | POST without an Idempotency-Key header |
| 401 | UNAUTHORIZED | Missing or invalid X-API-KEY |
| 403 | FORBIDDEN | API key doesn't match the {tenant} in the URL |
| 404 | NOT_FOUND | Referenced wallet, hold, or capture doesn't exist (or isn't in your tenant) |
| 409 | INVALID_HOLD_STATE | Capturing a voided hold, voiding a captured one, etc. |
| 422 | VALIDATION_ERROR | Payload failed validation — the errors field lists per-field problems |
| 422 | INSUFFICIENT_FUNDS | Preauth or transfer exceeds the wallet's available balance |
| 422 | INVALID_ASSET | Unknown or unsupported asset_code |
| 422 | INVALID_ACCOUNT | Referenced account is not valid for the operation |
| 422 | LEDGER_IMBALANCE | Splits don't sum to the source amount, or another double-entry violation |
| 500 | INTERNAL_ERROR | Server-side error — contact support with the timestamp and request details |
For VALIDATION_ERROR, the response includes a per-field breakdown:
{
"error": "VALIDATION_ERROR",
"message": "The wallet id field must be a uuid.",
"errors": {
"wallet_id": ["The wallet id field must be a uuid."]
}
}
Idempotency
Every POST endpoint requires an Idempotency-Key header. If you send two requests with the same key, the second returns the exact response of the first — no duplicate journal is created.
Why it matters: network failures are silent. A request that times out after sending but before receiving the response leaves you unsure whether the operation succeeded. Retrying with the same key is always safe.
How to name keys: any unique string up to 191 characters. Best practice is a stable, meaningful identifier per operation:
deposit-BANK-WIRE-98765432for a specific wirecapture-INV-4521for a specific invoicerefund-INV-4521-partial-1for a specific refund
Never reuse a key across different operations. Reusing
invoice-4521-refundfor a $50 refund and later for a $100 refund returns the $50 response the second time — the $100 refund never happens.
Keys are stored per journal, scoped to your tenant, and never expire.
Traceability
- Every journal response includes
parentJournalIdwhen it reverses or continues another operation — follow the chain refund → capture → preauth for a full audit trail. - Every entry includes
lineageRef— the source IDs (FIFO-consumed) that funded that specific movement. - Deposit journals include the created source's UUID in
metadata.source_uuidfor cross-referencing.
Roadmap Notes
- Webhooks for balance changes are on the roadmap — poll balances or entries in the meantime.
- Marketplace-style escrow (visible to both parties) is planned as a separate flavor from card-like holds — contact Inyo if you need it.
