Skip to main content
API Reference

Wallets API

HTTP API reference for the F2.M4 Programmable Wallets — create wallets, post ledger entries, bind funding sources, execute mandate-gated payments, triage reconciliation anomalies.

4 min read · updated

Wallets API

REST API for the Wallets concept. Per-agent fund pools with mandate-gated debits, multi-rail funding, and automatic reconciliation.

Base URL: https://api.codespar.dev

All endpoints require authentication. See Authentication. Endpoints noted as admin require either a bearer api key OR service auth with an x-codespar-user header for an org admin/owner.

Wallet object

FieldTypeDescription
idstringWallet ID, wlt_<16chars>
org_idstringOwning organization
project_idstringOwning project — wallets are project-scoped
agent_idstring | nullOptional agent binding; null for org-level wallets
display_namestringFree-form, max 120 chars
status"active" | "frozen" | "closed"Frozen wallets reject new ops; closed is terminal
created_atstringISO 8601
closed_atstring | nullSet when status flips to closed
metadataobjectFree-form operator-supplied JSON

When fetched via GET /v1/wallets/:id, the response also embeds a balances array (one entry per currency).

Currency whitelist

BRL, USD, MXN, COP, ARS, USDC, BRLA. Fiat-only currencies route through the gateway (Stripe / Mercado Pago / Asaas / Pix). Stablecoin currencies (USDC, BRLA) currently support funding only; the offramp execute path lands in M4.4.


Wallets

Create a wallet

POST /v1/wallets

Body

{
  "display_name": "Customer Service Agent",
  "currency": "BRL",
  "agent_id": "agt_optional",
  "metadata": {}
}

Seeds a zero-balance row in the requested currency. Other currencies get rows lazily as funding events for those currencies post.

Response201 Created with the wallet object.

List wallets

GET /v1/wallets

Project-scoped. Optional query params:

  • status=active|frozen|closed
  • agent_id=<id>
  • limit=1..100 (default 25)

Response

{ "wallets": [/* Wallet */] }

Get a wallet (with balances)

GET /v1/wallets/:id

Response — Wallet object with embedded balances:

{
  "id": "wlt_…",
  "balances": [
    {
      "wallet_id": "wlt_…",
      "currency": "BRL",
      "balance_minor": "10000",
      "available_minor": "8500",
      "updated_at": "2026-04-26T12:00:00Z"
    }
  ]
}

balance_minor and available_minor are bigint strings (centavos / cents). available <= balance is enforced at the DB layer.


Ledger

Post a ledger entry

POST /v1/wallets/:id/ledgeradmin

Direct ledger writes. The gateway's processPayment is the primary caller for hold/release/debit; webhook adapters use this for fund. Hand-rolled writes are typically operator-driven corrections.

Body

{
  "currency": "BRL",
  "amount_minor": "5000",
  "kind": "fund",
  "mandate_id": null,
  "attempt_id": "demo-fund-001",
  "external_ref": "E12345…",
  "metadata": { "description": "Pix incoming" }
}

kind enum: fund | hold | release | debit | reconcile | reverse | fee. Sign refinements:

KindSignMandate required
fundpositiveno
releasepositiveno
holdnegativeyes
debitnegativeyes
feenegativeno
reconcilezerono
reversemirrors originalno

Idempotency(wallet_id, attempt_id, kind) and (wallet_id, kind, external_ref) are partial unique indexes. A retried call with the same attempt_id returns the prior row with HTTP 200, no double-post; a fresh insert returns 201.

Errors400 invalid_body, 409 wallet_not_active, 409 balance_constraint_violation (CHECK trip).

List ledger entries

GET /v1/wallets/:id/ledger

Query params:

  • limit=1..200 (default 50)
  • before_id=<bigint> — cursor pagination, descending by id
  • kind=<one of the kinds above>

Response

{
  "entries": [/* LedgerEntry */],
  "next_before": "12345"
}

Funding sources

A funding source binds a connection to a wallet for a specific currency. The funding bridge converts that connection's commerce.payment.* events into kind=fund ledger entries.

Constraint — at most one binding per (connection_id, currency) while enabled=true. Prevents the recon engine from double-applying the same receipt.

Bind a funding source

POST /v1/wallets/:id/funding-sourcesadmin

{
  "connection_id": "ca_…",
  "currency": "BRL",
  "metadata": {}
}

