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:

JSON
{"amount": 2500, "currency": "USD"}
  • amount is an integer in the currency's minor unit. For USD the minor unit is the cent, so 2500 means 2,500 cents: $25.00.
  • currency is 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 representSend amountNot
$0.50500.50
$19.99199919.99
$25.00250025
$1,000.00100000"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:

JavaScript
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_money is 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_money is 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.
FieldWhere it appears
unit_price_moneyLine items (orders, invoices, payment links, subscription plans), product variants
unit_price_delta_moneyModifiers: the per-unit price change a modifier applies
amount_moneyRefunds, 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:

Bash
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"}
    }]
  }'
JSON
{
  "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:

Bash
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_amounts is what the order should collect: subtotal_money, discount_money, charge_money, tax_money, requested_tip_money, and total_money.
  • settlement_amounts is 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), and balance_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_money on orders goes negative when the balance tips in the buyer's favor, such as unapplied credit on the order. Its floored-at-zero counterpart is amount_due_money, which is max(balance_money, 0): prefer it when all you need is what to collect.
  • total_money on order line items.
  • amount_money, fee_money, and net_money on balance transactions, where charges post positive and refunds and fees post negative, and net_money on 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_money or percent (1 to 100), never both.
  • A coupon takes either amount_off_money or percent_off (1 to 100).
  • An order charge takes either amount_money or percent.

{"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 with INVALID_CURRENCY.
  • A valid ISO code Flint doesn't support yet (EUR, JPY) fails with UNSUPPORTED_CURRENCY:
JSON
{
  "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_amount and max_amount list filters are bare integers in minor units and require the currency query parameter alongside them.

Validation Quick Reference#

RuleError code
amount must be a JSON integer, not a float or stringINVALID_JSON
Amounts you send can't be negativevalidation error naming the field, such as LINE_ITEM_NEGATIVE_PRICE
Payments, refunds, and tips must be greater than zeroINVALID_AMOUNT
A refund can't exceed what remains refundableAMOUNT_EXCEEDS_REFUNDABLE
You can't refund an order with no settled paymentsNO_PAYMENTS_FOR_ORDER
A capture can't exceed the authorized amountCAPTURE_AMOUNT_EXCEEDS_AUTHORIZED
Currency must be a valid uppercase ISO 4217 codeINVALID_CURRENCY
Currency must be one Flint supports (USD today)UNSUPPORTED_CURRENCY
Currencies must match within an order, and between capture and authorizationCURRENCY_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:

JavaScript
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:

TypeScript
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.2 is not 0.3 in floating point. Convert to integer minor units first, then add and multiply.
  • Sending a percent as a fraction. Percent fields are whole numbers: 15, not 0.15.
  • Recomputing totals client-side. Proration, tax, and rounding happen on Flint's side. Read pricing_amounts and settlement_amounts off 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#

Rate this doc