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.
For layout brevity, the /api/v1 base path prefix is omitted from the
endpoint tables below.
Endpoints
| Endpoint | Auth | Description |
|---|---|---|
POST /wallets | ✅ | Create a new wallet |
GET /wallets | ✅ | List wallets (filter by userId, currency) |
GET /wallets/:id | ✅ | Wallet details |
GET /wallets/:id/balance | ✅ | Current balance (available, pending, frozen) |
POST /wallets/:id/credit | ✅ | Credit funds |
POST /wallets/:id/debit | ✅ | Debit funds |
POST /wallets/:id/hold | ✅ | Freeze funds (two-phase: step 1) |
POST /wallets/:id/confirm | ✅ | Confirm hold → final debit (step 2a) |
POST /wallets/:id/cancel | ✅ | Cancel hold → return to available (step 2b) |
POST /wallets/:id/reversal | ✅ | Reverse a completed transaction |
POST /wallets/transfer | ✅ | Transfer between two wallets |
GET /wallets/:id/transactions | ✅ | Transaction history (paginated) |
GET /transactions/:id | ✅ | Transaction 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"
}
| Field | Type | Description |
|---|---|---|
id | ULID | Wallet ID |
currency | ISO 4217 | e.g. USD, EUR, BTC |
balance.available | integer (cents) | Funds available for debit or hold |
balance.pending | integer (cents) | Incoming funds not yet settled |
balance.frozen | integer (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 UPDATEserializes operations at the row level. Two concurrent debits on the same wallet serialize to milliseconds. PostgreSQLCHECK (balance >= 0)constraint makes a negative balance impossible. Ifavailable < amount→400 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 parameter | Limit |
|---|---|
| Default TTL | 72 hours (MONEY_HOLD_TTL_HOURS) |
| Max TTL | 168 hours (7 days) |
| Max concurrent holds per wallet | 100 (MONEY_MAX_HOLDS_PER_WALLET). Exceeded → 429 Too Many Requests |
| Hold requires available | available >= 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. Eventmoney.hold.expiredis 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 constraint | Value |
|---|---|
| Eligible statuses | completed only |
| Double reversal | Not allowed — 400 Bad Request |
| Max original age | 365 days (MONEY_REVERSAL_MAX_AGE_DAYS). Older → 400 (problems/reversal-window-expired) |
| Hold transactions | Reversal not applicable — use cancel instead |
Reversal State Compatibility
| Transaction status | reversal() | cancel() |
|---|---|---|
held | ❌ 400 | ✅ Unfreezes |
confirmed (hold → confirm) | ✅ Counter-transaction | ❌ |
completed (credit/debit) | ✅ Counter-transaction | ❌ |
canceled | ❌ 400 | ❌ Already canceled |
reversed | ❌ 400 | ❌ 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
| Type | Description |
|---|---|
credit | Funds added to available |
debit | Funds removed from available |
transfer | Debit on source + credit on destination |
hold | Funds moved from available to frozen |
confirm | Frozen funds permanently deducted (held → confirmed) |
cancel | Frozen funds returned to available (held → canceled) |
reversal | Counter-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>
| Filter | Description |
|---|---|
type | credit, debit, transfer, hold, confirm, cancel, reversal |
status | completed, pending, held, failed, canceled, reversed |
since / until | ISO 8601 timestamp boundaries |
limit | Default 20, max 100 |
Business Limits
| Parameter | Default | Defined by |
|---|---|---|
| Max single transaction | 10,000,000 cents ($100,000) | Billing plan |
| Max wallet balance | 100,000,000 cents ($1,000,000) | Billing plan |
| Minimum amount | 1 cent | Fixed |
| Negative balance | Not allowed | Fixed (CHECK balance >= 0) |
| Storage type | BIGINT in PostgreSQL | Fixed (~$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
| Guarantee | Implementation |
|---|---|
| Idempotency | Same idempotencyKey → same result returned |
| ACID | All operations in one PostgreSQL transaction |
| No float | Integer cents only (shopspring/decimal) |
| Audit trail | Every operation written to Audit Service |
| Double-entry | Every operation has debit + credit entries |
| Concurrent debit safety | SELECT ... FOR UPDATE + CHECK (balance >= 0) |
| Transfer deadlock-free | Lock ordering by ascending wallet UUID |
Error Reference
| Error type | Status | Trigger |
|---|---|---|
problems/insufficient-funds | 400 | available < amount on debit, hold, or transfer |
problems/invalid-amount | 400 | amount <= 0 or non-integer |
problems/hold-limit-exceeded | 429 | Concurrent holds on wallet > 100 |
problems/reversal-window-expired | 400 | Original transaction older than 365 days |
problems/double-reversal | 400 | Transaction already reversed |
problems/hold-not-reversible | 400 | Attempting reversal on a held transaction — use cancel |
problems/idempotency-conflict | 409 | Same idempotencyKey, different request body |
problems/plan-limit-exceeded | 402 | Transaction amount exceeds Billing plan limit |
problems/billing-unavailable | 503 | Billing down + plan limits not cached |