Currencies, amounts & dates
Three conventions show up on every endpoint that handles money or time: the Money envelope, ISO 4217 currency codes with per-currency scale, and UTC datetimes in RFC 3339 form.
Money on the wire
Every monetary amount in the API is a JSON object - never a bare number, never a minor-unit integer:
Code
| Field | Notes |
|---|---|
value | Decimal string, scaled to the currency's precision. JSON numbers are avoided to keep arithmetic exact across languages and to preserve trailing zeros ("100.00" ≠ "100"). |
currency | ISO 4217 code, three uppercase letters. |
The wire scale is fixed per currency:
| Currencies | Scale | Example value for one whole unit |
|---|---|---|
| USD, EUR, GBP, CHF, AUD, CAD, … (most) | 2 | "1.00" |
| JPY, KRW, ISK, BIF, CLP, … (zero-decimal) | 0 | "1" |
| BHD, IQD, JOD, KWD, OMR, TND, LYD | 3 | "1.000" |
| CLF, UYW | 4 | "1.0000" |
Any other 3-letter ISO 4217 code defaults to scale 2. Inputs you send with extra precision are rounded (half away from zero) to the registered scale at the boundary.
Use a decimal library on the client side (
bigdecimal.js, PythonDecimal, Goshopspring/decimal, JavaBigDecimal). Parsingvalueas a JavaScriptNumberwill silently lose precision once amounts exceedNumber.MAX_SAFE_INTEGERminor units (~$90 trillion at scale 2 - fine for invoices, not fine for ledgers that aggregate without rounding).
Currency codes
A currency code is exactly three uppercase A–Z letters. That shape is enforced at the boundary on every write that names a currency - a customer's preferred_currency, a subscription's currency, a wallet, a price, an FX override - and malformed input is refused with a 400 naming the field.
Nothing is normalised for you. Send the code exactly as ISO 4217 writes it: "eur", " EUR", "", "EU" and "EURO" are all rejected, not corrected.
The shape check is not a membership check against the ISO 4217 register. A well-formed code the platform has never heard of is structurally valid, and every amount in it then rounds at the default scale 2 - the scale table above holds 43 codes, and anything outside it gets that default with no warning.
Which currencies you may use is a server-side allow-list
Each workspace carries an explicit list of enabled currencies, split into a billing scope and a catalog scope. It is checked on every write that chooses a currency, and it fails closed: a code that is not on the list is refused with 400 / CURRENCY_NOT_ALLOWED, and a workspace with an empty list accepts nothing - including the currency it would otherwise have defaulted to.
Nothing seeds it, so enabling currencies is part of setting a workspace up. See Currency policy.
Multi-currency
Three different objects carry a currency, and they mean three different things. Getting them straight explains almost every currency question the API raises.
| Where | Field | What it means |
|---|---|---|
| Customer | preferred_currency | A preference. It is the default for objects created for this customer afterwards, and it is mutable at any time. |
| Subscription | currency | The contract. Fixed when the subscription is created, frozen for its whole life, and the currency every invoice it produces is billed in. |
| Invoice, credit note, payment, refund, wallet | currency | Money at rest. Pinned at write and never re-denominated. |
A customer's preference
A customer's preference seeds the currency of things created next - their auto-provisioned wallet, and a subscription created without an explicit currency. It is not a lock, and changing it later does not reach backwards: existing subscriptions keep billing their contract currency, and finalised documents keep the currency and FX rate they were issued with. See Changing a customer's billing currency.
Wallets are one per (customer, currency), so a customer can hold several at once - a EUR wallet and a USD wallet side by side.
When the catalog prices a different currency
A product can hold prices in several currencies at once:
Code
When the catalog prices the subscription's contract currency, that price is charged directly. When it does not, the catalog price is converted into the contract currency at billing time rather than the invoice quietly going out in the catalog's currency. The invoice line records the rate used (exchange_rate_id) and which FX policy resolved it (fx_policy_applied).
The full FX selection model - the candidate rates pinned per invoice (period_start_exchange_rate_id, issue_exchange_rate_id, segment_exchange_rate_id) and the three selectable policies that pick between them (invoice_issue, period_start, daily_snapshot) - is documented in Exchange rates. A conversion still needs a rate: if none is configured for the pair, the operation fails rather than guessing.
Dates and timestamps
| Type | Wire format | Example |
|---|---|---|
| Datetime | RFC 3339 (ISO 8601 subset), always UTC | "2025-03-15T14:30:00Z" |
| Date-only | ISO 8601 calendar date | "2025-03-15" |
All datetimes the API emits end in Z (UTC). Timestamps you send should also be UTC; the API does not interpret offsets - convert before sending.
Billing anchor day
Subscriptions accept billing_anchor_day (1–28) to align cycles to a fixed day-of-month. Values above 28 are rejected so the anchor never falls on a day that doesn't exist in February. The current_period_start and current_period_end fields on the subscription tell you exactly when the next charge will be cut.
Filtering by date
Date filters use the standard filter operator grammar:
Code
gt / gte / lt / lte are exclusive / inclusive in the obvious way. There's no separate created_after / created_before query convention - every endpoint follows the same field.operator=value pattern.
Related
- Currency policy - the per-workspace allow-list, its two scopes, and
CURRENCY_NOT_ALLOWED - Exchange Rates - FX rate model and the three selectable FX policies (
invoice_issue/period_start/daily_snapshot) - Subscriptions - the contract currency and where it comes from
- Pagination - the
field.operator=valuefilter grammar - API reference - per-endpoint money fields and currency-aware DTOs