Your First Payment
This guide walks you through charging a customer, verifying the transaction, and handling the result. We’ll use Chapa for the example — it has the most straightforward sandbox setup.
1. Initialize the Client
Section titled “1. Initialize the Client”import { Zirzir } from '@zirzir/sdk'
const zirzir = new Zirzir({ provider: 'chapa', credentials: { secretKey: process.env.CHAPA_SECRET_KEY! }})2. Initiate a Charge
Section titled “2. Initiate a Charge”const transaction = await zirzir.charge({ amount: 500, // Amount in ETB (no decimals for Ethiopian Birr) currency: 'ETB', email: 'abebe@example.com', firstName: 'Abebe', lastName: 'Kebede', txRef: 'order_123', // Your own unique reference callbackUrl: 'https://yourapp.com/payment/callback', returnUrl: 'https://yourapp.com/payment/success',})
console.log(transaction.checkoutUrl) // Redirect customer hereThe charge() method returns a normalized Transaction object:
{ id: 'zz_tx_01HX...', // Zirzir's transaction ID externalId: 'chapa_tx_abc123', // Chapa's transaction ID status: 'pending', amount: 500, currency: 'ETB', provider: 'chapa', checkoutUrl: 'https://checkout.chapa.co/...', txRef: 'order_123', createdAt: '2024-01-15T10:30:00Z', metadata: { /* raw provider response */ }}3. Redirect the Customer
Section titled “3. Redirect the Customer”After calling charge(), redirect the customer to transaction.checkoutUrl. They’ll complete payment on the provider’s page and be sent back to your returnUrl.
// Express exampleapp.post('/checkout', async (req, res) => { const transaction = await zirzir.charge({ amount: req.body.amount, currency: 'ETB', email: req.body.email, txRef: `order_${Date.now()}`, returnUrl: 'https://yourapp.com/payment/success', callbackUrl: 'https://yourapp.com/webhooks/zirzir', })
res.redirect(transaction.checkoutUrl)})4. Verify the Transaction
Section titled “4. Verify the Transaction”Never trust the return URL alone — always verify server-side.
app.get('/payment/success', async (req, res) => { const { txRef } = req.query
const transaction = await zirzir.verify(txRef as string)
if (transaction.status === 'success') { // Fulfill the order await fulfillOrder(transaction.txRef) res.render('success', { transaction }) } else { res.render('failed', { transaction }) }})Transaction Statuses
Section titled “Transaction Statuses”| Status | Meaning |
|---|---|
pending | Payment initiated, awaiting customer action |
success | Payment confirmed and settled |
failed | Payment failed or was declined |
cancelled | Customer cancelled on the checkout page |
refunded | Payment was refunded |
5. Handle Webhooks
Section titled “5. Handle Webhooks”Return URLs can fail — the customer closes the tab, their internet drops. Webhooks are the reliable signal. In server mode (with server.public_url and ZIRZIR_WEBHOOK_SECRET set on the server), Zirzir POSTs a signed payment.completed / payment.failed / payment.cancelled event to your callbackUrl once the payment settles. Standalone mode has no server to send events, so verify with verify() instead.
app.post('/webhooks/zirzir', express.raw({ type: 'application/json' }), async (req, res) => { let event try { event = zirzir.webhooks.constructEvent( req.body, req.headers['x-zirzir-signature'] as string, process.env.ZIRZIR_WEBHOOK_SECRET!, ) } catch { return res.status(400).send('Invalid signature') }
if (event.type === 'payment.completed') { await fulfillOrder(event.data.reference) }
res.status(200).send('OK')})See Webhooks for the full webhook reference including event types, retry behavior, and signature verification.
Full Example
Section titled “Full Example”import express from 'express'import { Zirzir } from '@zirzir/sdk'
const app = express()const zirzir = new Zirzir({ provider: 'chapa', credentials: { secretKey: process.env.CHAPA_SECRET_KEY! }})
// Initiate paymentapp.post('/pay', async (req, res) => { try { const tx = await zirzir.charge({ amount: req.body.amount, currency: 'ETB', email: req.body.email, txRef: `order_${Date.now()}`, returnUrl: `${process.env.APP_URL}/payment/success`, callbackUrl: `${process.env.APP_URL}/webhooks/zirzir`, }) res.redirect(tx.checkoutUrl) } catch (err) { res.status(500).json({ error: err.message }) }})
// Verify on returnapp.get('/payment/success', async (req, res) => { const tx = await zirzir.verify(req.query.txRef as string) res.json({ status: tx.status, amount: tx.amount })})
// Webhook handler (server mode only: events are sent by a Zirzir server)app.post('/webhooks/zirzir', express.raw({ type: 'application/json' }), async (req, res) => { let event try { event = zirzir.webhooks.constructEvent( req.body, req.headers['x-zirzir-signature'] as string, process.env.ZIRZIR_WEBHOOK_SECRET!, ) } catch { return res.sendStatus(400) } if (event.type === 'payment.completed') { await fulfillOrder(event.data.reference) } res.sendStatus(200)})
app.listen(3000)Next Steps
Section titled “Next Steps”- verify() & refund() — how to issue full and partial refunds
- Webhooks — reliable event delivery
- Provider Guides — provider-specific quirks and sandbox setup