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.