The API owns monetary rounding for single and batch ingestion. No payload redesign or client-side pre-rounding is required. Extra decimals are accepted; JSON numeric strings are not.
Nearest cent, halfway away from zero
| Submitted JSON number | Normalized amount |
|---|---|
10.254 | 10.25 |
10.255 | 10.26 |
-10.255 | -10.26 |
1.005 | 1.01 |
-0.004 | 0.00 |
1.005e1 | 10.05 |
Amounts above are shown at two-decimal storage scale; clients send numbers, not strings. Negative examples apply only where the field permits negatives. Zero has no negative sign.
Rounding uses decimal arithmetic on the shortest decimal representation of the parsed JavaScript number, not Math.round(value * 100). Scientific notation is accepted. Precision already lost while constructing or parsing the JSON number cannot be recovered, even with a decimal library.
Fields and bounds
Rounded fields: disbursement_amount, gross_value, upfront_fee, total_expected_margin, approved_amount, operation_revenue, operation_cost, asset_value, depreciation_amount, payment_total, payment_principal, payment_fee.
- Values must be finite JSON numbers.
"10.255"is not accepted as an amount. - Rounded values and derived sums must fit
NUMERIC(15,2):-9999999999999.99to9999999999999.99. Overflow returns400; extra decimals alone do not. - Existing sign restrictions apply to the original submitted value even if it would round to zero. Other numeric bounds are checked after rounding.
- Ratios such as
insurer_coverage, counts, and values insideinfoare not rounded as money. Optional null/omission behavior remains unchanged.
Round components before deriving amounts
Rounding happens before storing the event, calculating derived amounts, hashing business content, and writing domain rows.
For example, a loan create can contain:
{
"category": "disbursement",
"data": {
"step": "create", "disbursement_id": "DSB-ROUND-001", "kind": "loan",
"client_id": "COMPANY-001", "disbursement_amount": 10.005, "upfront_fee": 10.005,
"currency": "USD", "total_expected_margin": 1,
"disbursement_date": "2026-09-14", "maturity_date": "2026-12-14"
}
}The client must already exist. With gross_value omitted, the two components each round to 10.01, producing 20.02, not 20.01. The default fee is zero; default gross value is amount plus fee. Explicit gross values are also rounded.
Content hashes are not request idempotency
Use the shared hash module and calculator to check canonical output. API 4.2.0 also repairs creditor inclusion in disbursement hashes under version 2; that separate repair needs an operator-run backfill. The no-backfill statements here refer only to monetary rounding.
Otherwise identical updates with 10.251, 10.254, or 10.25 normalize to the same 10.25 business value. If that content is already stored, an update can return 200 with action: unchanged and no domain side effects.
But reusing one key first with 10.251 and then with 10.254 returns 409 IDEMPOTENCY_KEY_REUSED. Request idempotency identifies the original submitted content, not its rounded equivalent. Preserve the original number and key for exact retries.
Content-hash version remains 2. Existing cent-valued records retain their canonical hashes: this rounding update alone requires no hash-version bump or backfill. See synchronization and the v2 vectors if you compute hashes locally.
