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 in major units (euros, not cents). JSON numbers are avoided to keep arithmetic exact across languages. The API emits a canonical form with trailing zeros trimmed (EUR 49.50 comes back as "49.5"), and "10", "10.0" and "10.00" are the same amount, so parse into a decimal type and compare numerically, never as strings. |
currency | ISO 4217 code, three uppercase letters. |
Each currency has a minor-unit scale, which decides how amounts are rounded and how many decimals to display. The digit count of value does not tell you the scale; use the sibling currency:
| Currencies | Scale |
|---|---|
| USD, EUR, GBP, CHF, AUD, CAD, … (most) | 2 |
| JPY, KRW, ISK, BIF, CLP, … (zero-decimal) | 0 |
| BHD, IQD, JOD, KWD, OMR, TND, LYD | 3 |
| CLF, UYW | 4 |
The full table ships in the API spec as x-currency-scales on the Currency schema, with x-currency-default-scale (2) for anything not listed. Amounts you send with extra precision are rounded half away from zero to the currency's scale. Per-unit rates (a price's list_price, a tier's unit_amount) are kept at full precision, up to 8 decimals.
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 an ISO 4217 code in three uppercase letters. In request bodies (a customer's preferred_currency, a subscription's currency, a wallet, a price, an FX override, a currency on the allow-list) it is validated against the Currency schema, which lists the 47 codes the platform supports; anything else is refused with a 400 naming the field.
Send the code exactly as ISO 4217 writes it in a body: "eur", " EUR", "", "EU" and "EURO" are rejected, not corrected.
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. POST /v1/prices/resolve shows the result in advance, including the rate used (exchange_rate_id) and the FX policy that selected it (fx_policy_applied: invoice_issue, period_start, daily_snapshot or spot).
On an issued invoice, each atom (GET /v1/invoices/{id}/atoms) records the FX snapshot its reporting amount was pinned to (exchange_rate_id) and the instant the policy pinned it at (fx_policy: per_segment, period_start, invoice_issue or daily_snapshot). See Exchange rates. A conversion still needs a rate: if none is available 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; convert before sending.
One exception is deliberate: a scheduled change with when: "at" takes a wall clock in at (sent with a Z suffix) plus an IANA time zone in zone, so "midnight on 1 January in Vienna" stays midnight in Vienna. An offset in at is refused there.
Where billing periods fall
A subscription's billing periods are aligned by alignment: {"kind": "start"} counts from the first paid phase's start, {"kind": "day_of_month", "day": 15} cuts on a fixed day (1–28, so the day always exists in February), and {"kind": "last_day"} on every month's last day. It comes from the plan's billing_alignment, else the organization's default, and is copied onto the subscription when it first goes live with a paid phase. The subscription's current_period (start, end, half-open) tells you the period now running.
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 - snapshots, overrides, and pinning the rate at finalisation
- 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