Skip to content

Taking a payment

A cart, a discount, tax, a charge, a receipt — start to finish, with CkMoney doing every step that touches an amount. CommonKit does not talk to a payment processor. It prepares the values you hand to one and reads the values it hands back, so that what you charge is what you meant to charge.

Three rules carry the whole page:

  1. Never do arithmetic on money yourself. No *, no /, no rounding by hand. Every calculation below is one CkMoney call, exact and rounded once.
  2. Ask for exceptions wherever a wrong number costs money. By default a value that cannot be used comes back as a fallback, and the fallback for an amount is zero. On a form that is a convenience. In a payment it is an order for nothing.
  3. Say the currency every time. currency defaults to "USD". If you sell in anything else, a call without it is a bug waiting for its first customer.

The silent zero

Read this before anything else, because it is the mistake that ships:

CkMoney.Multiply("19.99", "8.25%");   // -> 0

The factor must be a plain number, "8.25%" is not one, so the call is rejected — and a rejected calculation answers its fallback, 0. No error, no log line, a total of nothing. Pass throwIfInvalid: true and the same call tells you what is wrong:

CkMoney.Multiply(
    input: "19.99",
    factor: "8.25%",
    throwIfInvalid: true
);
// throws CkMoneyException:
// CkMoney.Multiply: cannot use "8.25%" (String) as a factor — not a number.

Every calculating call on this page passes throwIfInvalid: true. A percentage, by the way, is read by CkNumber.Percent, which turns "8.25%" into the rate 0.0825:

CkMoney.Multiply("19.99", CkNumber.Percent("8.25%"));   // -> 1.65

1. Check what came in

Prices from your own database are yours to trust. An amount typed into a form, read from a spreadsheet or sent by another system is not. Ask before you calculate, and show the reason when the answer is no:

CkMoney.IsAmount("ten dollars");   // -> false
CkMoney.Explain("ten dollars");    // -> "letters in the amount"

2. Calculate

Three lines at 19.99, 5.01 and 12.50; ten percent off; 8.25% sales tax on what is left.

string[] prices = { "$19.99", "$5.01", "12.50" };
string currency = "USD";

decimal subtotal = CkMoney.Total(
    values: prices,
    throwIfInvalid: true
);   // -> 37.50

decimal discount = CkMoney.Multiply(
    input: subtotal,
    factor: 0.10m,
    currency: currency,
    throwIfInvalid: true
);   // -> 3.75

decimal net = CkMoney.Subtract(
    input: subtotal,
    amount: discount,
    currency: currency,
    throwIfInvalid: true
);   // -> 33.75

decimal tax = CkMoney.Multiply(
    input: net,
    factor: 0.0825m,
    currency: currency,
    throwIfInvalid: true
);   // -> 2.78  (2.784375, rounded once, away from zero)

decimal total = CkMoney.Total(
    values: new[] { net, tax },
    throwIfInvalid: true
);   // -> 36.53

Which method for which sum:

You want Call Not
Several amounts added Total + in a loop
One amount less another Subtract -
Tax, a discount, a quantity Multiply *
A unit price from a total Divide /
An amount shared equally Split Divide, whose three thirds make 99.99
An amount shared by ratio — a discount over the lines, a revenue share Allocate Multiply per share, which loses or invents cents
Another currency, at your rate Convert Multiply, which rounds for the wrong currency

To show the customer how the discount falls on each line, share it out by the lines' value. The parts always add back to the discount:

CkMoney.Allocate(
    input: discount,
    ratios: prices.Select(price => CkMoney.Amount(price))
);   // -> [2.00, 0.50, 1.25]

3. Check what goes out

Before the amount leaves your code:

CkMoney.IsPositive(total);              // -> true   a charge is above zero
CkMoney.IsRounded(total, currency);     // -> true   no more decimals than the currency has
CkMoney.IsAtLeast(total, "0.50");       // -> true   your processor's minimum charge

IsRounded is the one people skip. An amount with three decimals in a two-decimal currency is refused by some processors and quietly rounded by others, and either way your records and theirs now disagree.

4. Hand it to the processor

Processors take an amount in one of two shapes. Know which one yours wants.

Whole minor units — an integer count of the smallest unit, cents for the dollar. US$36.53 is 3653; ¥1099 is 1099, the yen having no smaller unit:

long amount = CkMoney.MinorUnits(
    input: total,
    currency: currency,
    throwIfInvalid: true
);   // -> 3653

Decimal text — the amount with exactly the currency's decimals, "10.00" and never "10":

string amount = CkMoney.Format(total, currency);   // -> "36.53"

The currency code goes along in the casing the processor asks for. ISO writes codes in upper case; some processors want lower:

CkMoney.Currency(currency);                                   // -> "USD"
CkMoney.Currency(currency, textCase: TextCase.Lowercase);     // -> "usd"

A card network, an acquirer's settlement file or a bank message carries the currency as three digits instead, and NumericCode writes those: "840" for the dollar, "978" for the euro.

5. Read what comes back

The processor answers in the shape it was sent. Turn it back into an amount, and into text a person can read:

decimal charged = CkMoney.FromMinorUnits(3653, currency);   // -> 36.53
CkMoney.Display(charged, currency);                         // -> "36.53 USD"

Then check it against what you asked for. The two amounts must be the same, and SameAmount refuses to call them the same if they name different currencies:

CkMoney.SameAmount(charged, total);   // -> true

A refund is never more than the charge:

CkMoney.IsAtMost("10.00", total);     // -> true
CkMoney.IsAtMost("100.00", total);    // -> false

Where a processor disagrees with ISO 4217

CommonKit counts a currency's decimals the way ISO 4217 does, and almost always your processor does too. Where it does not, the library does not know — no processor is written into it, because their rules are theirs to change. You say it per call, with decimals (how many decimals the amount may carry) or scale (the power of ten the whole units are written at).

The cases below are from each processor's own documentation, read on 2026-09-20. Treat the table as a pointer, not as the truth: check your processor's currency page before you go live in any currency on it, and check it again when you add one.

Processor Currency Their rule Say it with
Stripe ISK, UGX No decimals, yet written as if there were two: 5 ISK is 500, and the last two digits are always 00 MinorUnits(5, "ISK", scale: 2) → 500; FromMinorUnits(500, "ISK", scale: 2) → 5
Stripe HUF, TWD Charges take two decimals; payouts must be whole units IsRounded(amount, "HUF", decimals: 0) before a payout
Stripe every currency The code in lower case Currency(code, textCase: TextCase.Lowercase)
PayPal HUF, TWD No decimals: "1000.50" is an error Format("1000.50", "HUF", decimals: 0) → "1001"; IsRounded("1000.50", "HUF", decimals: 0) → false
PayPal — Around 25 currencies in all; most of the world's are not accepted Check their list; InCurrencies(code, "USD", "EUR", …) keeps a form to the ones you take

Get the scale wrong and the damage is a factor of a hundred, in either direction, on a real card. That is the reason to read the processor's page and not only this one.

What CommonKit does not do

Everything that is the payment and not the amount: talking to the processor, idempotency keys so a retry does not charge twice, webhooks, refunds and disputes, storing card data (do not), exchange rates. Convert takes the rate from you; where it comes from and how fresh it is are your decisions.