Webhooks
Receive real-time payment notifications without polling.
Webhooks
ePay sends an HTTP POST to your configured callback URL whenever a payment event occurs. Set a global callback URL in your dashboard, or supply a per-transaction callbackUrl when initializing — the per-transaction URL always takes precedence.
Webhook delivery requires HTTPS. HTTP callback URLs are not accepted.
Events
| Event | Triggered when |
|---|---|
payment.success | Payment completed successfully. |
payment.failed | Payment attempt failed. |
payment.cancelled | Transaction cancelled via the API or checkout. |
payment.refunding | A refund has been initiated and is being processed. |
payment.refunded | Refund completed successfully. |
payment.reversed | Payment was reversed. |
Payload
Every event delivers the same payload shape:
{
"event": "payment.success",
"mode": "live",
"reference": "PAB12CD3420260813",
"merchantReference": "order_123",
"amount": "250.00",
"serviceFee": "7.50",
"currency": "ETB",
"status": "completed",
"paymentMethod": "telebirr",
"customer": {
"name": "Abebe Bikila",
"email": "abebe@example.com",
"phone": "+251911234567"
},
"paidAt": "2026-08-13T11:47:00.000Z",
"createdAt": "2026-08-13T11:30:00.000Z"
}Prop
Type
Signature Verification
Every request includes an X-Epay-Signature header. Always verify it before processing the payload.
X-Epay-Signature: sha256={hex_signature}The signature is HMAC-SHA256 over the raw request body using your webhook secret key.
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhook(
rawBody: Buffer,
signatureHeader: string,
secret: string,
): boolean {
const received = signatureHeader.replace('sha256=', '');
const expected = createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-epay-signature'] as string;
if (!verifyWebhook(req.body, sig, process.env.EPAY_WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
// handle event...
res.sendStatus(200);
});Always verify against the raw request body before JSON-parsing. Re-stringifying
parsed JSON may alter whitespace or key order, causing valid signatures to fail.
Use timingSafeEqual / hmac.Equal — never === — to prevent timing attacks.
Retries
If your endpoint does not return a 2xx within 10 seconds, ePay retries with exponential backoff.
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | ~1 minute |
| 3 | ~2 minutes |
| 4 | ~4 minutes |
| 5 | ~8 minutes |
After 5 failed attempts the delivery is marked permanently failed.
Respond with 2xx immediately and process the event asynchronously. Slow
responses that exceed 10 seconds are treated as failures. Make your handler
idempotent — use reference to deduplicate repeated deliveries.
Next: Error Codes — full reference of every status code and error message.