Docs/Ledger APIv0.0.1-preview
Accounts
Creating accounts, flags and limits, funding through control accounts, and the account lifecycle.
Accounts are created explicitly, behave according to flags declared at creation, and are never deleted — the account space is part of the audit record.
Creating an account
An account create carries: a 128-bit id (caller-chosen), ledger_id, currency (ISO 4217 numeric), tier, flags, optional limit_low / limit_high, and an optional participant_id for grouping accounts under an owning participant.
Every id here is a number: account ids are 128-bit integers, ledger_id and source_id are 32-bit integers — in the SDKs you pass integers, never strings. An account is named by its full key, (ledger_id, currency, id): every read or update of an account names all three, and a key that names no account answers ERR_NOT_FOUND. In this release the id is also still unique across the engine — creating id 100 under a second ledger or currency, or re-creating it with different attributes, answers ERR_ACCOUNT_CONFLICT (44) — so partition your id space by ledger or tenant up front. An identical retry of the original create is OK_DUPLICATE, even after later limit updates. The one string identifier in the system is a transfer’s txn_id, your business reference. (Over the REST gateway, account ids travel as decimal strings like "1001" — a JSON-safety convention for the full 128-bit space, not a naming scheme.)
from tuladb.models import AccountCreate
c.create_account(AccountCreate(
id=1001,
ledger_id=1,
currency=840,
flags=1, # DEBIT_NORMAL
limit_low=0, # never below zero
limit_high=1_000_000_000_000,
))
The wire protocol has an account batch operation — up to 1,024 creates per request, with a per-item status for each — but in this release every SDK and tulactl sends one account per call, and no REST gateway exposes a bulk create. Provision accounts in a loop (or pipeline the calls) for now.
Flags
| Flag | Bit | Behaviour |
|---|---|---|
DEBIT_NORMAL |
1 | Normal balance side is debit (a label — see below) |
CREDIT_NORMAL |
2 | Normal balance side is credit (a label — see below) |
CONTROL |
4 | Control account for funding/defunding (immutable) |
SETTLEMENT |
8 | Settlement-tier account (immutable) |
ALLOW_NEGATIVE |
16 | Posted balance may go negative |
CLOSED |
32 | Terminal — no further activity |
NO_DEBIT |
64 | Refuses debits |
NO_CREDIT |
128 | Refuses credits |
INACTIVE |
256 | Temporarily refuses activity (reversible) |
The common default is 3 (DEBIT_NORMAL | CREDIT_NORMAL). DEBIT_NORMAL and CREDIT_NORMAL are labels for your own reporting: the engine stores them but no balance, limit or sign rule reads them — limits and ALLOW_NEGATIVE decide what an account may do.
Limits
limit_low and limit_high bound the account’s balance, enforced by the engine on every mutation. A transfer that would cross a limit is declined with ERR_LIMIT — and the decline is recorded like any other rejection.
Limits can be updated after creation — one side per request, so an audit trail shows exactly which bound changed:
from tuladb.models import AccountKey
key = AccountKey(ledger_id=1, currency=840, id=1001)
c.update_account_limit_low(key, 0)
c.update_account_limit_high(key, 5_000_000)
participant_id is create-only and never mutable — it’s identity, not configuration.
Funding and control accounts
Value enters and leaves a ledger through control accounts. Seeding a customer account is itself a balanced transfer from a control account, so double-entry holds even at the boundary:
tulactl create-account --id 1000 --ledger 1 --currency 840 --control --credit-normal
tulactl seed --control 1000 --id 1 --amount 100000 --ledger 1 --currency 840 --source 1001 --txn demo-seed-1
There is no “set balance” operation anywhere in the API — a balance is only ever the sum of its postings.
Lifecycle
Deactivate an account to refuse new activity while keeping history queryable, and reactivate it later:
c.set_account_active(key, False) # transfers now decline with ERR_DEBIT_ACCOUNT_INACTIVE (26)
# or ERR_CREDIT_ACCOUNT_INACTIVE (27)
c.set_account_active(key, True)
CLOSED is set only at creation (tulactl create-account --closed, or the flag on AccountCreate); there is no call that closes an existing account, and set_account_active on a closed account answers ERR_CONFIG. To retire an account in use, deactivate it.
Creation and limit changes are kept as an ordered account metadata history, queryable per account: each event carries its timestamp and the old and new limits. Activation changes are not in it, and no event records who made the change — for that, use the console’s audit log, or the engine’s request audit log.
Reading accounts
balance = q.get_balance(key) # six-field Balance snapshot
rows = q.list_account_balances(1, 1001) # every currency id 1001 holds in ledger 1
total, accounts = q.list_accounts(offset=0, limit=256) # total = every account, not just this page
events = q.get_account_history(key) # metadata event trail
Account-addressed calls take the key, never a bare id (the Python SDK raises TypeError on an id-only call rather than guess which account it meant).
Balance reads reflect applied state on the serving node. q here is a query-port client, where a balance is a local read and not linearizable unless cluster_linearizable_query_reads is enabled; when the answer gates a spend, read it on the client port (c), which is linearizable by default. Listing and search go through derived indexes — see Queries for the consistency contract.