Appearance
OmniVAS - Digital Voucher Card Merchant API
Integration Guide & Technical Reference
Document Revision History
| Version | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2025-12-19 | Omniva Team | Initial release |
Table of Contents
Introduction
This document provides comprehensive technical specifications for integrating with OmniVAS Gift Voucher Redemption API. It covers design principles, object definitions, operational behaviors, and error handling procedures.
Intended Audience: Technical teams responsible for implementing merchant integrations with the OmniVAS platform.
Getting Started
Prerequisites
| Step | Action | Details |
|---|---|---|
| 1 | Account Signup | Complete registration via the account activation email from the OmniVAS Business Team. Activation must occur within 24 hours. |
| 2 | Portal Login | Access the merchant portal at tmerchants.telesend.et |
| 3 | API Configuration | Navigate to /manage-apis to enable API access, configure IP whitelisting, and generate API credentials. |
IMPORTANT
- Ensure API Integration is enabled before proceeding.
- Store API credentials securely using industry-standard practices.
- Whitelist valid IP addresses (format:
xxx.xxx.xxx.xxx) or Fully Qualified Domain Names (FQDN).
Base URLs
| Environment | Base URL |
|---|---|
| Production | https://api.telesend.et/api/v1/merchant/client |
| Test | https://tapi.telesend.et/api/v1/merchant/client |
Authentication
Authentication Flow
Step 1: Obtain API Key
Retrieve your API key from the Manage API Page.
Step 2: Configure IP Whitelist
Register your server IP address(es) in the portal's Manage APIs section.
Step 3: Generate and Register Public Key
To ensure secure and tamper-proof communications, all request bodies must be digitally signed.
Key Generation Requirements:
- Generate a private/public key pair.
- Use the private key to sign your request payloads.
- Register the public key in the portal's Manage APIs section (Public Key field).
- The registered public key will be used to verify incoming requests.
Generate Key Pair
sh
# Install OpenSSL if not already installed
sudo apt-get install openssl
# Generate private key
openssl ecparam -name secp256k1 -genkey -noout -out ec-secp256k1-priv-key.pem
# Generate public key from private key
openssl ec -in ec-secp256k1-priv-key.pem -pubout > ec-secp256k1-pub-key.pemjs
import crypto from 'node:crypto';
import fs from 'node:fs';
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'secp256k1',
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'sec1',
format: 'pem',
},
});
fs.writeFileSync('ec-secp256k1-priv-key.pem', privateKey);
fs.writeFileSync('ec-secp256k1-pub-key.pem', publicKey);
console.log('Generated ec-secp256k1-priv-key.pem and ec-secp256k1-pub-key.pem');After executing the above commands, you will have two .pem files in your current directory:
ec-secp256k1-priv-key.pem— Keep this private key secure and use it when signing requests to the server.ec-secp256k1-pub-key.pem— Copy and register this public key in the portal's Manage APIs section.
IMPORTANT
Signature verification in OmniVAS is performed over the exact JSON payload string (server-side JSON.stringify(request.body)) using:
- Algorithm:
SHA256 - Key type:
EC secp256k1 - Signature encoding:
IEEE-P1363 - Header:
x-signature: <base64-signature>
Signature Implementation Snippets
Use these snippets to generate x-signature for all non-GET endpoints.
js
const crypto = require('crypto');
const fs = require('fs');
const privateKey = fs.readFileSync('./ec-secp256k1-priv-key.pem', 'utf8');
const payloadObject = {
voucherCode: '3225118500682251',
amount: 500,
idempotencyKey: 'unique-key-123',
metadata: '{}',
};
// Sign the exact JSON string you send in request body
const payload = JSON.stringify(payloadObject);
const signature = crypto
.sign('sha256', Buffer.from(payload), {
key: privateKey,
dsaEncoding: 'ieee-p1363',
})
.toString('base64');
console.log('Payload:', payload);
console.log('x-signature:', signature);python
import base64
import json
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
with open('ec-secp256k1-priv-key.pem', 'rb') as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
payload_obj = {
'voucherCode': '3225118500682251',
'amount': 500,
'idempotencyKey': 'unique-key-123',
'metadata': '{}'
}
# Match server-side canonical style (compact JSON)
payload = json.dumps(payload_obj, separators=(',', ':'))
der_signature = private_key.sign(payload.encode('utf-8'), ec.ECDSA(hashes.SHA256()))
r, s = utils.decode_dss_signature(der_signature)
# Convert DER (r,s) -> IEEE-P1363 (r||s), 32 bytes each for secp256k1
sig_p1363 = r.to_bytes(32, 'big') + s.to_bytes(32, 'big')
signature_b64 = base64.b64encode(sig_p1363).decode('utf-8')
print('Payload:', payload)
print('x-signature:', signature_b64)sh
curl -X POST "BASE_URL/topup-transactions/initiate-claim" \
-H "Content-Type: application/json" \
-H "x-api-token: ACCESS_TOKEN" \
-H "x-signature: BASE64_P1363_SIGNATURE" \
-d '{"voucherCode":"3225118500682251","amount":500,"idempotencyKey":"unique-key-123","metadata":"{}"}'NOTE
x-signatureis required on endpoints protected byMerchantSignatureGuard(currently merchant topup transaction POST endpoints).- GET endpoints under topup transactions (for example
GET /topup-transactions/:voucherCode) do not requirex-signature. - Sign exactly the body you send, with the same field names and values.
Step 4: Obtain Access Token
Request:
sh
curl -X POST "BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d '{"key": "YOUR_API_KEY"}'js
const response = await fetch('BASE_URL/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key: 'YOUR_API_KEY' }),
});
const data = await response.json();
console.log(data);python
import requests
response = requests.post(
'BASE_URL/auth/login',
headers={'Content-Type': 'application/json'},
json={'key': 'YOUR_API_KEY'}
)
print(response.status_code)
print(response.json())Response:
json
{
"accessToken": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
"success": true,
"statusCode": 200,
"message": "Login Successful"
}NOTE
- Access tokens are valid for 300 seconds (5 minutes).
- Token requests must originate from a secure back-channel only.
Step 5: Verify Configuration
Request:
sh
curl -X POST "https://tapi.telesend.et/api/v1/merchant/client/test" \
-H "x-api-token: ACCESS_TOKEN"js
const response = await fetch('https://tapi.telesend.et/api/v1/merchant/client/test', {
method: 'POST',
headers: {
'x-api-token': 'ACCESS_TOKEN',
},
});
const data = await response.json();
console.log(data);python
import requests
response = requests.post(
'https://tapi.telesend.et/api/v1/merchant/client/test',
headers={'x-api-token': 'ACCESS_TOKEN'}
)
print(response.status_code)
print(response.json())Expected Response:
json
{
"test": "successful"
}Gift Voucher Redemption
Gift vouchers are prepaid digital cards with a specified monetary value, redeemable at OmniVAS-integrated merchants.
Redemption Flow Overview
1. Initiate Claim
Initiates the voucher redemption process and dispatches a verification code to the voucher owner via SMS.
Endpoint: POST /topup-transactions/initiate-claim
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-api-token | string | Yes | Access token obtained from login |
x-signature | string | Yes | Digitally signed payload using your private key |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
voucherCode | string | Yes | Voucher code to claim |
amount | number | No | Amount to redeem (defaults to full voucher balance if not specified) |
idempotencyKey | string | Yes | Unique key ensuring request idempotency and preventing duplicate processing |
metadata | string | No | Optional JSON string containing additional contextual data (eg. merchant name, product name, product price etc) |
Example Request
sh
curl -X POST "BASE_URL/topup-transactions/initiate-claim" \
-H "Content-Type: application/json" \
-H "x-api-token: ACCESS_TOKEN" \
-H "x-signature: SIGNED_PAYLOAD" \
-d '{
"voucherCode": "3225118500682251",
"amount": 500,
"idempotencyKey": "unique-key-123",
"metadata": "{}"
}'js
import crypto from 'node:crypto';
import fs from 'node:fs';
const privateKey = fs.readFileSync('./ec-secp256k1-priv-key.pem', 'utf8');
const body = {
voucherCode: '3225118500682251',
amount: 500,
idempotencyKey: 'unique-key-123',
metadata: '{}',
};
const payload = JSON.stringify(body);
const signature = crypto
.sign('sha256', Buffer.from(payload), {
key: privateKey,
dsaEncoding: 'ieee-p1363',
})
.toString('base64');
const response = await fetch('BASE_URL/topup-transactions/initiate-claim', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-token': 'ACCESS_TOKEN',
'x-signature': signature,
},
body: payload,
});
console.log(await response.json());python
import base64
import json
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
with open('ec-secp256k1-priv-key.pem', 'rb') as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
body = {
'voucherCode': '3225118500682251',
'amount': 500,
'idempotencyKey': 'unique-key-123',
'metadata': '{}'
}
payload = json.dumps(body, separators=(',', ':'))
der_signature = private_key.sign(payload.encode('utf-8'), ec.ECDSA(hashes.SHA256()))
r, s = utils.decode_dss_signature(der_signature)
signature = base64.b64encode(r.to_bytes(32, 'big') + s.to_bytes(32, 'big')).decode('utf-8')
response = requests.post(
'BASE_URL/topup-transactions/initiate-claim',
headers={
'Content-Type': 'application/json',
'x-api-token': 'ACCESS_TOKEN',
'x-signature': signature,
},
data=payload,
)
print(response.status_code)
print(response.json())Success Response
json
{
"success": true,
"message": "OTP Code has been sent to user Successfully",
"statusCode": 200,
"data": {
"message": "OTP Code has been sent to user Successfully",
"idempotencyKey": "unique-key-123",
"metadata": "{}"
}
}Response Body Explanations
| Field | Type | Description |
|---|---|---|
message | String | Confirmation message that OTP was sent |
idempotencyKey | String | The idempotency key from your request (allows you to verify response matches your submission) |
metadata | String | The metadata from your request (allows you to verify response matches your submission) |
Error Response Format
json
{
"statusCode": 4001,
"success": false,
"message": "Error description",
"data": null
}Error Codes
| Status Code | Error Message |
|---|---|
| 4041 | No voucher found with specified code |
| 4042 | Voucher is already redeemed |
| 4091 | Amount exceeds voucher balance |
| 5001 | Error fetching merchant service fee |
| 5002 | Failed to update voucher status |
| 5004 | Database transaction failed |
2. Get Voucher Detail Information
This gives you detail information based on the voucher code. This can be used to get full information about a voucher such as its remaining balance, expiration date and its current status.
Endpoint: GET /topup-transactions/:voucherCode
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-api-token | string | Yes | Access token obtained from login |
Success Response
json
{
"statusCode": 200,
"success": true,
"message": "Successful",
"data": {
"voucher": {
"id": 118,
"code": "2585771156933526",
"serialNumber": "1000000000046",
"amount": "50.000000",
"remainingBalance": "0.005000",
"status": "REDEEM_INITIATED",
"expirationDate": "2025-12-21T22:10:17.000Z"
},
"serviceFee": "1.000000"
}
}Response Body Explanations
| Field | Type | Description |
|---|---|---|
voucher | Object | Detailed Voucher data showing information about particular voucher |
serviceFee | String | Service Fee attached to the voucher |
The voucher object in the response holds these informations about a voucher.
| Field | Type | Description |
|---|---|---|
id | Number | Unique identifier for the voucher in the system |
code | String | Unique voucher code |
serialNumber | String | Serial number assigned to the voucher |
amount | String | Original voucher amount |
remainingBalance | String | Available balance remaining on the voucher |
status | String | Current voucher status (e.g., ACTIVE, REDEEM_INITIATED, REDEEMED) |
expirationDate | String | Date and time when the voucher expires (ISO 8601 format) |
3. Redeem Gift
Completes the redemption process and transfers funds to the merchant account.
Endpoint: POST /topup-transactions/redeem-gift
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-api-token | string | Yes | Access token obtained from login |
x-signature | string | Yes | Digitally signed payload using your private key |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
voucherCode | string | Yes | Voucher code to redeem |
verificationCode | string | Yes | SMS verification code received by voucher owner |
msisdn | string | No | Phone number of client (defaults to voucher owner if not specified) |
idempotencyKey | string | Yes | Unique key ensuring request idempotency and preventing duplicate processing |
metadata | string | No | Optional JSON string containing additional contextual data |
Example Request
sh
curl -X POST "BASE_URL/topup-transactions/redeem-gift" \
-H "Content-Type: application/json" \
-H "x-api-token: ACCESS_TOKEN" \
-H "x-signature: SIGNED_PAYLOAD" \
-d '{
"voucherCode": "3225118500682251",
"verificationCode": "735191",
"idempotencyKey": "unique-key-123",
"metadata": "{}"
}'js
import crypto from 'node:crypto';
import fs from 'node:fs';
const privateKey = fs.readFileSync('./ec-secp256k1-priv-key.pem', 'utf8');
const body = {
voucherCode: '3225118500682251',
verificationCode: '735191',
idempotencyKey: 'unique-key-123',
metadata: '{}',
};
const payload = JSON.stringify(body);
const signature = crypto
.sign('sha256', Buffer.from(payload), {
key: privateKey,
dsaEncoding: 'ieee-p1363',
})
.toString('base64');
const response = await fetch('BASE_URL/topup-transactions/redeem-gift', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-token': 'ACCESS_TOKEN',
'x-signature': signature,
},
body: payload,
});
console.log(await response.json());python
import base64
import json
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
with open('ec-secp256k1-priv-key.pem', 'rb') as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
body = {
'voucherCode': '3225118500682251',
'verificationCode': '735191',
'idempotencyKey': 'unique-key-123',
'metadata': '{}'
}
payload = json.dumps(body, separators=(',', ':'))
der_signature = private_key.sign(payload.encode('utf-8'), ec.ECDSA(hashes.SHA256()))
r, s = utils.decode_dss_signature(der_signature)
signature = base64.b64encode(r.to_bytes(32, 'big') + s.to_bytes(32, 'big')).decode('utf-8')
response = requests.post(
'BASE_URL/topup-transactions/redeem-gift',
headers={
'Content-Type': 'application/json',
'x-api-token': 'ACCESS_TOKEN',
'x-signature': signature,
},
data=payload,
)
print(response.status_code)
print(response.json())Success Response
json
{
"success": true,
"message": "Gift redeemed successfully",
"statusCode": 200,
"data": {
"response": "Success",
"referenceNumber": "TBR123456789",
"omnivasReferenceNumber": "TX-1234567",
"idempotencyKey": "unique-key-123",
"metadata": "{}"
}
}Response Body Explanations
| Field | Type | Description |
|---|---|---|
response | String | Status message indicating redemption success |
referenceNumber | String | Reference number from telebirr confirming fund transfer to merchant |
omnivasReferenceNumber | String | OmniVAS unique reference for tracking and support inquiries |
idempotencyKey | String | The idempotency key from your request (allows you to verify response matches your submission) |
metadata | String | The metadata from your request (allows you to verify response matches your submission) |
Error Response Format
json
{
"statusCode": 4031,
"success": false,
"message": "Error description",
"data": null
}Error Codes
| Status Code | Error Message |
|---|---|
| 4031 | The given voucher is restricted. Merchant cannot be changed for this voucher |
| 4032 | Voucher is already redeemed |
| 4033 | Verification code is missing |
| 4034 | No voucher redemption process initiated |
| 4035 | The provided verification code is incorrect |
| 4036 | Voucher expiration date has passed |
| 4041 | No voucher found with specified code |
| 4043 | No merchant found with the given ID |
| 4044 | Voucher balance is insufficient for transaction |
| 4045 | Specified partner company is not available |
| 4091 | Amount exceeds voucher balance |
| 4092 | Transaction mismatch error. Please retry |
| 4093 | Reserved balance update failed. Please contact support |
| 4094 | Redemption record creation failed |
| 4095 | Reserved balance debit from company failed |
| 4096 | Remaining balance cannot be negative after service fee deduction |
| 5002 | Failed to update voucher status |
| 5003 | Failed to queue SMS notification |
| 5004 | Database transaction failed |
| 5031 | Transfer failed: [Provider error message] |
End-to-End Example (Single Snippet)
js
import crypto from 'node:crypto';
import fs from 'node:fs';
const BASE_URL = 'https://tapi.telesend.et/api/v1/merchant/client';
const API_KEY = 'YOUR_API_KEY';
const VOUCHER_CODE = '3225118500682251';
const VERIFICATION_CODE = '735191';
const IDEMPOTENCY_KEY = 'unique-key-123';
function generateKeyPairIfMissing() {
const privPath = 'ec-secp256k1-priv-key.pem';
const pubPath = 'ec-secp256k1-pub-key.pem';
if (!fs.existsSync(privPath) || !fs.existsSync(pubPath)) {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'secp256k1',
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'sec1', format: 'pem' },
});
fs.writeFileSync(privPath, privateKey);
fs.writeFileSync(pubPath, publicKey);
console.log('Generated ec-secp256k1-priv-key.pem and ec-secp256k1-pub-key.pem');
}
}
function signPayload(payload, privateKeyPem) {
return crypto
.sign('sha256', Buffer.from(payload), {
key: privateKeyPem,
dsaEncoding: 'ieee-p1363',
})
.toString('base64');
}
async function postWithSignature(path, body, accessToken, privateKeyPem) {
const payload = JSON.stringify(body);
const signature = signPayload(payload, privateKeyPem);
const res = await fetch(`${BASE_URL}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-token': accessToken,
'x-signature': signature,
},
body: payload,
});
return res.json();
}
async function main() {
generateKeyPairIfMissing();
console.log('Register ec-secp256k1-pub-key.pem in Manage APIs before signed calls.');
const privateKeyPem = fs.readFileSync('ec-secp256k1-priv-key.pem', 'utf8');
const loginRes = await fetch(`${BASE_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: API_KEY }),
});
const loginData = await loginRes.json();
const accessToken = loginData.accessToken;
if (!accessToken) {
throw new Error(`Login failed: ${JSON.stringify(loginData)}`);
}
const initiateResult = await postWithSignature(
'/topup-transactions/initiate-redeem',
{
voucherCode: VOUCHER_CODE,
amount: 500,
idempotencyKey: IDEMPOTENCY_KEY,
metadata: '{}',
},
accessToken,
privateKeyPem
);
console.log('Initiate response:', initiateResult);
const redeemResult = await postWithSignature(
'/topup-transactions/redeem-gift',
{
voucherCode: VOUCHER_CODE,
verificationCode: VERIFICATION_CODE,
idempotencyKey: IDEMPOTENCY_KEY,
metadata: '{}',
},
accessToken,
privateKeyPem
);
console.log('Redeem response:', redeemResult);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});python
import base64
import json
import os
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
BASE_URL = 'https://tapi.telesend.et/api/v1/merchant/client'
API_KEY = 'YOUR_API_KEY'
VOUCHER_CODE = '3225118500682251'
VERIFICATION_CODE = '735191'
IDEMPOTENCY_KEY = 'unique-key-123'
def generate_keypair_if_missing():
priv_path = 'ec-secp256k1-priv-key.pem'
pub_path = 'ec-secp256k1-pub-key.pem'
if not os.path.exists(priv_path) or not os.path.exists(pub_path):
private_key = ec.generate_private_key(ec.SECP256K1())
public_key = private_key.public_key()
priv_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
pub_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
with open(priv_path, 'wb') as f:
f.write(priv_pem)
with open(pub_path, 'wb') as f:
f.write(pub_pem)
print('Generated ec-secp256k1-priv-key.pem and ec-secp256k1-pub-key.pem')
def sign_payload(payload, private_key):
der_signature = private_key.sign(payload.encode('utf-8'), ec.ECDSA(hashes.SHA256()))
r, s = utils.decode_dss_signature(der_signature)
p1363 = r.to_bytes(32, 'big') + s.to_bytes(32, 'big')
return base64.b64encode(p1363).decode('utf-8')
def post_with_signature(path, body, access_token, private_key):
payload = json.dumps(body, separators=(',', ':'))
signature = sign_payload(payload, private_key)
response = requests.post(
f'{BASE_URL}{path}',
headers={
'Content-Type': 'application/json',
'x-api-token': access_token,
'x-signature': signature,
},
data=payload,
)
return response.json()
def main():
generate_keypair_if_missing()
print('Register ec-secp256k1-pub-key.pem in Manage APIs before signed calls.')
with open('ec-secp256k1-priv-key.pem', 'rb') as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
login_response = requests.post(
f'{BASE_URL}/auth/login',
headers={'Content-Type': 'application/json'},
json={'key': API_KEY},
).json()
access_token = login_response.get('accessToken')
if not access_token:
raise RuntimeError(f'Login failed: {login_response}')
initiate_result = post_with_signature(
'/topup-transactions/initiate-redeem',
{
'voucherCode': VOUCHER_CODE,
'amount': 500,
'idempotencyKey': IDEMPOTENCY_KEY,
'metadata': '{}',
},
access_token,
private_key,
)
print('Initiate response:', initiate_result)
redeem_result = post_with_signature(
'/topup-transactions/redeem-gift',
{
'voucherCode': VOUCHER_CODE,
'verificationCode': VERIFICATION_CODE,
'idempotencyKey': IDEMPOTENCY_KEY,
'metadata': '{}',
},
access_token,
private_key,
)
print('Redeem response:', redeem_result)
if __name__ == '__main__':
main()Security Best Practices
This section outlines critical security measures for merchants integrating with the Gift Voucher API.
Security Architecture Overview
Credential Management
| Practice | Priority | Description |
|---|---|---|
| Environment Isolation | Critical | Use separate API keys for test and production environments |
| Key Rotation | High | Rotate API keys every 90 days or immediately upon suspected compromise |
| Access Control | High | Restrict API key access to authorized personnel only |
WARNING
- Never hardcode API keys in source code.
- Never commit credentials to version control systems.
- Never expose API keys in client-side applications or logs.
Support
For technical support or integration assistance, please contact the OmniVAS Business Team.
© 2025 OmniVAS. All rights reserved.