H5 C2B IntegrationStep 4: Notify / Callback

Step 4: Notifications

After a payment completes, Telebirr POSTs a signed JSON notification to your notify_url. Verify the signature before trusting anything in it.

🚫

Skipping signature verification is a security hole. An attacker can POST a fake notification claiming any order was paid. Always call verify() first.


The library way

use Melaku\Telebirr\NotificationHandler;
 
// notify.php — your notifyUrl endpoint
$rawBody = file_get_contents('php://input');
 
try {
    $notification = NotificationHandler::parse($rawBody);
} catch (\InvalidArgumentException $e) {
    NotificationHandler::respondError('Invalid JSON')->send();
    exit;
}
 
// Verify signature using Telebirr's public key (in your Config)
if (!NotificationHandler::verify($notification, $config)) {
    NotificationHandler::respondError('Signature verification failed')->send();
    exit;
}
 
if (NotificationHandler::isPaymentSuccessful($notification)) {
    $info = NotificationHandler::extractPaymentInfo($notification);
 
    // $info['merchantOrderId']  — match this to your order
    // $info['paymentOrderId']   — Telebirr's own order ID
    // $info['amount']           — payment amount
    // $info['tradeStatus']      — "Completed"
 
    updateOrderStatus($info['merchantOrderId'], 'paid');
    fulfillOrder($info['merchantOrderId']);
}
 
// Always respond 200 — Telebirr retries up to 15 times if it doesn't get 200
NotificationHandler::respondSuccess()->send();

Belt and braces (PHP v2.2.0+, JS v3.1.0+): before fulfilling, confirm the status server-to-server with one call — $client->getOrderStatus($info['merchantOrderId']) / await client.getOrderStatus(info.merchantOrderId) — and check paid and the amount against your own order. The signature proves the notification wasn’t tampered with; the query proves the money actually moved. Both libraries’ READMEs document the full idempotent settlement pattern (the browser return and this notification race — use a compare-and-set so they can’t double-fulfill).


Raw API (all languages)

// Raw PHP — without the library
$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, 'message' => 'Missing signature']);
    exit;
}
 
// Rebuild the signed string: extract sign, sort remaining fields by ASCII
$sign = $notification['sign'];
$data = $notification;
unset($data['sign'], $data['sign_type']);
 
ksort($data);
$rawStr = http_build_query($data); // key=value&...
 
// Verify against Telebirr's public key with SHA256WithRSA PSS
$publicKey = openssl_pkey_get_public($telebirrPublicKeyPem);
$result    = openssl_verify(
    $rawStr,
    base64_decode($sign),
    $publicKey,
    OPENSSL_ALGO_SHA256
);
 
if ($result !== 1) {
    http_response_code(403);
    echo json_encode(['success' => false, 'message' => 'Invalid signature']);
    exit;
}
 
if (($notification['trade_status'] ?? '') === 'Completed') {
    $orderId = $notification['merch_order_id'];
    updateOrderStatus($orderId, '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": "10.00",
  "trans_currency": "ETB",
  "trade_status": "Completed",
  "trans_end_time": "1670575472000",
  "sign": "AOwWQF0QDg0jzzs5...",
  "sign_type": "SHA256WithRSA"
}

Payment status values

trade_statusMeaning
CompletedPayment successful ✅
PayingUser has initiated payment but process isn’t complete
PendingWaiting for payment
ExpiredPayment window closed without payment
FailurePayment failed

Retry behavior

If your endpoint doesn’t respond with HTTP 200, Telebirr retries the notification up to 15 times on this schedule:

15s → 15s → 30s → 3m → 10m → 20m → 30m → 30m → 60m → 3h → 3h → 3h → 3h → 6h → 6h

Make your notification handler idempotent — check merch_order_id before updating order state to avoid double-fulfillment.

⚠️

Your notify URL must be publicly reachable and whitelisted by Telebirr. Localhost won’t work. Use a tool like ngrok for local testing.