Step 4: Notifications
Same as the H5 C2B flow — after payment, Telebirr POSTs a signed JSON notification to your notify_url. Verify the signature before trusting anything in it.
🚫
Don’t skip signature verification. An attacker can POST a fake “payment succeeded” notification to your endpoint. Always call NotificationHandler::verify() first.
The library way
use Melaku\Telebirr\NotificationHandler;
$rawBody = file_get_contents('php://input');
try {
$notification = NotificationHandler::parse($rawBody);
} catch (\InvalidArgumentException $e) {
NotificationHandler::respondError('Invalid JSON')->send();
exit;
}
if (!NotificationHandler::verify($notification, $config)) {
NotificationHandler::respondError('Signature verification failed')->send();
exit;
}
if (NotificationHandler::isPaymentSuccessful($notification)) {
$info = NotificationHandler::extractPaymentInfo($notification);
// $info['merchantOrderId'] — match to your order
// $info['paymentOrderId'] — Telebirr's reference ID
// $info['amount'] — payment amount
// $info['tradeStatus'] — "Completed"
updateOrderStatus($info['merchantOrderId'], 'paid');
fulfillOrder($info['merchantOrderId']);
}
// Always respond 200 — Telebirr retries 15 times if it doesn't get 200
NotificationHandler::respondSuccess()->send();Raw API (all languages)
$rawBody = file_get_contents('php://input');
$notification = json_decode($rawBody, true);
if (!$notification || empty($notification['sign'])) {
http_response_code(400);
echo json_encode(['success' => false]);
exit;
}
$sign = $notification['sign'];
$data = $notification;
unset($data['sign'], $data['sign_type']);
ksort($data);
$rawStr = http_build_query($data);
$publicKey = openssl_pkey_get_public($telebirrPublicKeyPem);
$valid = openssl_verify($rawStr, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
if ($valid !== 1) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Invalid signature']);
exit;
}
if (($notification['trade_status'] ?? '') === 'Completed') {
updateOrderStatus($notification['merch_order_id'], 'paid');
}
http_response_code(200);
echo json_encode(['success' => true]);Notification payload
{
"notify_url": "https://your-site.com/pay/notify",
"appid": "853694808089634",
"notify_time": "1670575472482",
"merch_code": "245445",
"merch_order_id": "1670575560882",
"payment_order_id": "00801104C911443200001002",
"total_amount": "50.00",
"trans_currency": "ETB",
"trade_status": "Completed",
"trans_end_time": "1670575472000",
"sign": "AOwWQF0QDg0jzzs5...",
"sign_type": "SHA256WithRSA"
}Retry behavior
Respond HTTP 200 or Telebirr retries up to 15 times:
15s → 15s → 30s → 3m → 10m → 20m → 30m → 30m → 60m → 3h → 3h → 3h → 3h → 6h → 6h
Make your handler idempotent — check if you’ve already processed merch_order_id before updating order state.