Radondocs

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,000

Why swapping is a config change

Every provider implements the same interfacecharge, 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 decimal

Passing a float throws

money(19.99, "USD") throws a RangeError19.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) => Money

Build a Money from integer minor units. Throws on a non-integer.

majorMoney(major: number, currency: string) => Money

Build a Money from a decimal, e.g. 19.99.

formatMinorUnits(minor: number, currency: string) => string

Format for display or the wire, e.g. "123.45".

isZeroDecimal(currency: string) => boolean

Whether 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.

idstring

The provider's charge/transaction id — pass it to retrieveCharge and refund.

redirectUrlstring | undefined

Present when the customer must finish on a hosted page.

amountMoney

The normalized amount and currency.

rawunknown

The 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.

On this page