Money & Currency
Every amount in the Flint API is an integer in the currency's minor unit, paired with an explicit currency code: {"amount": 2500, "currency": "USD"} is $25.00. There are no floats, no decimal strings, and no implied currency. Integers survive JSON parsing, arithmetic, and storage without rounding drift, so the totals Flint computes always reconcile exactly with what you record on your side.
This page is the reference for how money works across the API: the Money object, when a field is a unit price versus a total amount, the amounts Flint computes for you, how percentages behave, and the currency rules. If you haven't made your first call yet, start with Accept Your First Payment and keep this page open while you build.
The Money Object#
Every money field, in requests and responses, is the same two-field object:
{"amount": 2500, "currency": "USD"}
amountis an integer in the currency's minor unit. For USD the minor unit is the cent, so2500means 2,500 cents: $25.00.currencyis a three-letter uppercase ISO 4217 code.
Both fields are always present, in every request you send and every response you read. Some conversions:
| To represent | Send amount | Not |
|---|---|---|
| $0.50 | 50 | 0.50 |
| $19.99 | 1999 | 19.99 |
| $25.00 | 2500 | 25 |
| $1,000.00 | 100000 | "1000.00" |
The off-by-100 bug
Sending dollars where the API expects minor units is the most common bug in new integrations. {"amount": 25, "currency": "USD"} is 25 cents, not $25.00. If a test charge comes out a hundred times smaller than you expected, you sent major units; if a buyer sees $2,500 instead of $25, something multiplied by 100 twice. Write one conversion helper, use it at every call site, and assert on pricing_amounts.total_money in your tests.
amount must be a JSON integer. Floats like 12.50 and strings like "1250" are rejected with INVALID_JSON, and negative amounts are rejected wherever you send them. Fields that move money (payments, refunds, tips) must be greater than zero. There is no practical upper bound: amounts are 64-bit integers.
When you convert user input to minor units, multiply and round in one step. Binary floating point makes the rounding mandatory, not defensive:
Math.round(Number("19.99") * 100); // 1999
Number("19.99") * 100; // 1998.9999999999998
Prices vs Amounts#
One naming rule holds across the entire API:
unit_price_moneyis the price of one unit of something. It appears wherever you define what a thing costs: line items on orders, invoices, payment links, and subscription plans, and variants in your product catalog.amount_moneyis a total: money that moves, or a total you set directly. It appears on refunds, tips, order charges, quick_pay checkout items, invoice manual payments, standalone payment intents, payouts, and disputes.
| Field | Where it appears |
|---|---|
unit_price_money | Line items (orders, invoices, payment links, subscription plans), product variants |
unit_price_delta_money | Modifiers: the per-unit price change a modifier applies |
amount_money | Refunds, tips, order charges, quick_pay items, invoice manual payments, standalone payment intents, payouts, disputes |
Quantity math belongs to Flint. Send the per-unit price and the quantity, and read the extended totals back off the response instead of computing them yourself:
curl -X POST https://api.withflintpay.com/v1/orders \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: money-tote-001" \
-d '{
"line_items": [{
"name": "Canvas tote bag",
"quantity": 2,
"unit_price_money": {"amount": 1250, "currency": "USD"}
}]
}'
{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "open",
"line_items": [{
"order_line_item_id": "li_1kmn0aExample",
"name": "Canvas tote bag",
"quantity": 2,
"unit_price_money": {"amount": 1250, "currency": "USD"},
"base_subtotal_money": {"amount": 2500, "currency": "USD"},
"subtotal_money": {"amount": 2500, "currency": "USD"},
"tax_money": {"amount": 0, "currency": "USD"},
"total_money": {"amount": 2500, "currency": "USD"}
}],
"pricing_amounts": {
"subtotal_money": {"amount": 2500, "currency": "USD"},
"discount_money": {"amount": 0, "currency": "USD"},
"tax_money": {"amount": 0, "currency": "USD"},
"total_money": {"amount": 2500, "currency": "USD"}
},
"settlement_amounts": {
"paid_money": {"amount": 0, "currency": "USD"},
"amount_due_money": {"amount": 2500, "currency": "USD"}
}
}
}
The response carries the multiplication: base_subtotal_money is unit_price_money times quantity, subtotal_money adds any modifiers, and total_money applies the line's share of discounts and tax.
A refund, by contrast, is a total amount of money moving back to the buyer, so it takes amount_money:
curl -X POST https://api.withflintpay.com/v1/refunds \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: refund-tote-001" \
-d '{
"order_id": "ord_1kmn0aExample",
"amount_money": {"amount": 500, "currency": "USD"},
"reason": "requested_by_customer"
}'
Refunds are validated against settlement, not pricing: the amount can never exceed what remains refundable, and refunding an order with no settled payments fails with NO_PAYMENTS_FOR_ORDER. If you run the request above against the unpaid order from the previous step, that error is exactly what you'll see. See Refunds.
Payment intents linked to an order don't take an amount at all. Leave amount_money out when you pass order_id: the intent's amount derives from the order's outstanding balance and stays in sync as the order changes. You only set amount_money on a standalone payment intent.
Amounts Flint Computes for You#
Every order carries two computed summaries, and they answer different questions:
pricing_amountsis what the order should collect:subtotal_money,discount_money,charge_money,tax_money,requested_tip_money, andtotal_money.settlement_amountsis what has actually happened:paid_money,refunded_money,net_collected_money,settled_tip_money,credit_money,amount_due_money(what is still left to collect), andbalance_money.
Read totals from these objects instead of re-deriving them. Discounts prorate across line items, percent-based values round to the nearest minor unit, and tax depends on the order's tax location; recomputing any of that client-side invites penny drift. Show a buyer pricing_amounts.total_money before charging, and confirm settlement afterward by checking that settlement_amounts.amount_due_money has reached 0.
Percent-derived values also come back as money. A percent tip yields effective_amount_money on the tip; a percent coupon yields both amount_money (the theoretical discount) and applied_money (the actual discount after caps, such as the remaining discountable balance) on the applied discount.
Amounts That Can Be Negative#
Requests never accept negative amounts, but a few response fields are signed:
settlement_amounts.balance_moneyon orders goes negative when the balance tips in the buyer's favor, such as unapplied credit on the order. Its floored-at-zero counterpart isamount_due_money, which ismax(balance_money, 0): prefer it when all you need is what to collect.total_moneyon order line items.amount_money,fee_money, andnet_moneyon balance transactions, where charges post positive and refunds and fees post negative, andnet_moneyon payouts. See the Money Movement API Reference.
If you feed amounts into an accounting system, treat these fields as signed and everything else as non-negative.
Percentages Are Not Money#
Tips, coupons, and order charges can be defined as a percent instead of a fixed amount. Percent fields are whole numbers: 15 means 15%, 12.5 means 12.5%.
- A tip takes either
amount_moneyorpercent(1 to 100), never both. - A coupon takes either
amount_off_moneyorpercent_off(1 to 100). - An order charge takes either
amount_moneyorpercent.
{"percent": 0.15} is 0.15 percent, not 15 percent. If a tip or discount computes to almost nothing, you sent a fraction where the API expects a whole number.
Percents are inputs; money is what comes back. A {"percent": 15} tip on an order with a $40.00 post-discount subtotal returns effective_amount_money: {"amount": 600, "currency": "USD"}, rounded to the nearest minor unit. See Tips & Fees and Coupons.
Currency#
currency is a three-letter uppercase ISO 4217 code, and today Flint accepts exactly one: USD.
- A malformed or unrecognized code (
usd,US,XYZ) fails withINVALID_CURRENCY. - A valid ISO code Flint doesn't support yet (
EUR,JPY) fails withUNSUPPORTED_CURRENCY:
{
"error": {
"type": "validation_error",
"code": "UNSUPPORTED_CURRENCY",
"message": "Line item 0: Currency \"EUR\" is not supported in closed beta; supported currencies: USD",
"param": "line_items[0].unit_price_money.currency"
}
}
The format carries the code explicitly on every amount, so supporting additional currencies later won't reshape your integration. Until then, write code that reads amount and currency together rather than assuming dollars.
Currency must also be consistent within a flow:
- All line items on an order share one currency, and charges, discounts, tips, and payments must match the order's currency (
CURRENCY_MISMATCH). - A capture must match the currency of its authorization (
CAPTURE_CURRENCY_MISMATCH). - The
min_amountandmax_amountlist filters are bare integers in minor units and require thecurrencyquery parameter alongside them.
Validation Quick Reference#
| Rule | Error code |
|---|---|
amount must be a JSON integer, not a float or string | INVALID_JSON |
| Amounts you send can't be negative | validation error naming the field, such as LINE_ITEM_NEGATIVE_PRICE |
| Payments, refunds, and tips must be greater than zero | INVALID_AMOUNT |
| A refund can't exceed what remains refundable | AMOUNT_EXCEEDS_REFUNDABLE |
| You can't refund an order with no settled payments | NO_PAYMENTS_FOR_ORDER |
| A capture can't exceed the authorized amount | CAPTURE_AMOUNT_EXCEEDS_AUTHORIZED |
| Currency must be a valid uppercase ISO 4217 code | INVALID_CURRENCY |
| Currency must be one Flint supports (USD today) | UNSUPPORTED_CURRENCY |
| Currencies must match within an order, and between capture and authorization | CURRENCY_MISMATCH, CAPTURE_CURRENCY_MISMATCH |
Every error arrives in the standard envelope with type, code, message, and a param pointing at the offending field. See Error Handling.
Displaying Amounts#
Convert from minor units only at the display edge, and let the formatter own symbols and separators:
const formatMoney = ({ amount, currency }) => {
const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency });
const scale = 10 ** formatter.resolvedOptions().maximumFractionDigits;
return formatter.format(amount / scale);
};
formatMoney({ amount: 2500, currency: "USD" }); // "$25.00"
Deriving the scale from the formatter instead of hardcoding 100 keeps the helper correct for currencies with zero or three decimal places, should you ever handle them. Going the other way, always round when converting input to minor units: Math.round(Number(input) * scale).
In the Node SDK#
The Node SDK uses the same integer amounts with camelCase field names:
import { Flint } from "@flintpay/node";
const flint = new Flint({ apiKey: process.env.FLINT_API_KEY! });
const order = await flint.orders.create({
lineItems: [
{ name: "Canvas tote bag", quantity: 2, unitPriceMoney: { amount: 1250, currency: "USD" } },
],
});
order.pricingAmounts.totalMoney; // { amount: 2500, currency: "USD" }
Common Mistakes#
- Sending dollars.
{"amount": 25}is 25 cents. Convert to minor units in one shared helper and use it at every call site. - Doing money math in floats.
0.1 + 0.2is not0.3in floating point. Convert to integer minor units first, then add and multiply. - Sending a percent as a fraction. Percent fields are whole numbers:
15, not0.15. - Recomputing totals client-side. Proration, tax, and rounding happen on Flint's side. Read
pricing_amountsandsettlement_amountsoff the order. - Assuming the currency. Every amount carries its currency. Format, compare, and sum amounts using both fields, and never add amounts across currencies.
Next Steps#
- Accept Your First Payment: put the Money object to work in four calls.
- Refunds: full and partial refunds against settled payments.
- Tips & Fees: percent and fixed tips, service fees, and surcharges.
- Invoicing: manual payments and balances on invoices.
- Orders API Reference: every money field on the order object.
- Money Movement API Reference: balances, payouts, and the signed transaction ledger.
- Node SDK: the same amounts with types and camelCase names.
