Appearance
Merchant-Redeem Flow
Use this merchant-redeem flow for server-to-server voucher redemption:
- Log in with the merchant API key to receive an
x-api-token. - Look up the voucher by code.
- Initiate the claim and trigger OTP delivery.
- Redeem the gift with the OTP.
- Send the external redemption callback when the downstream redemption finishes.
Use the endpoint paths shown in each step.
Response Envelope
Merchant-redeem endpoints return this envelope:
json
{
"success": true,
"message": "Successful",
"statusCode": 200,
"data": {}
}Business failures can return HTTP 200 with success: false and an application statusCode in the response body, so branch on success before completing checkout.
Authentication and Signing
| Header | Required | Description |
|---|---|---|
x-api-token | Yes, except login | Token returned by POST /api/v1/merchant/client/auth/login. |
x-signature | Yes for signed POST endpoints | Base64 ECDSA P-256 SHA-256 signature over JSON.stringify(request body). |
User-Agent | No | Optional client identifier. |
The GET voucher lookup requires x-api-token only. initiate-claim, redeem-gift, and redeem-gift/callback require both x-api-token and x-signature.
End-to-End Flow
Step 1: Login
POST /api/v1/merchant/client/auth/login
Request Body
| Field | Type | Required | Rules |
|---|---|---|---|
key | string | Yes | Merchant API key configured for the merchant API access record. |
Example Request
bash
curl -X POST "$MERCHANT_API_BASE_URL/merchant/client/auth/login" \
-H "Content-Type: application/json" \
-d '{
"key": "merchant_live_xxxxxxxxxxxxx"
}'Example Response
json
{
"accessToken": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
"success": true,
"statusCode": 200,
"message": "Login Successful"
}Use accessToken as the x-api-token header for the remaining merchant-redeem calls.
Step 2: Look Up Voucher
GET /api/v1/merchant/client/topup-transactions/:voucherCode
Lookup confirms the voucher exists, is redeemable by the authenticated merchant, and shows the current balance and service fee.
Voucher Code Rules
- Use the 16-digit
voucherCodereturned by partner voucher issuance. - Hyphen-separated display input such as
9876-5432-1098-7654is accepted and normalized. - Do not use the issuance
serialNumber(VCH-...) as a redemption code.
Example Request
bash
curl -X GET "$MERCHANT_API_BASE_URL/merchant/client/topup-transactions/9876543210987654" \
-H "x-api-token: $MERCHANT_API_TOKEN"Example Response
json
{
"success": true,
"message": "Successful",
"statusCode": 200,
"data": {
"voucher": {
"id": 123,
"code": "9876543210987654",
"serialNumber": "VCH-20260427-A1B2C3D4E5F67890",
"amount": "500.000000",
"remainingBalance": "350.000000",
"status": "PARTIALLY_REDEEMED",
"expirationDate": "2026-07-27T23:59:59.000Z"
},
"serviceFee": "1.000000"
}
}Voucher status values are UNUSED, PARTIALLY_REDEEMED, REDEEMED, EXPIRED, and REDEEM_INITIATED.
Step 3: Initiate Claim
POST /api/v1/merchant/client/topup-transactions/initiate-claim
Claim initiation creates the pending redemption claim and sends a 6-digit OTP to the voucher owner.
Request Body
| Field | Type | Required | Rules |
|---|---|---|---|
voucherCode | string | Yes | Voucher code to claim. Hyphens are stripped before validation. |
amount | number | No | Amount to redeem. Omit to redeem the full remaining balance net of service fee. |
idempotencyKey | string | No | Client retry key stored in the request body. |
metadata | string | No | Merchant metadata to persist on the redemption. |
merchantId | number | No | Optional body-level merchant check. Must match the authenticated merchant when provided. |
msisdn | string | No | Optional voucher owner phone check. |
userId | number | No | Optional merchant user id for audit attribution. |
channel | string | No | API or APP; defaults to API. |
Example Request
bash
curl -X POST "$MERCHANT_API_BASE_URL/merchant/client/topup-transactions/initiate-claim" \
-H "Content-Type: application/json" \
-H "x-api-token: $MERCHANT_API_TOKEN" \
-H "x-signature: $MERCHANT_SIGNATURE" \
-d '{
"voucherCode": "9876543210987654",
"amount": 50,
"idempotencyKey": "merchant-redemption-001",
"metadata": "{\"checkoutId\":\"CHK-1001\"}",
"channel": "API"
}'Example Response
json
{
"success": true,
"message": "Successful",
"statusCode": 200,
"data": {
"message": "OTP Code has been sent to user Successfully",
"idempotencyKey": "merchant-redemption-001"
}
}Step 4: Redeem Gift
POST /api/v1/merchant/client/topup-transactions/redeem-gift
Submit the OTP as verificationCode. A successful response returns referenceNumber and omnivasReferenceNumber; send both back in the callback after external redemption finishes.
Request Body
| Field | Type | Required | Rules |
|---|---|---|---|
voucherCode | string | Yes | Voucher code with optional hyphens. |
verificationCode | string | Yes | Exactly 6 digits. |
idempotencyKey | string | No | Same key used for the same redemption retry. |
metadata | string | No | Merchant metadata to persist on the redemption. |
merchantId | number | No | Optional merchant check; must match the authenticated merchant. |
msisdn | string | No | Optional voucher owner phone check. |
userId | number | No | Optional merchant user id for audit attribution. |
channel | string | No | API or APP; defaults to API. |
Example Request
bash
curl -X POST "$MERCHANT_API_BASE_URL/merchant/client/topup-transactions/redeem-gift" \
-H "Content-Type: application/json" \
-H "x-api-token: $MERCHANT_API_TOKEN" \
-H "x-signature: $MERCHANT_SIGNATURE" \
-d '{
"voucherCode": "9876543210987654",
"verificationCode": "123456",
"idempotencyKey": "merchant-redemption-001",
"metadata": "{\"checkoutId\":\"CHK-1001\"}"
}'Example Response
json
{
"success": true,
"message": "Successful",
"statusCode": 200,
"data": {
"amount": "50.000000",
"response": "Success",
"referenceNumber": "red_01HWJ9S8E4Y9G7E4F6N5Q2P3Z8",
"omnivasReferenceNumber": "7721",
"remainingBalance": "299.000000",
"metadata": "{\"checkoutId\":\"CHK-1001\"}",
"to": "+251911234567"
}
}omnivasReferenceNumber is the ODM claim reference. Use it in the callback request.
Step 5: Send Callback
POST /api/v1/merchant/redeem-gift/callback
Use this endpoint to complete the pending external redemption prepared by redeem-gift.
Request Body
| Field | Type | Required | Rules |
|---|---|---|---|
callbackId | string | Yes | Unique callback id for idempotent callback replay. |
omnivasReferenceNumber | string | Yes | Claim reference returned by redeem-gift. |
referenceNumber | string | No | Pending reference returned by redeem-gift. |
voucherCode | string | Yes | Voucher code redeemed externally. |
status | string | Yes | SUCCESS or FAILED. |
telebirrRef | string | Required for SUCCESS | External settlement reference. |
processedAt | string | No | ISO timestamp when the external redemption was processed. |
amount | string | No | Redeemed amount for callback validation. |
failureCode | string | No | External failure code for failed callbacks. |
failureReason | string | Required for FAILED | External failure reason. |
metadata | object or string | No | Additional external redemption metadata. |
Success Callback Request
bash
curl -X POST "$MERCHANT_API_BASE_URL/merchant/redeem-gift/callback" \
-H "Content-Type: application/json" \
-H "x-api-token: $MERCHANT_API_TOKEN" \
-H "x-signature: $MERCHANT_SIGNATURE" \
-d '{
"callbackId": "cb-merchant-001",
"omnivasReferenceNumber": "7721",
"referenceNumber": "red_01HWJ9S8E4Y9G7E4F6N5Q2P3Z8",
"voucherCode": "9876543210987654",
"status": "SUCCESS",
"telebirrRef": "TB-240427-000125",
"processedAt": "2026-04-29T12:03:24.000Z",
"amount": "50.000000",
"metadata": {
"checkoutId": "CHK-1001"
}
}'Success Callback Response
json
{
"success": true,
"message": "Successful",
"statusCode": 200,
"data": {
"status": "COMPLETED",
"claimId": 7721,
"redemptionId": 9801,
"telebirrRef": "TB-240427-000125",
"message": "External redemption success applied."
}
}Failed Callback Request
json
{
"callbackId": "cb-merchant-002",
"omnivasReferenceNumber": "7721",
"referenceNumber": "red_01HWJ9S8E4Y9G7E4F6N5Q2P3Z8",
"voucherCode": "9876543210987654",
"status": "FAILED",
"failureCode": "EXTERNAL_DECLINED",
"failureReason": "External redemption was declined.",
"amount": "50.000000"
}Retry and Idempotency
- Merchant-redeem idempotency uses the optional body field
idempotencyKey, not theIdempotency-Keyheader. - Reuse the same
idempotencyKeyonly when retrying the same claim or redemption request. - For
initiate-claim, a matching pending claim returns the same success message andidempotencyKey. - For
redeem-gift, a completed or awaiting redemption with the same key returns the stored redemption reference. - For callbacks, use a unique
callbackId; replaying the same successful callback returns the completed result.
Response Codes
| Code | Meaning |
|---|---|
200 | Request processed. Check body success and statusCode for the final result. |
401 | Missing or invalid x-api-token, missing or invalid x-signature, or invalid login key. |
4033 | Verification code missing or no voucher redeem process started. |
4035 | Verification code is not correct. |
4037 | Verification code has expired. |
4038 | Maximum verification attempts exceeded. |
4041 | No voucher found with specified code. |
4042 | Voucher is already redeemed. |
4091 | Amount exceeds voucher balance. |
4092 | Transaction mismatch. |
5031 | Downstream transfer failed. |