Skip to main content

Money REST API Reference

The Money Service provides safe decimal-arithmetic financial operations. All amounts are in cents (positive integer). Floating-point is never used. $12.50 = 1250.

See the REST API Overview for authentication, error format, pagination, and rate limiting.

note

For layout brevity, the /api/v1 base path prefix is omitted from the endpoint tables below.


Endpoints

EndpointAuthDescription
POST /walletsCreate a new wallet
GET /walletsList wallets (filter by userId, currency)
GET /wallets/:idWallet details
GET /wallets/:id/balanceCurrent balance (available, pending, frozen)
POST /wallets/:id/creditCredit funds
POST /wallets/:id/debitDebit funds
POST /wallets/:id/holdFreeze funds (two-phase: step 1)
POST /wallets/:id/confirmConfirm hold → final debit (step 2a)
POST /wallets/:id/cancelCancel hold → return to available (step 2b)
POST /wallets/:id/reversalReverse a completed transaction
POST /wallets/transferTransfer between two wallets
GET /wallets/:id/transactionsTransaction history (paginated)
GET /transactions/:idTransaction details

Wallet Object

{
"id": "01j9pwal0000000000000001",
"tenantId": "01j9pten0000000000000001",
"userId": "01j9pusr0000000000000001",
"currency": "USD",
"label": "Main wallet",
"balance": {
"available": 125000,
"pending": 0,
"frozen": 5000
},
"createdAt": "2026-04-01T10:00:00Z",
"updatedAt": "2026-04-22T03:00:00Z"
}
FieldTypeDescription
idULIDWallet ID
currencyISO 4217e.g. USD, EUR, BTC
balance.availableinteger (cents)Funds available for debit or hold
balance.pendinginteger (cents)Incoming funds not yet settled
balance.frozeninteger (cents)Funds locked by an active hold

POST /api/v1/wallets

Create a new wallet for a user or module.

POST https://api.septemcore.com/v1/wallets
Authorization: Bearer <access_token>
Content-Type: application/json

{
"userId": "01j9pusr0000000000000001",
"currency": "USD",
"label": "Main wallet"
}

Response 201 Created — the new wallet object.


Credit / Debit

Both operations share the same request schema. An idempotencyKey (UUID v4/v7) is required on every mutation.

POST /api/v1/wallets/:id/credit

POST https://api.septemcore.com/v1/wallets/01j9pwal0000000000000001/credit
Authorization: Bearer <access_token>
Idempotency-Key: 018e9c73-4b2a-7000-ab12-000000000001
Content-Type: application/json

{
"amount": 5000,
"currency": "USD",
"reason": "payout_commission",
"meta": { "referenceId": "order_42" }
}

Response 200 OK:

{
"transactionId": "01j9ptx0000000000000001",
"type": "credit",
"status": "completed",
"amount": 5000,
"currency": "USD",
"balanceAfter": {
"available": 130000,
"pending": 0,
"frozen": 5000
},
"createdAt": "2026-04-22T04:00:00Z"
}

POST /api/v1/wallets/:id/debit

Same schema as credit (amount, currency, reason, meta, idempotencyKey).

Concurrent debit protection: SELECT ... FOR UPDATE serializes operations at the row level. Two concurrent debits on the same wallet serialize to milliseconds. PostgreSQL CHECK (balance >= 0) constraint makes a negative balance impossible. If available < amount400 Bad Request (problems/insufficient-funds).


Hold / Confirm / Cancel (two-phase debit)

Use this pattern when integrating with external payment systems (Stripe, PayPal). Freeze funds → wait for external confirmation → finalize or release.

Phase 1 — POST /api/v1/wallets/:id/hold

POST https://api.septemcore.com/v1/wallets/01j9pwal0000000000000001/hold
Authorization: Bearer <access_token>
Idempotency-Key: 018e9c73-4b2a-7000-ab12-000000000002
Content-Type: application/json

{
"amount": 5000,
"currency": "USD",
"ttl": "72h",
"reason": "pre_authorization"
}

Effect: available -= 5000, frozen += 5000. Transaction status: held.

Response 200 OK includes transactionId of the hold — required for step 2.

Hold parameterLimit
Default TTL72 hours (MONEY_HOLD_TTL_HOURS)
Max TTL168 hours (7 days)
Max concurrent holds per wallet100 (MONEY_MAX_HOLDS_PER_WALLET). Exceeded → 429 Too Many Requests
Hold requires availableavailable >= amount. Otherwise → 400 Bad Request (problems/insufficient-funds)

TTL expiry: A background Go worker polls every 60 seconds (MONEY_HOLD_CLEANUP_INTERVAL_SEC). Expired holds are auto-cancelled: frozen → available. Event money.hold.expired is published and an audit record is created.

Phase 2a — POST /api/v1/wallets/:id/confirm

Confirms a hold — funds are deducted permanently.

POST https://api.septemcore.com/v1/wallets/01j9pwal0000000000000001/confirm
Authorization: Bearer <access_token>
Idempotency-Key: 018e9c73-4b2a-7000-ab12-000000000003
Content-Type: application/json

{
"holdTransactionId": "01j9ptx0000000000000002"
}

Effect: frozen -= 5000. Transaction status: confirmed (= final debit).

Phase 2b — POST /api/v1/wallets/:id/cancel

Cancels a hold — returns funds to available.

