Request Signature
Every request to the Telebirr API must be signed with your RSA private key. The server verifies your signature using the public key you registered. Get this wrong and you get "verify sign failed" with no further explanation.
PHP library users: signing is done transparently. You never call the signer directly. This page is the reference for everyone writing raw implementations or debugging a signature mismatch.
How the signature works
Collect all request fields
Flatten biz_content sub-fields into the top-level set. Include everything except sign and sign_type (they don’t exist yet). Skip null and empty-string values — they’re excluded from the signature.
Sort by ASCII key order
Alphabetical order of the key names, case-sensitive. A comes before a in ASCII.
Join as key=value&key=value
appid=930231098009602&business_type=BuyGoods&callback_info=From web&merch_code=101011...Sign with SHA256WithRSA + PSS padding
Feed the joined string into a SHA256 digest, sign with your RSA private key using PSS fill mode (RSASSA-PSS). Base64-encode the result.
Add to the request
Set sign to the base64 string and sign_type to "SHA256WithRSA".
Implementation
function signRequestObject(array $req, string $privateKeyPem): string {
// 1. Flatten biz_content into top-level
$flat = [];
foreach ($req as $k => $v) {
if ($k === 'sign' || $k === 'sign_type') continue;
if ($v === null || $v === '') continue;
if ($k === 'biz_content' && is_array($v)) {
foreach ($v as $bk => $bv) {
if ($bv !== null && $bv !== '') $flat[$bk] = (string) $bv;
}
} else {
$flat[$k] = (string) $v;
}
}
// 2. Sort by ASCII key order
ksort($flat);
// 3. Join as key=value&...
$rawStr = http_build_query($flat);
// 4. Sign with SHA256WithRSA PSS
$privateKey = openssl_pkey_get_private($privateKeyPem);
openssl_sign($rawStr, $signature, $privateKey, OPENSSL_ALGO_SHA256);
// Note: openssl_sign with OPENSSL_ALGO_SHA256 uses PSS by default in most builds.
// If you get verify failures, confirm your OpenSSL version supports PSS.
return base64_encode($signature);
}Example
Given this request body:
{
"timestamp": "1684481139",
"nonce_str": "XG9C5S6R0NLEYF1AGYW5BT237SMDYCUH",
"method": "payment.preorder",
"version": "1.0",
"biz_content": {
"notify_url": "https://your-site.com/notify",
"appid": "930231098961202",
"merch_code": "123456",
"merch_order_id": "1684481138534",
"trade_type": "Checkout",
"title": "diamond_1.5",
"total_amount": "1.5",
"trans_currency": "ETB",
"timeout_express": "120m",
"redirect_url": "https://your-site.com/return"
}
}The pre-signature string (all fields flattened + sorted):
appid=930231098961202&merch_code=123456&merch_order_id=1684481138534&method=payment.preorder&nonce_str=XG9C5S6R0NLEYF1AGYW5BT237SMDYCUH¬ify_url=https://your-site.com/notify&redirect_url=https://your-site.com/return&timeout_express=120m×tamp=1684481139&title=diamond_1.5&total_amount=1.5&trade_type=Checkout&trans_currency=ETB&version=1.0Sign that string, base64-encode the result, put it in sign.
Common failures
| Error | Cause |
|---|---|
verify sign failed | Wrong sort order, wrong field included/excluded, wrong padding algorithm |
parameter validation error | merch_order_id contains non-alphanumeric characters |
| Wrong signature after refactor | biz_content fields not being flattened before signing |
| Works in test, fails in production | Different RSA key pairs for each environment |
Generating RSA keys
# Generate private key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private-key.pem
# Extract public key (share this with Telebirr)
openssl pkey -in private-key.pem -out public-key.pem -puboutKeep private-key.pem secret. Send public-key.pem to Telebirr during onboarding.