zerofee
v3.0.2
Published
Official Node.js SDK for ZeroFee - Unified Payment Orchestration for Africa
Maintainers
Readme
zerofee
Official Node.js / TypeScript SDK for ZeroFee — unified payment orchestration for Africa and global payments.
One API for cards (Stripe, PayPal), mobile money (M-Pesa, MTN, Orange, Wave, Airtel via PawaPay), and West-African rails (Hub2, PaiementPro). Create a hosted checkout with three fields, or drive a direct mobile-money charge — the SDK routes to the best available provider for the customer.
Installation
npm install zerofee
# or
pnpm add zerofee
# or
yarn add zerofeeRequires Node.js >= 18 (uses the built-in global fetch and crypto). Ships CommonJS, ESM, and TypeScript declarations.
Quick start
import { ZeroFee } from 'zerofee'
const zf = new ZeroFee('sk_live_xxx') // sk_sand_xxx for sandbox
// Create a hosted checkout session — only 3 fields required
const payment = await zf.payments.create({
amount: 10000,
sourceCurrency: 'USD',
paymentReference: 'ORDER-123',
successUrl: 'https://yoursite.com/success',
cancelUrl: 'https://yoursite.com/cancel',
})
// Redirect the customer to complete payment
console.log(payment.checkoutUrl)Amounts are in major currency units (e.g. 10000 = 10,000 XOF, 85.43 = $85.43) — never cents.
Direct mobile-money charge
Pass a paymentMethod to charge directly instead of creating a hosted session. Some methods (e.g. Orange Money CI) require an OTP step:
const payment = await zf.payments.create({
amount: 5000,
sourceCurrency: 'XOF',
paymentReference: 'Premium plan',
paymentMethod: 'PAYIN_ORANGE_CI',
customer: { phone: '+2250700000000' },
})
if (payment.paymentFlow?.type === 'otp') {
// Customer enters the OTP they received by SMS
const confirmed = await zf.payments.authenticate(payment.id, { otp: '123456' })
console.log(confirmed.status) // 'completed' | 'failed'
}Checking status
// Authenticated retrieve
const payment = await zf.payments.retrieve('txn_abc123')
// Public status endpoint (safe to poll from a client)
const status = await zf.payments.getStatus('txn_abc123')
console.log(status.status) // 'pending' | 'processing' | 'completed' | 'failed' | ...
// Checkout session status (public)
const session = await zf.checkout.getSessionStatus('cs_abc123')Verifying webhooks
ZeroFee signs every webhook with HMAC-SHA256 in the X-ZeroFee-Signature header
(t={timestamp},v1={signature}, signed over `${timestamp}.${rawBody}`).
Always verify against the raw request body — not a re-serialized object.
import express from 'express'
import { ZeroFee, WebhookSignatureVerificationError } from 'zerofee'
const zf = new ZeroFee('sk_live_xxx')
const app = express()
app.post(
'/webhooks/zerofee',
express.raw({ type: 'application/json' }), // raw body, not express.json()
(req, res) => {
try {
const event = zf.webhooks.constructEvent(
req.body.toString('utf8'),
req.headers['x-zerofee-signature'] as string,
process.env.ZEROFEE_WEBHOOK_SECRET!, // whsec_xxx
)
switch (event.type) {
case 'payment.completed':
// event.data is the Payment
console.log('Paid:', event.data.id)
break
case 'payment.failed':
break
}
res.json({ received: true })
} catch (err) {
if (err instanceof WebhookSignatureVerificationError) {
return res.status(400).send('Invalid signature')
}
throw err
}
},
)The signed timestamp is checked against a 5-minute tolerance by default (replay
protection). Override it with constructEvent(body, sig, secret, { toleranceSeconds }).
Error handling
Every failure throws a typed subclass of ZeroFeeError:
import {
ZeroFee,
AuthenticationError,
ValidationError,
NotFoundError,
RateLimitError,
APIError,
} from 'zerofee'
try {
await zf.payments.create({ amount: 5000, sourceCurrency: 'USD', paymentReference: 'x' })
} catch (err) {
if (err instanceof ValidationError) {
console.error('Bad request:', err.message)
} else if (err instanceof AuthenticationError) {
console.error('Check your API key')
} else if (err instanceof RateLimitError) {
console.error('Slow down')
} else if (err instanceof APIError) {
console.error(err.statusCode, err.code, err.requestId)
}
}Idempotency
Pass an idempotency key as the second argument to payments.create to safely retry
without creating duplicate charges:
await zf.payments.create(params, 'order-123-attempt-1')API surface
| Resource | Methods |
|----------|---------|
| zf.payments | create, retrieve, getStatus, list, authenticate, cancel |
| zf.checkout | getPaymentMethods, retrieveSession, getSessionStatus |
| zf.webhooks | constructEvent, verifySignature |
| zf.customers | list, retrieve |
| zf.invoices | list, retrieve, getByTransaction |
| zf.countries | list, retrieve |
| zf.currencies | list, convert, getRates |
| zf.discovery | getPayinMethod, listFlowTypes, listWebhookEvents |
Configuration
const zf = new ZeroFee('sk_live_xxx', {
baseUrl: 'https://api.0fee.dev/v1', // default
timeout: 30000, // ms, default 30s
})Links
- Documentation: https://0fee.dev/docs
- API reference: https://0fee.dev/docs
- Issues: https://github.com/skiil-ai/0fee.dev/issues
License
MIT © ZeroFee