Errors404 connection_not_found (not in caller's project), 409 connection_not_active, 409 funding_source_conflict.

List funding sources

GET /v1/wallets/:id/funding-sources

{ "funding_sources": [
  {
    "wallet_id": "wlt_…",
    "connection_id": "ca_…",
    "currency": "BRL",
    "enabled": true,
    "created_at": "…",
    "metadata": {}
  }
] }

Unbind a funding source

DELETE /v1/wallets/:id/funding-sources/:connection_id/:currencyadmin

Returns 204 No Content on success, 404 funding_source_not_found otherwise.


Execute

Run a payment through the gateway

POST /v1/wallets/:id/executeadmin

Drives the full F2.M4 lifecycle: policy → mandate → wallet hold → route → execute → wallet settle → budget record.

Body

{
  "amount": 19.99,
  "currency": "BRL",
  "recipient": "Acme Supplier Co.",
  "description": "Pix supplier payout",
  "mandate_id": "mnd_abc123",
  "preferred_method": "pix",
  "attempt_id": "exec-2026-04-26-001",
  "metadata": {}
}

amount is in major units (e.g. 19.99 = R$ 19,99). The gateway converts internally.

Status mapping

Gateway statusHTTPWhen
completed200All gates passed; provider returned settled
requires-approval402InsufficientFundsError — top up the wallet to retry
denied403Policy or mandate gate failed
failed422Route or execute gate failed

Response body — Full GatewayPaymentResult always (even on non-200):

{
  "requestId": "gw-…",
  "status": "completed",
  "policy": { "allowed": true, "reason": "" },
  "mandate": { "valid": true, "mandateId": "mnd_…" },
  "route": { "method": "pix", "provider": "asaas" },
  "payment": { "transactionId": "tx_…", "amountSent": 19.99 },
  "wallet": {
    "holdId": "100",
    "debitId": "102",
    "releaseId": "101",
    "insufficientFunds": false
  },
  "audit": [
    { "timestamp": "…", "step": "policy_check", "status": "pass", "detail": "…" }
  ]
}

Stablecoin currencies (USDC, BRLA) return 400 currency_not_routable — the fiat gateway has no on-chain rails today.


Reconciliation anomalies

The recon engine flags two failure modes:

  • debit_without_receipt — a debit older than the grace window has no matching provider event
  • receipt_without_debit — a commerce.payment.* event with no matching wallet ledger row

List anomalies

GET /v1/wallets/:id/recon-anomalies

Optional query: status=open|resolved|dismissed (default open).

{ "anomalies": [
  {
    "id": "1",
    "wallet_id": "wlt_…",
    "kind": "debit_without_receipt",
    "ledger_entry_id": "100",
    "external_ref": "tx_…",
    "amount_minor": "-1000",
    "currency": "BRL",
    "detected_at": "…",
    "status": "open",
    "resolved_at": null,
    "resolution_note": null,
    "metadata": {}
  }
] }

Resolve / dismiss

POST /v1/wallets/:id/recon-anomalies/:aidadmin

{
  "status": "resolved",
  "note": "Reconciled manually against bank statement"
}

Or {"status": "dismissed", "note": "False positive — webhook arrived 90s late"}.

Errors404 anomaly_not_found (already resolved or wrong wallet).


Idempotency at a glance

PathIdempotency keyBehavior on retry
Ledger POST(wallet_id, attempt_id, kind) OR (wallet_id, kind, external_ref)HTTP 200 + prior row
Funding-source POST(connection_id, currency) partial uniqueHTTP 409 funding_source_conflict
Execute POSTwalletAttemptId (defaults to requestId)Forwarded to wallet ops; same prior-row semantics
Recon-anomaly resolveWHERE status='open' predicateHTTP 404 if already resolved

Errors

All non-2xx responses follow the shared error envelope:

{
  "error": {
    "code": "balance_constraint_violation",
    "message": "ledger entry would violate a wallet balance invariant",
    "details": { "wallet_id": "…", "currency": "BRL", "kind": "hold" }
  },
  "request_id": "req_…"
}

Specific codes used by this surface:

CodeHTTPMeaning
wallet_not_active409Wallet is frozen or closed
balance_constraint_violation409DB CHECK trip — overdraw or negative balance
ledger_conflict409Different unique index than the partials (rare)
funding_source_conflict409Connection already bound for this currency
funding_source_not_found404Binding doesn't exist
connection_not_found404Connection not in caller's project
connection_not_active409Connection status ≠ connected
currency_not_routable400Stablecoin execute (M4.4)
anomaly_not_found404Already resolved/dismissed or wrong wallet
not_found404Wallet doesn't exist or cross-tenant

Next

Edit on GitHub

Last updated on