Core concepts
The mental model behind Radon Payments — providers, minor units, normalized results, webhooks, and how swapping providers actually works.
Radon Payments has a small number of ideas. Once they click, every method and every provider behaves predictably.
Providers
A provider is a payment processor — Stripe, Paystack, Adyen, Coinbase Commerce. You configure the ones you use; Radon lazy-loads each adapter only when it's first touched, so a Stripe-plus-Paystack app never bundles Adyen's code.
const payments = new RadonPayments({
providers: {
stripe: {}, // credentials from env
paystack: { secretKey: "sk_test_..." }, // or inline
},
defaultProvider: "stripe",
});Every call uses defaultProvider unless you override it per call:
await payments.charge({ amount: money(500000, "NGN") }, { provider: "paystack" }); // ₦5,000Why swapping is a config change
Every provider implements the same interface — charge, refund,
parseWebhook, and so on. The shapes you pass in and get back are identical
across providers, so changing processor is a config edit, not a rewrite.
Money is always integer minor units
Every amount is an integer in the currency's smallest unit, paired with an ISO-4217 code. This is the single most common source of bugs in payment code, so Radon makes it explicit and impossible to get wrong silently.
import { money, majorMoney } from "@radonsdk/payments";
money(1999, "USD"); // $19.99 — 1999 cents
money(500000, "NGN"); // ₦5,000 — 500000 kobo
majorMoney(19.99, "USD"); // same as money(1999, "USD"), from a decimalPassing a float throws
money(19.99, "USD") throws a RangeError — 19.99 isn't an integer number of
cents. Use majorMoney(19.99, "USD") or convert yourself with toMinorUnits.
Zero-decimal currencies (JPY, KRW, XOF…) and three-decimal ones (KWD, BHD…) are
handled for you via currencyExponent().
Helpers you'll reach for:
money(amount: number, currency: string) => MoneyBuild a Money from integer minor units. Throws on a non-integer.
majorMoney(major: number, currency: string) => MoneyBuild a Money from a decimal, e.g. 19.99.
formatMinorUnits(minor: number, currency: string) => stringFormat for display or the wire, e.g. "123.45".
isZeroDecimal(currency: string) => booleanWhether a currency has no minor unit (JPY, KRW, …).
Normalized results
Every charge() returns the same ChargeResult shape, whatever the
provider. Radon maps each processor's states into one status union:
status"succeeded" | "requires_action" | "processing" | "failed" | "refunded" | "canceled"The normalized outcome. Redirect-based providers return requires_action
with a redirectUrl.
idstringThe provider's charge/transaction id — pass it to retrieveCharge and refund.
redirectUrlstring | undefinedPresent when the customer must finish on a hosted page.
amountMoneyThe normalized amount and currency.
rawunknownThe untouched provider response — reach here for fields Radon doesn't
normalize (Stripe's payment_method_details, PayPal's purchase_units, …).
Redirect vs. direct providers
Some providers (Stripe with cards) can settle a charge in one call. Many
(Paystack and most African providers, Klarna, PayPal) are redirect-based:
charge() returns status: "requires_action" and a redirectUrl. Send the
customer there, then confirm with retrieveCharge() when they return. See
Charges for the full pattern.
Webhooks, normalized
Providers notify you of events (a redirect payment completing, a dispute) via
webhooks. Radon verifies the signature and collapses every provider's
payload into one NormalizedEvent, so you write one handler:
const event = await payments.webhooks.handle({ body: req.rawBody, headers: req.headers });
if (event.type === "charge.succeeded") { /* fulfill */ }Signatures are computed over the exact bytes received — always pass the raw body. Full details in Webhooks.
Test vs. live
mode: "test" uses every provider's sandbox; mode: "live" goes live. Flip the
one flag to switch your whole integration.
Stripe is a special case
Stripe decides test vs live from the key prefix (sk_test_… vs sk_live_…),
not the mode flag. Using mode: "live" with a sk_test_ key still hits
Stripe's test environment.
Escape hatch
The unified API covers the common cases. For provider-only features, read
result.raw, or register your own adapter by extending BaseProvider — any
custom slug is Pro-gated but built exactly like the built-ins.