Charges
Take one-time payments with payments.charge() — handle direct and redirect-based providers, idempotency, currencies, and confirmation.
A charge is a single, one-time payment. payments.charge() returns the same
normalized ChargeResult whatever
the provider — you branch on status, not on which processor you used.
A basic charge
import { money } from "@radonsdk/payments";
import { payments } from "@/lib/payments";
const charge = await payments.charge({
amount: money(1999, "USD"), // $19.99
customer: { email: "ada@example.com" },
});
if (charge.status === "succeeded") {
// ✅ money captured — fulfill the order
}charge() runs any middleware
you've registered, applies a coupon if one is attached,
then fires your onPaymentSucceeded / onPaymentFailed hooks.
The input
amountMoneyrequiredInteger minor units + ISO-4217 currency, built with money() or
majorMoney(). See money.
customer{ email?: string; id?: string; name?: string }The payer. Some providers (Paystack and most African processors) require
email and will throw without it.
idempotencyKeystringA unique key that makes retrying the same charge safe — the provider returns
the original result instead of double-charging. Forwarded natively where the
provider supports it (e.g. Stripe's Idempotency-Key header).
metadataRecord<string, string>Arbitrary key/values stored on the charge and echoed back in webhooks — great for your order id.
Amounts are minor units
money(1999, "USD") is $19.99, not $1,999. Passing a float like
money(19.99, "USD") throws — use majorMoney(19.99, "USD"). This is the
single most common payments bug; Radon makes it loud instead of silent.
Redirect flows
Many providers don't settle in one call — the customer must approve the payment
on a hosted page (bank 3-D Secure, PayPal, Paystack, Klarna). For these,
charge() returns status: "requires_action" and a redirectUrl.
const charge = await payments.charge({
amount: money(500000, "NGN"), // ₦5,000
customer: { email: "ada@example.com" },
}, { provider: "paystack" });
if (charge.status === "requires_action" && charge.redirectUrl) {
// Send the customer to the provider's hosted page…
return Response.redirect(charge.redirectUrl);
}When the customer returns to your success_url, confirm the real outcome
server-side — never trust the redirect alone:
const confirmed = await payments.retrieveCharge(charge.id, { provider: "paystack" });
if (confirmed.status === "succeeded") {
await fulfillOrder(confirmed.metadata.orderId);
}Prefer webhooks for fulfillment
A customer can close the tab before redirecting back. The reliable signal that a
payment completed is the provider's webhook — treat
the redirect as a UX nicety and fulfill on charge.succeeded.
Choosing the provider per charge
With multiple providers configured, pass { provider } to route a single call;
otherwise defaultProvider is used.
await payments.charge({ amount: money(1999, "USD") }, { provider: "stripe" });
await payments.charge({ amount: money(500000, "NGN") }, { provider: "paystack" });Reading provider-only fields
Radon normalizes the common fields. For anything provider-specific, read
charge.raw — the untouched API response.
const charge = await payments.charge({ amount: money(1999, "USD") });
const stripePaymentMethod = (charge.raw as any).payment_method_details; // Stripe-only