@radonsdk/payments
v0.1.0
Published
One unified, provider-agnostic API for 24 payment providers — global, African, and crypto. Write payments.charge() once, swap providers via config. Webhook normalization, a provider-side coupon engine, composable hooks, and bring-your-own-provider. Own yo
Readme
@radonsdk/payments
One unified, provider-agnostic API for 24 payment providers — global, African, and crypto. Write
payments.charge()once and swap providers with a config change, never a code change.
Radon Payments gives you a single, typed interface (charge, refund, createSubscription, parseWebhook, …) that every provider implements. Provider-specific quirks — auth schemes, minor vs. major currency units, wildly different webhook payloads — are absorbed inside each adapter and never leak to your code. Own your data, no vendor lock-in.
- 24 providers, one API — Stripe, PayPal, Adyen, Paystack, Flutterwave, Coinbase Commerce, Mercado Pago, and more.
- Normalized webhooks — every provider's events collapse into one schema; one handler,
payments.webhooks.handle(req). - Provider-agnostic coupon engine, composable hooks/middleware, and a bring-your-own-provider interface.
- Lazy-loaded adapters — a Stripe + Paystack app never bundles Alipay's code. The core is ~39 KB.
- Strict TypeScript, ESM + CJS, Node ≥ 18, zero required dependencies.
Install
npm install @radonsdk/payments
# or: pnpm add @radonsdk/paymentsRadon never stores your secrets. Each adapter reads credentials from RADON_<PROVIDER>_* environment variables (or from the providers config block, which takes precedence). See .env.example for every provider's variable names.
Quickstart (< 5 minutes)
import { RadonPayments, money } from "@radonsdk/payments";
const payments = new RadonPayments({
mode: "test", // sandbox keys everywhere; flip to "live" to go live
providers: { stripe: {} }, // credentials come from RADON_STRIPE_SECRET_KEY
defaultProvider: "stripe",
});
// Amounts are ALWAYS integer minor units + an ISO-4217 currency. $19.99:
const charge = await payments.charge({
amount: money(1999, "USD"),
customer: { email: "[email protected]" },
});
console.log(charge.status); // "succeeded" | "requires_action" | ...
console.log(charge.redirectUrl); // present when the customer must finish on a hosted pageSwitch providers without touching your charge code — it's a config change:
const payments = new RadonPayments({
mode: "test",
providers: { stripe: {}, paystack: {} },
});
await payments.charge({ amount: money(1999, "USD") }, { provider: "stripe" });
await payments.charge({ amount: money(500000, "NGN") }, { provider: "paystack" }); // ₦5,000money(1999, "USD") is $19.99; money(500000, "NGN") is ₦5,000.00. You always pass minor units — the adapter converts to whatever the provider's API actually wants (kobo, decimal strings, etc.).
Free vs. Pro
Five providers are free. The rest require a Radon Pro license key, verified once in await payments.init() and cached for the process lifetime.
| Tier | Providers |
| --- | --- |
| Free | stripe, paystack, paypal, opay, bachs |
| Pro (license) | everything else (see catalog below) + any bring-your-own provider |
const payments = new RadonPayments({
providers: { adyen: {} }, // a Pro provider
licenseKey: process.env.RADON_LICENSE_KEY, // or config.license: { key, verifyUrl, ... }
});
await payments.init(); // verifies the license (throws if invalid); unlocks Pro
await payments.charge({ amount: money(1999, "EUR") }, { provider: "adyen" });- Free providers work with no license and no
init(). - Using a Pro provider without a valid license throws
LicenseRequiredError; a bad/unreachable key throwsLicenseInvalidError(fail-closed). - Get a license at https://radonsdk.xyz/pricing. Introspect tiers with the exported
FREE_PROVIDERSset andisProProvider(slug).
The unified API
// Charge (one-time). Returns a normalized ChargeResult.
const c = await payments.charge({ amount: money(1999, "USD"), customer: { email } });
// Confirm an async / redirect charge later.
const latest = await payments.retrieveCharge(c.id, { provider: "paystack" });
// Refund (full or partial).
await payments.refund({ chargeId: c.id, amount: money(500, "USD") });
// Subscriptions (providers that support them).
const sub = await payments.subscriptions.create({
customer: { email },
plan: { amount: money(1500, "USD"), interval: "month" },
});
await payments.subscriptions.cancel(sub.id, { atPeriodEnd: true });Operations not supported by a provider throw a typed UnsupportedOperationError — never a silent failure — and provider.capabilities tells you up front what's available.
Webhooks — one handler for every provider
Each provider signs webhooks differently (HMAC-SHA256/512, MD5, RSA, timestamped schemes, sorted-key digests…). Radon verifies the signature inside the adapter and hands you one normalized event:
// Express / Fastify / Next — pass the RAW body (signatures are computed over exact bytes).
app.post("/webhooks", async (req, res) => {
try {
const event = await payments.webhooks.handle(
{ body: req.rawBody, headers: req.headers },
{ provider: "stripe" }, // optional when only one provider is configured
);
// event.type is one of the normalized types below
res.sendStatus(200);
} catch {
res.sendStatus(400); // signature failed — reject
}
});Normalized event types:
payment.succeeded payment.failed
subscription.created subscription.updated subscription.cancelled
refund.issued dispute.opened invoice.paid
unknownRegister handlers as hooks (below), or use the return value of handle().
Coupons (provider-agnostic)
Coupons live in Radon, not the provider — so one coupon works identically across all 32. The discount is applied before the provider is charged; a redemption is recorded only after the charge succeeds.
await payments.coupons.create({ code: "LAUNCH20", type: "percentage", value: 20, maxRedemptions: 100 });
const result = await payments.charge({ amount: money(5000, "USD"), coupon: "LAUNCH20" });
result.coupon; // { code: "LAUNCH20", amountDiscounted: 1000, originalAmount: 5000 }Plug in your own persistence with the couponStore config option (defaults to in-memory).
Hooks, middleware & plugins
// Lifecycle hooks
payments.on("onPaymentSucceeded", (charge) => analytics.track(charge));
payments.on("onWebhookEvent", (event) => queue.enqueue(event));
// Around-style charge middleware (onion order)
payments.useMiddleware(async (input, next) => {
const result = await next(input);
await audit.log(input, result);
return result;
});
// Bundle hooks + middleware as a reusable plugin
payments.use({ name: "my-plugin", onRefundIssued: (r) => {/* … */} });Bring your own provider
Implement the PaymentProvider interface (or extend BaseProvider) and register it. Custom providers are Pro-gated.
import { BaseProvider, registerProvider } from "@radonsdk/payments";
class MyPspProvider extends BaseProvider {
readonly name = "my-psp";
readonly capabilities = { charge: true, refund: true, webhooks: true, subscriptions: false, redirect: false, crypto: false };
async charge(input) { /* call your API via this.http(...) */ }
async refund(input) { /* … */ }
parseWebhook(req, secret) { /* verify + normalize */ }
}
registerProvider("my-psp", async () => MyPspProvider);Provider catalog
24 providers, fully implemented with real API calls and signature-verified webhooks. Import any adapter directly for an explicit dependency: import { StripeProvider } from "@radonsdk/payments/providers/stripe".
Global
| Slug | Provider | Tier | Notes |
| --- | --- | --- | --- |
| stripe | Stripe | Free | cards, subscriptions, refunds, HMAC webhooks |
| paypal | PayPal | Free | Orders v2, OAuth2, async webhook verify |
| braintree | Braintree | Pro | GraphQL charge/refund, SHA1-HMAC webhook |
| adyen | Adyen | Pro | minor units, HMAC-SHA256 notification |
| checkout | Checkout.com | Pro | Bearer + HMAC webhook |
| square | Square | Pro | amount_money, notification-URL HMAC |
| authorizenet | Authorize.Net | Pro | opaque-data nonce, X-ANET-Signature |
| razorpay | Razorpay | Pro | order-based, subscriptions, HMAC webhook |
| mollie | Mollie | Pro | decimal-string amounts, fetch-to-verify |
| gocardless | GoCardless | Pro | mandate-based direct debit, versioned |
| klarna | Klarna | Pro | HPP session → redirect (no signed webhook) |
| skrill | Skrill | Pro | Quick Checkout, MD5 status signature |
Africa
| Slug | Provider | Tier | Notes |
| --- | --- | --- | --- |
| paystack | Paystack | Free | minor units (kobo), HMAC-SHA512 webhook |
| opay | OPay | Free | dual-auth Cashier, HMAC-SHA3-512 callback |
| bachs | Bachs | Free | decimal-string money, subscriptions, HMAC-SHA256 webhook |
| flutterwave | Flutterwave | Pro | major units, verif-hash webhook |
| korapay | Korapay | Pro | HMAC over data only |
| fincra | Fincra | Pro | business-id header, HMAC-SHA512 |
Crypto
| Slug | Provider | Tier | Notes |
| --- | --- | --- | --- |
| coinbase | Coinbase Commerce | Pro | fiat + native crypto amounts, HMAC-SHA256 |
| nowpayments | NOWPayments | Pro | sorted-key HMAC-SHA512 IPN |
| opennode | OpenNode | Pro | Bitcoin/Lightning, id-HMAC callback |
BNPL / regional
| Slug | Provider | Tier | Notes |
| --- | --- | --- | --- |
| afterpay | Afterpay / Clearpay | Pro | decimal amounts, checkout→capture |
| mercadopago | Mercado Pago | Pro | Checkout Pro, x-signature webhook, subscriptions |
| payu | PayU (Global/CEE) | Pro | OAuth2, minor-unit orders, OpenPayU MD5 webhook |
Money & currency
Radon's canonical amount is an integer in the currency's minor unit + an ISO-4217 code — money(1999, "USD") is $19.99, money(100, "JPY") is ¥100 (zero-decimal), money(1000, "KWD") is 1.000 KWD (three-decimal). Integers avoid floating-point rounding bugs. Each adapter converts to its API's format; you never think about it.
Crypto charges quote a fiat price and return both the native asset amount and the fiat-equivalent on result.crypto ({ amount, fiatEquivalent, address }).
import { money, majorMoney, toMinorUnits } from "@radonsdk/payments";
money(1999, "USD"); // { amount: 1999, currency: "USD" }
majorMoney(19.99, "USD"); // same, from a decimalTest vs. live
One flag flips every configured provider between sandbox and production credentials/endpoints simultaneously:
new RadonPayments({ mode: "test", providers: { /* … */ } }); // default: sandbox everywhere
new RadonPayments({ mode: "live", providers: { /* … */ } }); // productionInstall size & lazy loading
The core (import { RadonPayments } from "@radonsdk/payments") contains zero provider code — only the interface, registry, and the webhook/coupon/hook engines (~39 KB). Adapters are loaded on demand via dynamic import() the first time a provider is used, and each is emitted as its own subpath entry (@radonsdk/payments/providers/<slug>). A dev who configures only Stripe + Paystack never executes — or, under a code-splitting bundler, bundles — the other 22 adapters or their optional dependencies. This is why one package can cover two dozen providers without bloating your install.
API surface
RadonPayments, createPayments, money / majorMoney / toMinorUnits / formatMinorUnits, BaseProvider, registerProvider, FREE_PROVIDERS / isProProvider, LicenseClient, the HttpClient + signature helpers (for BYO adapters), and every typed error (PaymentError, LicenseRequiredError, UnsupportedOperationError, WebhookSignatureError, …). All types are exported.
License
MIT © Radon SDK. The SDK is MIT-licensed; the Pro tier requires a commercial license key at runtime for Pro providers.