POST https://api.septemcore.com/v1/wallets/01j9pwal0000000000000001/cancel
Authorization: Bearer <access_token>
Idempotency-Key: 018e9c73-4b2a-7000-ab12-000000000004
Content-Type: application/json

{
"holdTransactionId": "01j9ptx0000000000000002"
}

Effect: frozen -= 5000, available += 5000. Transaction status: canceled.


Transfer — POST /api/v1/wallets/transfer

Atomic transfer between two wallets in one PostgreSQL ACID transaction.

POST https://api.septemcore.com/v1/wallets/transfer
Authorization: Bearer <access_token>
Idempotency-Key: 018e9c73-4b2a-7000-ab12-000000000005
Content-Type: application/json

{
"fromWalletId": "01j9pwal0000000000000001",
"toWalletId": "01j9pwal0000000000000002",
"amount": 2500,
"currency": "USD",
"reason": "internal_settlement"
}

Deadlock prevention: Wallets are locked in ascending UUID order (SELECT ... FOR UPDATE ORDER BY id ASC). Two concurrent transfers always lock in the same order. On PostgreSQL deadlock (40P01) — automatic retry up to 3× (100 ms / 200 ms / 400 ms exponential backoff).


Reversal — POST /api/v1/wallets/:id/reversal

Creates a counter-transaction referencing a completed original transaction. The original is marked reversed: true.

POST https://api.septemcore.com/v1/wallets/01j9pwal0000000000000001/reversal
Authorization: Bearer <access_token>
Idempotency-Key: 018e9c73-4b2a-7000-ab12-000000000006
Content-Type: application/json

{
"originalTransactionId": "01j9ptx0000000000000001",
"reason": "customer_refund"
}
Reversal constraintValue
Eligible statusescompleted only
Double reversalNot allowed — 400 Bad Request
Max original age365 days (MONEY_REVERSAL_MAX_AGE_DAYS). Older → 400 (problems/reversal-window-expired)
Hold transactionsReversal not applicable — use cancel instead

Reversal State Compatibility

Transaction statusreversal()cancel()
held400✅ Unfreezes
confirmed (hold → confirm)✅ Counter-transaction
completed (credit/debit)✅ Counter-transaction
canceled400❌ Already canceled
reversed400❌ Double reversal denied

Transaction Object

{
"id": "01j9ptx0000000000000001",
"walletId": "01j9pwal0000000000000001",
"type": "credit",
"status": "completed",
"amount": 5000,
"currency": "USD",
"reason": "payout_commission",
"idempotencyKey": "018e9c73-4b2a-7000-ab12-000000000001",
"referenceTransactionId": null,
"reversed": false,
"meta": { "referenceId": "order_42" },
"balanceAfter": {
"available": 130000,
"pending": 0,
"frozen": 5000
},
"createdAt": "2026-04-22T04:00:00Z"
}

Transaction Types

TypeDescription
creditFunds added to available
debitFunds removed from available
transferDebit on source + credit on destination
holdFunds moved from available to frozen
confirmFrozen funds permanently deducted (held → confirmed)
cancelFrozen funds returned to available (held → canceled)
reversalCounter-transaction crediting back a completed debit

Transaction History — GET /api/v1/wallets/:id/transactions

Cursor-paginated list. Partitioned by created_at (monthly partitions) — index: (wallet_id, created_at DESC). Stored for 7 years (AML/KYC).

GET https://api.septemcore.com/v1/wallets/01j9pwal0000000000000001/transactions
?limit=20
&cursor=eyJpZCI6IjAx...
&type=debit
&status=completed
&since=2026-01-01T00:00:00Z
Authorization: Bearer <access_token>
FilterDescription
typecredit, debit, transfer, hold, confirm, cancel, reversal
statuscompleted, pending, held, failed, canceled, reversed
since / untilISO 8601 timestamp boundaries
limitDefault 20, max 100

Business Limits

ParameterDefaultDefined by
Max single transaction10,000,000 cents ($100,000)Billing plan
Max wallet balance100,000,000 cents ($1,000,000)Billing plan
Minimum amount1 centFixed
Negative balanceNot allowedFixed (CHECK balance >= 0)
Storage typeBIGINT in PostgreSQLFixed (~$92 quadrillion max)

Plan limits are cached in Valkey (billing:plan_limits:{tenantId}, TTL 5 minutes). Cache miss → gRPC to Billing to resolve. Billing down + cache miss → 503 Service Unavailable (fail-closed: financial safety over availability).


Guarantees

GuaranteeImplementation
IdempotencySame idempotencyKey → same result returned
ACIDAll operations in one PostgreSQL transaction
No floatInteger cents only (shopspring/decimal)
Audit trailEvery operation written to Audit Service
Double-entryEvery operation has debit + credit entries
Concurrent debit safetySELECT ... FOR UPDATE + CHECK (balance >= 0)
Transfer deadlock-freeLock ordering by ascending wallet UUID

Error Reference

Error typeStatusTrigger
problems/insufficient-funds400available < amount on debit, hold, or transfer
problems/invalid-amount400amount <= 0 or non-integer
problems/hold-limit-exceeded429Concurrent holds on wallet > 100
problems/reversal-window-expired400Original transaction older than 365 days
problems/double-reversal400Transaction already reversed
problems/hold-not-reversible400Attempting reversal on a held transaction — use cancel
problems/idempotency-conflict409Same idempotencyKey, different request body
problems/plan-limit-exceeded402Transaction amount exceeds Billing plan limit
problems/billing-unavailable503Billing down + plan limits not cached