Docs/Ledger APIv0.0.1-preview
Error reference by surface
Every error a TulaDB surface can return, surface by surface — REST gateway responses, SDK error types, tulactl, tuladbd and tool exit codes, the Iceberg exporter and the operations console.
Errors and rejections covers the engine’s status codes, the reject reason codes and the retry rules. This page lists what every other surface returns: the HTTP responses of the REST gateways, the error types each SDK raises, and the exit codes of the command-line tools, the daemon and the exporter. Every row was checked against the code of the current release.
Where one surface wraps another, the inner code survives. A gateway response carries the engine status number in ledgerd_error.status_code or ledgerd_response.status; an SDK error carries it as status_code or Status; tulactl prints it on stderr. Look up the number in the status code table.
What each write records
Whether a refusal leaves a durable rejected record depends on the write type as much as on the code. The rule and the per-code table are in Durable and non-durable refusals; this is the same answer by write type. Each row’s behaviour was reproduced against a running engine; codes a single node can’t produce follow the engine source, and the per-code table says which.
| Write | Recorded as a rejected transaction | Answered and not recorded |
|---|---|---|
| Transfer (single or batch item) | Every non-retryable verdict reached after the duplicate check: 2, 5, 6, 9, 11, 15, 23–27, 32–34, 42 (reason 0xFFFF002A) |
Every retryable status (4, 10, 12, 13, 18, 38, …); refusals before an identity exists: 8, 19 (reserved ledger id), 36; server-side refusals: 35, the query-port refusal (19 or 37); 17 from a halted node |
| Reserve (single or batch item) | The same verdicts as a transfer. Its record has phase VOIDED, so the hold never existed |
As for a transfer |
| Commit / rollback, by transfer id or by key | Never. A lifecycle refusal is answered from the reservation’s own record and adds nothing to it | 9 for an identity never reserved (it can still be reserved afterwards); 10 after the other side won, or for a reserve that was declined; 39 after the reservation expired — the record’s EXPIRED phase is the durable fact; 21 for a hold the node can’t read yet (from code) |
Explicit reject (TXN_FLAG_REJECT) |
Always 41, with your reject_reason_code verbatim. A transfer later sent under the same identity is answered 41 |
36 for a malformed reject (an engine-reserved reason code, a reason without the flag); shed with 4 or 38 like any write. A reject of an identity that already has an outcome answers that outcome (1 for a posted transfer) and records nothing |
| Tiered transfer / reserve (single or batch item) | The same verdicts across all four legs, plus 3 (a leg on the wrong tier) and 19 when a position leg and a settlement leg name the same account. A cap breach records reason 0, not 0xFFFF002A |
As for a transfer, plus 19 when tiered transfers are disabled and 36 for a reject flag on a tiered frame (from code) |
| Tiered commit / rollback | Never | As for commit and rollback |
| Batch | Each item is decided on its own and follows its write type’s row | The same identity twice in one batch: the first item’s verdict counts and the later one answers 10. A refusal of the whole frame (35, 38, the query-port refusal) gives every item the frame’s status, and none is recorded |
| Account create | No rejected-record concept | Every refusal: 3, 5, 15, 22, 23, 44, capacity. A retry is decided against the current state — the same create succeeds once the ledger is registered, and an identical re-create of an existing account answers 1 |
Registry and admin writes — create ledger, source or currency; activate or deactivate an account, source or currency; limits, max_txn_amount, reserve expiry, source allowlists |
No rejected-record concept | Every refusal, decided against the current state. A conflicting re-registration answers 16; a log write that fails answers 12 (reproduced with a failing log), and a retry is decided afresh |
Two consequences are worth designing for:
- A recorded decline keeps its identity. Fixing the cause — funding the account, registering the source, lifting the cap — doesn’t change the answer for the old
txn_id. Submit the corrected payment under a new one. - A decline that couldn’t be recorded isn’t reported as that decline. If the engine can’t write the rejected record (the log write fails), it answers
18or the log’s own status instead of the business reason, so a client never sees a final-looking decline that nothing remembers. Retry under the same identity; the request is then decided, and recorded, afresh.
REST gateway responses
All three REST gateways — Python, Java and C — map engine statuses to HTTP with the same table. The table is the contract in the SDK’s contracts/status-codes.env.
When the engine answered
| Engine answer | Submit (transfer, reserve, reject, commit, rollback, tiered, /v1/transactions) |
Read or admin call |
|---|---|---|
0 OK |
200, ok: true |
200 |
1 OK_DUPLICATE |
409, error: "duplicate_txn" |
— |
41 on an explicit reject |
200, ok: true — the reject is the recorded outcome |
— |
44 ERR_ACCOUNT_CONFLICT |
409, error: "account_conflict" |
409, error: "account_conflict" |
43 ERR_HISTORY_PURGED |
410, error: "history_purged" |
410, error: "history_purged" |
| a retryable status | 503, error: "ledgerd_error", Retry-After: 1 |
the same |
16, 17, 19, or a code the gateway doesn’t know |
502, error: "ledgerd_error" |
the same |
| any other status (a decline) | 200, ok: false, no error key |
404, error: "ledgerd_error" for 9 ERR_NOT_FOUND; 422, error: "ledgerd_error" otherwise |
Whenever the engine answered, the body has ledgerd_error with status_code (the number), status and status_name (both the name), retryable and message. A submit also carries ledgerd_response with the engine’s status number.
The 409 duplicate_txn body carries duplicate: true, status_code: 1, status: "OK_DUPLICATE" and the original transfer’s transfer_id, phase, batch_seq and batch_index. The write happened — treat it as success.
A 502 means the engine couldn’t decide: an invariant, corruption or configuration fault, or a status newer than the gateway. Alert on it; don’t retry blindly.
When the engine didn’t answer
| HTTP | error |
When | What to do |
|---|---|---|---|
503 |
ledgerd_connect_error |
The gateway couldn’t connect to the engine, or the connection dropped or timed out mid-request. No Retry-After and no ledgerd_error: there is no engine answer to carry |
A write’s outcome is unknown. Retry with the same txn_id |
Refused by the gateway
None of these reached the engine, so nothing was recorded. Validation refusals (error: "invalid_request") carry a code from the refusal catalogue; the others are identified by their HTTP status and error alone.
| HTTP | error |
code |
When | Gateways |
|---|---|---|---|---|
400 |
invalid_request |
REQ_* |
The request’s shape is wrong: see the catalogue | all |
400 |
invalid_request |
invalid_amount |
Decimal amounts (feat/decimal-amounts). amount_decimal can’t be converted. reason is invalid (not a decimal string — a JSON number is refused too), too_many_decimals (more fractional digits than the currency’s scale) or overflow (too large for the engine’s integer); field names the field, scale is the currency’s scale |
Python, Java, C |
400 |
invalid_request |
currency_not_registered |
Decimal amounts (feat/decimal-amounts). An amount_decimal names a currency with no registered scale, so it can’t be converted. Carries field and currency |
Python, Java, C |
400 |
invalid_request |
REQ_FIELD_CONFLICT |
Decimal amounts (feat/decimal-amounts). Both amount and amount_decimal were sent; fields: ["amount", "amount_decimal"] |
Python, Java, C |
401 |
unauthorized |
— | API-key authentication is on and the key header (X-API-Key by default) is missing or wrong. Java also answers this when authentication is on but no key is configured |
all |
403 |
forbidden |
— | The caller’s role (read-only, ledger operator, cluster admin) doesn’t allow the route | C only |
404 |
not_found |
— | No such route. Python answers FastAPI’s {"detail": "Not Found"} instead |
all |
405 |
method_not_allowed |
— | Wrong HTTP method. Python answers {"detail": "Method Not Allowed"}; C checks the method only on keyed-account routes |
all |
413 |
payload_too_large |
— | Body larger than TULADB_SDK_SERVER_MAX_BODY_BYTES (64 KiB by default) |
C only |
422 |
invalid_request |
REQ_* |
A batch whose envelope is unusable: transfers missing, empty, not a list, over 1,024 items, or not JSON. A malformed item is reported at its index in a 200 instead |
Python, Java (C has no batch route and answers 404) |
429 |
rate_limited |
— | Over the per-client requests-per-minute limit | all |
500 |
internal_error |
— | An unhandled failure inside the gateway | all |
503 |
auth_not_configured |
— | Authentication is on but no API key is configured (Java answers 401 instead) |
Python, C |
503 |
no_route |
— | An admin call (create ledger, currencies) with no engine route configured. Python answers 503 invalid_request with REQ_INVALID here; Java answers 502 |
C |
No gateway answers 504.
The decimal-amount codes are on the feat/decimal-amounts branches of all three gateways (Python, Java and C) and ship with the next gateway release; see Decimal amounts. They are the first gateway codes that don’t start with REQ_; they still never start with ERR_.
Inside a batch
A batch answers 200 with one result per item, in order. An item refused by the gateway carries error: "invalid_request" and its code at its index; an item the engine answered carries status, status_name, retryable and ledgerd_error. If the engine refuses the whole batch frame, every item gets the frame’s status. If the connection fails, every item gets error: "ledgerd_connect_error" and retryable: true — resubmit them under the same identities.
Where the gateways differ
Next gateway release. The three gateways’
fix/error-consistencybranches implement one error contract and pass the same 53-case shared contract suite, with and without an engine. The released gateways differed on the points listed at the end.
With those branches, every refusal has the same HTTP status, error, code and field on all three gateways:
-
One body shape for every non-2xx:
ok: false,error,message(pluscode,field/fieldswhere they apply). Nohttp_statusfield. A200 ok: falsedecline carries no top-levelmessage; the text is inledgerd_error.message. -
invalid_requestonly with400(422for a batch envelope). A missing SDK capability is501 not_implemented; an admin call with no engine route is503 no_route; the engine unreachable is always503 ledgerd_connect_error, never502. -
Same cause, same code: a missing account id →
REQ_FIELD_CONFLICTwith every id field of the mode infields; an ambiguous route →REQ_ROUTE_AMBIGUOUSwithcandidatesas a count; an unknown or unrouted currency →REQ_FIELD_INVALID_VALUEwithfield: "currency"(orledger_id); malformed JSON or a missing/wrongContent-Type→REQ_INVALID; a wrong JSON type →REQ_FIELD_INVALID_VALUEwith the bare field name. -
Limits:
max_scan1–1,000,000 (default 65,536); bodies over 64 KiB (1 MiB on batch routes) →413 payload_too_large; a wrong method →405withAllow. -
Access checks in one order:
OPTIONS,503 auth_not_configured,429(withRetry-After),401,403,404/405,413,400. Python and Java refuse to start if role-grant settings are present, because only C enforces roles. -
Integer fields take JSON integers only (breaking in the next release):
"amount": "800",1.5,1.0ortrueisREQ_FIELD_INVALID_VALUEnaming the field, on every gateway and every batch item. A string amount would be ambiguous withamount_decimal("800"means 800.00), so the message points there. Account ids stay decimal strings; a bodycurrencytakes an integer or an ISO alpha-3 code ("USD"), never a digit string; ledger create also takes a custom unit name. URL query parameters are unchanged.
The three gateways now answer every request covered by the contract identically.
Released gateways: Python answered a missing account id or an ambiguous route with REQ_INVALID, used invalid_request on 501/503, returned FastAPI {"detail": …} bodies on 404/405, prefixed query fields with query. and capped max_scan at 65,536; Java added http_status, listed REQ_ROUTE_AMBIGUOUS candidates as ids, answered an unknown currency with REQ_INVALID (a 500 on a query parameter) and auth-without-key with 401; C served some wrong-method requests and read malformed JSON as missing fields.
SDK error types
Every SDK separates three failures: an engine status (the ledger answered, and the answer was not success), a transport failure (the connection failed or dropped — a write’s outcome is unknown), and a protocol error (the reply could not be decoded — the SDK resets the connection). A declined write inside a batch is not an error in any SDK: it is a per-item status in the result.
The final and retryable sets are the same in all five SDKs and are tested against the shared contract. A code the SDK does not know is classed other, never success.
Python
| Type | Parent | Carries | Raised when |
|---|---|---|---|
LedgerError |
RuntimeError |
message; on an engine status also status_code, status_name, msg_type and result |
The engine answered a non-success status for a call ("tuladb returned msg_type=130: status=N …"), a transport failure ("ledgerd connection failed: …"; the socket is reset), or a client-side failure such as a closed pool or a leader that can’t be found |
LedgerProtocolError |
LedgerError |
message | A reply failed framing, CRC or size checks ("invalid response frame: …"). The socket is reset |
LedgerTxnStatusError |
LedgerError |
status_code, status_name, result |
Only from raise_for_result(), which you call yourself to turn a non-accepted result into an exception |
LedgerDuplicateTxnError |
LedgerTxnStatusError |
as above | raise_for_result(…, allow_duplicate=False) on status 1 |
Bad arguments raise ValueError or TypeError before anything is sent. The leader-aware SmartLedgerClient raises a final status at once and re-routes and retries after a transport or protocol error or a “not the leader” answer. On a transfer, reserve, tiered write or reject it returns an other status to you instead of retrying it (next SDK release; see Retrying a declined write).
Go
| Type | Carries | Returned when |
|---|---|---|
*tuladb.LedgerError |
Status int32, Message string |
The engine answered a non-success status. The only error that leaves the connection open |
*tuladb.TimeoutError (matches tuladb.ErrTimeout) |
Op (connect, read, write), Window, Err |
A connect, read or write timeout. The connection is closed |
tuladb.ErrNotConnected |
— | A call on a closed or dropped client; when wrapped as “client closed during the call”, the outcome is unknown |
*tuladb.ProtocolError (matches tuladb.ErrProtocol) |
message | A reply that isn’t an engine answer, or failed framing (next SDK release; the released SDK returned a plain error, and status 0 for an unexpected message type). The connection is dropped |
tuladb.StatusIsFinal, StatusIsRetryable, ErrorIsFinal and ErrorIsRetryable classify; the Error* forms return false for anything that isn’t a *LedgerError. tuladb.Retry retries retryable statuses and every non-LedgerError (treated as transport).
Java
| Type | Parent | Carries | Thrown when |
|---|---|---|---|
LedgerStatusException |
LedgerProtocolException |
statusCode(), statusName(), msgType(), result(), isFinal(), isRetryable() |
The engine answered a non-success status (next SDK release; catch blocks for LedgerProtocolException still catch it) |
LedgerProtocolException |
IOException |
as above; statusCode() null for a protocol failure |
In the released SDK, the engine-status exception. From the next SDK release it is also thrown, with no status, for a reply that isn’t an engine answer — retried like a transport failure |
LedgerTimeoutException |
SocketTimeoutException |
phase (CONNECT, TLS_HANDSHAKE, WRITE, READ), timeout |
A timeout. The connection is closed |
NotConnectedException |
IOException |
— | Nothing was sent, so retrying is safe |
IOException |
— | message | Framing failures ("bad wire magic", CRC mismatch, short header), TLS setup, a closed client, leader discovery failure |
LedgerTxnStatusException |
RuntimeException |
statusCode(), status() |
Only from assertAccepted(), which you call yourself |
LedgerDuplicateTxnException |
LedgerTxnStatusException |
as above | assertAccepted(result, false) on status 1 |
Bad arguments throw IllegalArgumentException.
Rust
One enum, tuladb::Error:
| Variant | Carries | Returned when |
|---|---|---|
Error::Ledger |
status, message, result (the declined result, when there is one) |
The engine answered a non-success status |
Error::Io |
std::io::Error; is_timeout(), is_not_connected() |
Transport failure |
Error::Protocol |
message | A reply that failed framing checks or isn’t an engine answer (the released SDK reported status 0 for an unexpected message type), client-side validation, or TLS setup |
is_final(), is_retryable() and status() classify. RetryPolicy::retry retries Io and retryable Ledger errors, never Protocol.
C
Functions return 0 on success and -1 on failure. After a failure:
ledgerd_client_last_status()is the engine status, orLEDGERD_STATUS_UNKNOWN(-1) when the failure happened on the client side — connect, timeout, framing or CRC.ledgerd_client_last_error()is a message for a log. It is not a contract.- Transport failures also set
errno(ETIMEDOUT,ECONNRESET,EIO,ENOTCONN). - Batch items the engine never answered carry
LEDGERD_STATUS_UNKNOWNas their status.
The status constants run from LEDGERD_STATUS_OK (0) to LEDGERD_STATUS_ERR_RECONFIG_NO_QUORUM (48); ledgerd_status_is_final() and ledgerd_status_is_retryable() classify.
Unknown codes
For a status number newer than the SDK, every SDK returns the name ERR_<n> (for example ERR_49), keeping the number, and classes it as other, never success. (In the released SDKs, C, Go and Rust returned ERR_UNKNOWN; the next SDK release aligns them.)
Retrying a declined write
An SDK’s automatic retry follows one rule in every language: on a transfer, reserve, tiered transfer or reject, it retries only a retryable status or a transport/protocol failure. An other status is returned to you, because on these writes most of them are durable: retrying the same txn_id would replay the same decline forever. Fix the cause and submit with a new txn_id. (The leader-aware Python, Java and C clients auto-retried other statuses in the released SDKs; the next SDK release stops it. Go’s Retry and Rust’s retry already followed the rule.)
tulactl exit codes
Next release. These exit codes are on core branch
fix/exit-codesand ship with the next engine release; the released 0.0.1-preview behaviour differs where noted.
tulactl never exits with the engine status itself. A refused request prints <command> failed: server error N: NAME on stderr — the number is there for scripts. A duplicate submit exits 0 and prints ↺.
| Exit | Meaning |
|---|---|
0 |
Done. Also help and --version |
1 |
The engine answered with a non-success status, or the operation was refused or not confirmed |
2 |
Usage error: unknown command, missing or invalid arguments. Checked before connecting, so it is the same answer whether or not a daemon is running |
3 |
Cannot connect, or a transport error. For offline commands, the input file can’t be read |
Some commands have codes of their own, meant for scripts and probes:
| Command | 0 |
1 |
3 |
4 |
|---|---|---|---|---|
health |
reachable, ping answered | reachable, ping failed | unreachable | — |
ready |
leader known, lag within --max-lag |
not ready: no leader, catching up, or the status call failed | unreachable | — |
topology check |
OK | WARN | file unreadable | REJECT |
join-status |
no join in force, history OK | a join in force, history not OK, or a metadata step busy | journal unreadable | — |
digest-at-cut |
captured | refused | transport failure | answered without a digest: too late, not reached, busy, multilane, unavailable |
upgrade-status |
every node reachable | a node didn’t answer | — | — |
upgrade-finalize |
finalised, or nothing to finalise | refused (nothing changed), aborted, or activation failed | cannot connect | — |
autotune-plan |
plan printed | a voter didn’t report memory, or nothing fits | — | — |
history-floor-clear |
no copy still has a logical floor | refused, or a floor remains | — | — |
reconfig (offline) |
done | refused because the node is live | cluster_config.dat unreadable |
— |
Usage errors are 2 on every command. In the released 0.0.1-preview, unreachable was 2 for health and ready, digest-at-cut used 3 for “not captured”, bad arguments exited 1 on some commands, and an unknown command was reported only after connecting.
See tulactl for the commands themselves.
tuladbd exit codes
Next release. These exit codes are on core branch
fix/exit-codesand ship with the next engine release; the released 0.0.1-preview behaviour differs where noted.
| Exit | Meaning | What to do | Restarted by the systemd unit? |
|---|---|---|---|
0 |
Clean exit: orderly shutdown on SIGTERM/SIGINT, --help, --version, or a successful one-shot mode such as --check-config |
— | no |
1 |
A start or run failure a restart may clear: data directory unusable or already open, initialisation or recovery failed, a listener couldn’t bind, or a refused --autotune-rate / --autotune-probe |
Read the FATAL log line and fix the cause |
yes |
2 |
Engine halted (fail-stop) — and only that. The log has FATAL [daemon] engine_halted <reason>; the final metrics snapshot shows engine_halted=1 |
Read the reason, then restart; recovery replays from durable state. If the same reason repeats, keep the node down and investigate | yes |
3 |
--check-config --memory-plan-json couldn’t write its JSON |
— | — |
64 |
Usage error (EX_USAGE): unknown option, missing value, bad --assume-memory |
Fix the command line | no |
78 |
Configuration error (EX_CONFIG): the configuration or an environment override didn’t load or validate (including the TLS-required checks), it doesn’t fit memory with memory_fit_check = 2, node_addr can’t be bound, two ports collide, or autotune flags without autotune on |
Fix the configuration | no |
killed by SIGBUS |
A store into a memory-mapped data file failed — almost always a full disk; stderr says “most likely the disk is FULL” | Free disk space before restarting. Normally the free-space reserve refuses writes with ERR_CAPACITY long before this |
yes |
The shipped systemd unit restarts on failure (five starts in ten minutes) and sets RestartPreventExitStatus=64 78, so a bad command line or configuration stops instead of looping. Never add 2 to that list: a halted engine must restart and recover. --check-config exits 78 on an invalid configuration; a valid one that doesn’t fit this machine’s memory exits 1. In the released 0.0.1-preview, usage errors exited 2 (the same code as a halt) and configuration errors 1.
Halt reasons are event names in the FATAL log line, for example posted_undo_failed, pending_commit_undo_failed, rollback_release_failed, snapshot_install_partial, recovery_integrity_fail_closed, replay_capacity_refused, dedup_index_coverage_gap, apply_gap_unknown_state, stranded_halt and failed_then_kept_halt. The last two fire only with quorum_failstop_halt_on_stranded on, which is the default. See Resilience for how fail-stop fits the recovery design.
Operator tool exit codes
Every operator tool follows one scheme: 0 OK, 1 failed (including an unreadable input), 2 usage — plus the tool-specific codes below. (tuladb_chain_verify usage errors were 1 and a failed lane 2 in the released 0.0.1-preview.)
| Tool | 0 |
1 |
2 |
Other |
|---|---|---|---|---|
tuladb_backup |
OK (verify: OK, verify-restore: OK) |
failed or refused: output exists without --force, no durable snapshot for online-full, restore target open by a running daemon or not empty |
usage | — |
tuladb_upgrade |
IN-PLACE UPGRADE SAFE, or ROLLBACK SAFE with --compat-with |
ROLLBACK NOT SAFE, MIGRATION REQUIRED, or no recognisable store |
usage, missing or unreadable data directory, config failed to load | — |
tuladb_verify_store |
verify-store: PASS |
verify-store: FAIL with a recovery_error:, consistency_error: or deep_verify_error: line |
usage, missing directory | — |
tuladb_audit_verify |
audit-verify: PASS |
audit-verify: FAIL, including an unreadable log |
usage, bad HMAC key | — |
tuladb_chain_verify |
OK | a lane invalid, or no lane WAL directories | usage | 3: EXPECT FAIL |
wal_seq_dump |
no gaps | gaps found | usage, WAL won’t open, no frames in range | — |
tuladb_export_txns |
OK | config, open or write failure | usage | — |
tuladb_prometheus_exporter |
--once rendered |
--once failed |
bad flag | — |
ROLLBACK NOT SAFE and MIGRATION REQUIRED are answers, not faults: the tool read the store and is telling you not to proceed. Don’t downgrade or wipe on a non-zero exit.
tuladb-installer exits 0 on success and 1 on every refusal or failure, printing tuladb-installer: ERROR: <reason> (2 for an internal error); a failed install rolls back what it installed (see Install TulaDB). Interrupted by Ctrl-C or SIGTERM, it rolls back and exits 130 or 143. tuladb-installer status exits 0 ready, 3 not installed, 4 unreachable, 5 not ready. The evaluation-stack installers (install.sh, install-native.sh, install-cluster.sh, install-eks.sh) exit 0 on success and 1 on every refusal, printing ✗ <reason> — for example a checksum mismatch, a failed signature check on the checksum manifest, a script piped from stdin instead of downloaded and verified, or an unsupported platform. Any other failing command exits with that command’s status.
Iceberg exporter exit codes
The exporter reserves three codes for conditions a restart can’t fix. The shipped systemd unit lists them in RestartPreventExitStatus=86 87 88, so it stops instead of looping. Kubernetes can’t exempt exit codes, so there they show as CrashLoopBackOff: scale the exporter to zero while you act.
| Exit | Meaning | What to do |
|---|---|---|
0 |
Clean exit (SIGTERM), or the command succeeded; status while running |
— |
1 |
A command failed (run failed:, bootstrap failed: …), no tables, a stale metrics --once, an incomplete watermark, clear-durability-halt without --confirm, or an unhandled exception |
Read the log; restartable |
2 |
Usage error or refusal: bad arguments, reconcile not OK, bootstrap refused:, watermark publishing disabled |
Fix the arguments or the configuration |
3 |
status: not running (stale or missing pid) |
— |
86 |
Durability halt. A compaction or repair commit landed but couldn’t be made durable. A DURABILITY_HALT marker sits next to the checkpoint, and run, start, once, repair, bootstrap, compact --execute and cleanup --execute refuse with 86 while it exists |
Check the disk and the table (reconcile), then clear-durability-halt --config … --confirm |
87 |
Source range reclaimed. The ledger answered ERR_EXPORT_RANGE_RECLAIMED (40): the export cursor is below the WAL reclaim floor, so the gap between them is gone from the live log. stderr names the gap |
A decision, not a retry: reseed at the floor into a new table generation and accept the gap, or bootstrap a fresh pipeline |
88 |
Partition-group conflict. The ledger answered ERR_EXPORT_GROUP_CONFLICT (47) with a sub-code other than 71, or the checkpoint was recorded under a different group size or epoch |
Fix the group configuration (index, size, pipeline id, principal) or forget the stale keys on the ledger |
ERR_EXPORT_GROUP_CONFLICT with sub-code 71 means “not the leader”: the exporter retries it against the leader, and exits 1 only if that retry also fails.
Operations console API
Next console release. This mapping is on the console’s
fix/error-consistencybranch; the released console answered engine declines and unreachable-engine failures with400.
The operations console’s JSON API answers errors as {"ok": false, "error": "<message>", "request_id": "…"}, with code (and sometimes field) added where one applies. Two exceptions: the role check and the sign-in route answer {"detail": "<message>"}, so read error or detail. Unhandled failures are 500 with error: "internal_error". Roles rank viewer, then operator (moves money), then admin (changes the ledger’s shape).
| HTTP | error / code |
When |
|---|---|---|
200 |
ok: false, error: "declined", declined: true, status_code, status, status_name, retryable: false |
The engine declined a submit (transfer, reserve, tiered, funding, commit, rollback, reject) — the same rule as the REST gateways: a decline is an answer, not an HTTP error. A recorded explicit reject is 200 ok: true with reject_recorded: true (status 41), first time and on replay |
404 / 422 |
ledgerd_error with status_code |
The engine refused an admin call (account create, limits, activation, ledger/source/currency registry): 404 for status 9, otherwise 422 |
400 |
code from the gateway catalogue (REQ_FIELD_OUT_OF_RANGE, REQ_REASON_CODE_RESERVED, …) |
The console refused the request on its shape |
503 |
ledgerd_connect_error, code: "LEDGER_UNREACHABLE", retryable: true, outcome_unknown on submits |
The ledger couldn’t be reached during a write. The outcome is unknown; retry with the same txn_id |
503 |
ledgerd_error, Retry-After: 1 |
The engine answered a retryable status (4, 10, 12, 13, 14, 18, 21, 38, 48). Nothing was written |
502 |
ledgerd_error |
An engine fault status (16, 17, 19) or a code the console doesn’t know |
401 |
unauthorized |
No valid session or API key. A failed sign-in answers invalid username or password |
403 |
<role> role required; you are <role> |
The caller’s role is too low for the route |
404 |
transfer not found, no such report, … |
No such object |
409 |
duplicate_txn, ok: false, duplicate: true, status_code: 1, the original’s identity, original_result_code, request_id |
The submit was a replay of an applied write; the effect exists. On a reject, original_result_code 41 means the rejection was already recorded; any other value means the txn_id belongs to a posted transfer and no rejection was recorded |
409 / 410 |
account_conflict / history_purged |
Status 44 / 43 |
409 |
RECONCILE_BUSY, IN_USE, LAKE_CONFIG, ledger_id already exists |
A conflict with the console’s current state |
413 |
request body too large |
Body over LEDGER_DASHBOARD_MAX_BODY_BYTES (1 MiB by default) |
422 |
code from the gateway catalogue, field, details |
The body failed schema validation |
429 |
LAKE_BUSY |
Too many lake queries at once. The only rate limit in the console |
502 |
engine … failed: … |
An engine listing call failed |
503 |
audit log unavailable; refusing to act unrecorded |
LEDGER_DASHBOARD_AUDIT_REQUIRED is on and the audit sink isn’t ready. Every mutating request is refused, before authentication |
503 |
LEDGER_UNREACHABLE |
The ledger is down and the answer needs it |
503 |
dashboard API key is required in prod |
ENV=prod with no API key configured |
Lake query errors carry LAKE_* codes: 400 for a refused statement (LAKE_SQL_SYNTAX, LAKE_STATEMENT_REFUSED, LAKE_SQL_TOO_LONG, …), 408 or 504 for LAKE_TIMEOUT, and 503 when the lake isn’t configured or available (LAKE_UNAVAILABLE, LAKE_DISABLED, LAKE_THROTTLED, …).