SeptemCore LogoSeptemCore
API ReferenceREST API

Money REST API Reference

Money Service REST API reference. 13 endpoints across wallet CRUD, balance, credit/debit, hold/confirm/cancel (two-phase), transfer, reversal, and transaction history. All amounts in cents (integer). ACID guarantees. Idempotency required on all mutation operations.

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

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 /holds/:id/confirmConfirm hold → final debit (step 2a)
POST /holds/:id/cancelCancel hold → return to available (step 2b)
POST /transactions/:id/reversalReverse a completed transaction
POST /transfersTransfer between two wallets
GET /transactions?walletId=:idTransaction 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/holds/:id/confirm

Confirms a hold — funds are deducted permanently.

POST https://api.septemcore.com/v1/holds/01j9ptx0000000000000002/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/holds/:id/cancel

Cancels a hold — returns funds to available.

POST https://api.septemcore.com/v1/holds/01j9ptx0000000000000002/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/transfers

Atomic transfer between two wallets in one PostgreSQL ACID transaction.

POST https://api.septemcore.com/v1/transfers
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/transactions/:id/reversal

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

POST https://api.septemcore.com/v1/transactions/01j9ptx0000000000000001/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/transactions?walletId=:id

Keyset-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/transactions?walletId=01j9pwal0000000000000001
  ?page_size=20
  &page_token=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
page_sizeDefault 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

On this page