Description
Purpose and use
Currencies define which fiat and crypto denominations the bank can hold, display, and validate, and the precision rules that govern how amounts in those denominations are stored and shown. This module is the catalog — the reference list every account balance, transfer amount, tariff, and ledger posting checks against. It does not hold exchange rates: FX pricing and conversion live in the FX module.
Who uses this. Treasury, operations, product managers, finance controllers, and support consult currency records when activating a new money type or explaining amount precision. Maintaining the catalog is an administrative action gated by role.
How it works. Each currency carries its identity (code, numeric code, name, symbol), display metadata (format, icon, priority), activity status, and precision rules. Those precision rules determine how a major-unit amount shown to a user maps to the minor-unit value used for calculation and posting.
What users do. Review the active currencies, check a currency's decimal precision, add or update a currency, activate or deactivate one, and confirm that a product, account, or transfer uses an allowed denomination.
Outcomes and side effects. Currency changes affect validation, amount formatting, portfolio totals, and tariff calculation. They do not move money, but they can allow or block new account and transfer activity. All operations are gated by role-based access on the currencies record type and written to the audit trail.
Related manuals: FX, Accounts & Portfolios, Transfers, Tariffs.
The currency record
| Field | Meaning |
|---|---|
| Code | Three-letter ISO 4217 or asset code (e.g. EUR, BTC) |
| Numeric code | ISO 4217 numeric code |
| Name, symbol | Display identity |
| Decimal places | Decimals shown to users |
| Precision | Internal calculation precision (minor units) |
| Rounding | Rounding rule — up, down, or half_up |
| Fiat / base | Whether the currency is fiat, and whether it is the base for FX calculations |
| Format, icon, display priority | Presentation controls; the icon URL is resolved on read from configuration |
| Active | Whether the currency is available for use |
| Audit fields | Created and last-modified user and timestamp |
Tasks
List currencies
GET /v1/currencies
GET /v2/currencies
Two listings exist:
- v1 returns a plain list of the currencies the caller may see, and accepts simple field filters (code, numeric code, symbol, decimal places, precision, rounding, format, name, description, display priority, fiat/base flags, active), plus a limit and offset. Unknown filter fields are ignored rather than rejected.
- v2 is the paginated, search-oriented listing. It accepts structured search fields, sorting, and paging, and returns a result envelope carrying the page of records, the total, the unfiltered total, and whether more pages remain. When crypto inclusion is requested, it also joins linked crypto accounts and validates the supplied project (and optional customer) scope. Use v2 for operational grids and search screens.
limit=-1skips data fetching and returns only the count (total/total_unfiltered); any other value<= 0falls back to the default limit.
Results are always scoped by role — a caller sees only the currencies their permissions allow.
View one currency
GET /v1/currencies/{id}
Returns the full record for a single currency, with its icon URL resolved. The identifier must be a valid currency reference.
Add a currency
POST /v1/currencies
Creates a new currency from its code, name, symbol, precision, and display metadata, and returns the created record. Adding a currency requires create permission on the currencies catalog. A currency name must be unique — attempting to reuse an existing name is rejected as a server-side conflict; confirm the name is free before adding.
Update a currency
PUT /v1/currencies/{id}
Replaces the editable fields of an existing currency from the supplied values and returns the updated record. Fields omitted from the request are not preserved automatically — send the complete intended state. Requires update permission.
Activate or deactivate a currency
There is no separate toggle endpoint — set the active flag through the update operation. Deactivating a currency keeps the record but removes it from the denominations available for new activity; use this rather than deletion when a currency should stop being offered but its history must remain.
Delete a currency
DELETE /v1/currencies/{id}
Permanently removes the currency record. This is a hard removal, not a deactivation — the row is gone. Requires delete permission. Because accounts, balances, tariffs, and postings reference currencies, prefer deactivating (setting active off) over deleting unless the currency was created in error and is unused. On success the operation returns a standard confirmation message.
Amount and precision handling
This section explains why currency precision matters across the platform; the rules are owned here and consumed everywhere amounts are handled.
All monetary amounts in the system are represented as integer values in minor (atomic) units, carried as strings. Floating-point numbers are never used for money.
| Layer | Format |
|---|---|
| Ledger / API | Atomic integers |
| User interface | Formatted decimals |
Why. Integer atomic units prevent rounding drift and settlement mismatches, keep accounting deterministic and easy to reconcile, and satisfy the regulatory expectation of traceable, reversible calculations.
Internal representation
| Asset | Internal unit | Precision |
|---|---|---|
| EUR / USD | cents | 2 |
| BTC | satoshis | 8 |
| ETH | wei | 18 |
| USDC | micro | 6 |
How precision travels with an amount
- An amount is always accompanied by its currency, so the minor-unit value is never ambiguous.
- On reads, the API also returns the precision — the number of decimals the minor unit represents — so every returned amount is fully self-describing.
- On writes, callers send only the amount and currency; precision is not accepted in requests — the server resolves it from the currency record. This is why the currency catalog is the single source of precision truth.
Common pitfalls
- Precision mismatch. Crypto assets support up to 18 decimals while fiat typically uses 2. Always convert through atomic units; the explicit precision on reads removes ambiguity.
- Conversion loss. Avoid chained conversions (e.g. BTC → EUR → USD). Convert atomic ledger value → base currency → quote currency. Exchange-rate handling itself lives in the FX module.
- Regulatory expectations. Auditors expect deterministic rounding, traceable calculations, and reversible accounting — integer atomic units satisfy all three.
Common failures and what they mean
| Situation | What the operator sees | What to do |
|---|---|---|
| Not signed in | Authorization failure | Sign in with a valid session |
| No permission on the catalog | Access denied | Confirm the role grants the needed currencies permission |
| Currency not found | Not found | Verify the currency reference |
| Duplicate currency name on add | Rejected as a server error | Use a name not already in the catalog |
| Malformed request body | Invalid input | Correct the payload and retry |
Underlying message keys (for support triage) live in the module's localization catalogue; operators act on the business meaning above.
Returns countries in the standard paginated envelope through the shared query parser. Filterable and sortable fields: id, iso_alpha_2, iso_alpha_3, iso_numeric, icon, emoji, phone_prefix, order, iban_length, name, description, tags, created_at, created_by, modified_at, modified_by, active. Free-text search covers iso_alpha_2, iso_alpha_3, iso_numeric, icon, emoji, phone_prefix, name and description. Two fields are deliberately NOT listed. metadata is not in validFieldsV2 at all, so querying it answers 500 query_m.invalid_search_field — NOT a 400, and not query_m.invalid_field. validateFieldName builds MsgInvalidSearchField with no WithCode, and an empty Code sends HandleAppErrorWithCode to 500. Do not confuse it with query_m.invalid_field: that one has two unrelated sources, both answering 400 — validateFieldName raises it when the name fails the character regex, and the stack parser raises it for an unknown stack field. An unknown SORT field is query_m.invalid_sort_field, 400. icon_url IS in validFieldsV2 but has no column behind it (models.Country marks it gorm:"-"), so filtering or sorting on it builds SQL referencing a column that does not exist and returns 500 — a pre-existing defect in validFieldsV2, not something to rely on. Sorting on it is subject to the same data-pass caveat as any other sort key: see the sort parameter. NO RECORD-LEVEL PERMISSION FILTER APPLIES. The service passes UserID: uuid.Nil (its own comment calls this a guest route), and the shared getAll path short-circuits on uuid.Nil before the permitted-records lookup, merging a nil scope. So any caller with a valid token receives every row and unfiltered total/total_unfiltered counts, regardless of record grants. RbacRecordType is passed but unused on this path. The v1 list has no record scoping either.
Handler: GetAllCurrencies → SrvGetAllCurrencies. Returns a **bare array** of models.Currency (NOT a get_all envelope). Results are RBAC-scoped (PermReadAll or permitted record IDs). Query uses **flat** field names passed to ConstructQuery + CurrencyQueryParser — invalid fields are silently ignored. Use GET /v2/currencies for paginated get_all + search.* syntax.