Payments

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

EventTriggered when
payment.successPayment completed successfully.
payment.failedPayment attempt failed.
payment.cancelledTransaction cancelled via the API or checkout.
payment.refundingA refund has been initiated and is being processed.
payment.refundedRefund completed successfully.
payment.reversedPayment 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.

AttemptDelay
1immediate
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.

On this page