Skip to content

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.


import { Zirzir } from '@zirzir/sdk'
const zirzir = new Zirzir({
provider: 'chapa',
credentials: {
secretKey: process.env.CHAPA_SECRET_KEY!
}
})

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 here

The 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 */ }
}

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 example
app.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)
})

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 })
}
})
StatusMeaning
pendingPayment initiated, awaiting customer action
successPayment confirmed and settled
failedPayment failed or was declined
cancelledCustomer cancelled on the checkout page
refundedPayment was refunded

Return URLs can fail — the customer closes the tab, their internet drops. Webhooks are the reliable signal. Set up a webhook endpoint:

app.post('/webhooks/zirzir', async (req, res) => {
// Verify webhook signature
const isValid = zirzir.webhooks.verify(
req.rawBody,
req.headers['x-zirzir-signature'] as string
)
if (!isValid) {
return res.status(401).send('Invalid signature')
}
const event = req.body
if (event.type === 'transaction.success') {
await fulfillOrder(event.data.txRef)
}
res.status(200).send('OK')
})

See Webhooks for the full webhook reference including event types, retry behavior, and signature verification.


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 payment
app.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 return
app.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
app.post('/webhooks/zirzir', express.raw({ type: 'application/json' }), async (req, res) => {
const valid = zirzir.webhooks.verify(req.body, req.headers['x-zirzir-signature'] as string)
if (!valid) return res.sendStatus(401)
const event = JSON.parse(req.body.toString())
if (event.type === 'transaction.success') {
await fulfillOrder(event.data.txRef)
}
res.sendStatus(200)
})
app.listen(3000)