---
description: >-
  Wallet API endpoint reference — wallets, deposits, holds, captures, refunds, transfers, FX conversions, error codes, and idempotency semantics.
---

# 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 `amount` fields 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:

```json
{
  "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:

```json
{
  "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-98765432` for a specific wire
* `capture-INV-4521` for a specific invoice
* `refund-INV-4521-partial-1` for a specific refund

> **Never reuse a key across different operations.** Reusing `invoice-4521-refund` for 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 `parentJournalId` when 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_uuid` for 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.
