Accept crypto and stablecoin payments (USDC/USDT, multi-chain) with Payzum. Non-custodial — funds settle to your own wallet. Zero runtime dependencies, strict TypeScript, ESM. Node 18+.
npm install payzum
You need two values from your Payzum dashboard: an API key and a webhook secret.
import { Payzum } from "payzum"
const payzum = new Payzum("your-api-key") // Payzum.sandbox(...) for staging
const invoice = await payzum.payments.create({
priceAmount: "49.99", // a string — never a number
priceCurrency: "usd",
payCurrency: "all", // the buyer picks the coin on the checkout page
orderId: "ORDER-12345",
ipnCallbackUrl: "https://your-shop.example/payzum/ipn",
idempotencyKey: "ORDER-12345", // lets the SDK retry safely on network hiccups
})
redirect(String(invoice["invoice_url"]))That's the whole flow: create, redirect, and wait for the webhook. Which coins the buyer can pick is configured in your dashboard, not in code.
Payzum POSTs a signed webhook (IPN) to your ipnCallbackUrl. Hand the SDK the
raw request body and the headers — it finds the right header, checks the
signature and rejects replays, all by itself:
import { SignatureError, Verifier, isPaidStatus, paymentStatusFromMerchant } from "payzum"
const verifier = new Verifier(webhookSecret)
try {
// rawBody: the bytes exactly as received, before any JSON parsing
const data = verifier.verifyPaymentIpn(rawBody, req.headers)
const status = paymentStatusFromMerchant(String(data["payment_status"]))
if (isPaidStatus(status)) {
await fulfilOrder(String(data["order_id"])) // only fulfil on isPaidStatus()
}
} catch (e) {
if (e instanceof SignatureError) {
res.status(401).send("bad signature")
return
}
throw e
}Deliveries can arrive more than once — deduplicate on
verifier.eventId(req.headers) if a repeat must be a no-op on your side.
A payment is always in one of five states: waiting, partially_paid,
finished, expired, failed.
isPaidStatus(status)— safe to fulfil (covers overpayment too).isTerminalStatus(status)— nothing further will happen.paymentStatusFromMerchant(...)throws on anything unexpected, so a surprise value can never be mistaken for "paid".
Everything throws a typed error: ApiError (with .errorCode, e.g.
AMOUNT_BELOW_MINIMUM, to branch on — never branch on messages),
SignatureError for webhooks, TransportError for network failures,
PayzumError for local validation.
Transient failures are retried for you, honouring the server's back-off hints.
A create is only retried when you pass an idempotencyKey, and then in a
way that cannot double-charge.
Amounts go in as strings and come back as exact decimal strings — the SDK
never converts money through a JavaScript number in either direction.
new Payzum(apiKey, { transport }) accepts any Transport implementation, so
you can test without touching the network. The SDK's own suite runs with
npm test.
Docs: https://merchant.payzum.com/docs · llms: https://merchant.payzum.com/llms.txt