api ยท 8 min read
Building a Payment Verification System with cheki's Free API
Complete developer guide to integrating Ethiopian bank receipt verification into your app, POS system, or e-commerce checkout with cheki's free REST API.
This guide walks through integrating cheki's free REST API into a real application. No API key, no signup, no rate limits. We'll cover single verification, batch verification, error handling, and fraud prevention patterns.
API overview
| Endpoint | Method | Purpose |
|---|---|---|
| /api/verify | POST | Verify a single receipt |
| /api/verify/batch | POST | Verify up to 50 receipts at once |
| /api/banks | GET | List supported banks and status |
| /api/health | GET | Check API and per-bank endpoint latency |
Base URL: https://chekiapp.vercel.app
No authentication
cheki's API requires no API key, no Bearer token, and no signup. Just POST and get JSON. This is different from check.et which requires a business account and Authorization header.
Single verification
The most common use case: verify one receipt at a time.
curl -X POST https://chekiapp.vercel.app/api/verify \
-H "Content-Type: application/json" \
-d '{
"bank": "cbe",
"reference": "FT26140P01YB",
"accountNumber": "1000560536171"
}'Response:
{
"success": true,
"verified": true,
"bank": "Commercial Bank of Ethiopia",
"reference": "FT26140P01YB",
"amount": 20000,
"currency": "ETB",
"senderName": "Mr Mohammed Abdulwasi Reshid",
"senderAccount": "1****1685",
"receiverName": "SAMI ADIL ZEKARIA",
"receiverAccount": "1000560536171",
"date": "5/20/2026 7:29:00 PM",
"sourceUrl": "https://apps.cbe.com.et:100/?id=FT26140P01YB60536171"
}URL-based verification
If you have a receipt URL (e.g. from a QR code or shared link), you can skip the bank field. cheki auto-detects the bank from the URL:
curl -X POST https://chekiapp.vercel.app/api/verify \
-H "Content-Type: application/json" \
-d '{
"reference": "https://mbreciept.cbe.com.et/fHCxyV4mg5pRIwEkJO"
}'Batch verification
For end-of-day reconciliation, verify up to 50 receipts in a single request:
curl -X POST https://chekiapp.vercel.app/api/verify/batch \
-H "Content-Type: application/json" \
-d '{
"receipts": [
{"bank": "cbe", "reference": "FT26140P01YB", "accountNumber": "1000560536171"},
{"bank": "telebirr", "reference": "DET8FJGUJ4"},
{"bank": "boa", "reference": "AB12345678", "accountNumber": "12345"}
]
}'JavaScript integration
class ChekiAPI {
constructor(baseUrl = 'https://chekiapp.vercel.app') {
this.baseUrl = baseUrl;
}
async verify(bank, reference, accountNumber) {
const res = await fetch(`${this.baseUrl}/api/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bank, reference, accountNumber })
});
return res.json();
}
async verifyUrl(url) {
const res = await fetch(`${this.baseUrl}/api/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reference: url })
});
return res.json();
}
async batchVerify(receipts) {
const res = await fetch(`${this.baseUrl}/api/verify/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ receipts })
});
return res.json();
}
}
// Usage
const cheki = new ChekiAPI();
const result = await cheki.verify('cbe', 'FT26140P01YB', '1000560536171');
if (result.verified && result.amount === expectedAmount) {
console.log('Payment confirmed:', result.amount, result.currency);
}Python integration
import requests
class ChekiAPI:
def __init__(self, base_url='https://chekiapp.vercel.app'):
self.base_url = base_url
def verify(self, bank, reference, account_number=None):
payload = {'bank': bank, 'reference': reference}
if account_number:
payload['accountNumber'] = account_number
r = requests.post(f'{self.base_url}/api/verify', json=payload)
return r.json()
def verify_url(self, url):
r = requests.post(f'{self.base_url}/api/verify',
json={'reference': url})
return r.json()
# Usage
cheki = ChekiAPI()
result = cheki.verify('cbe', 'FT26140P01YB', '1000560536171')
if result.get('verified') and result.get('amount') == expected_amount:
print(f'Payment confirmed: {result["amount"]} {result["currency"]}')Error handling
The API returns structured errors. Handle these cases:
| HTTP Status | Error | Action |
|---|---|---|
| 200 | success: true, verified: true | Payment confirmed |
| 200 | success: false, fallbackUrl present | Geo-blocked, redirect user to fallbackUrl |
| 404 | Receipt not found | Reference is invalid or fake |
| 400 | Missing fields | Check request body |
| 502 | Bank endpoint unreachable | Bank server is down, retry later |
Fraud prevention checklist
Before releasing goods or services, run these checks:
Verify existence
Check result.verified is true. If false, the receipt doesn't exist on the bank system.
Check amount
Compare result.amount with the expected payment amount. Reject if different.
Check receiver
Compare result.receiverAccount with your account number. Reject if different.
Check freshness
Parse result.date and reject if the payment is older than your acceptable window (e.g. 1 hour).
Check duplicates
Store result.reference in your database. Before accepting, check if it already exists.
async function verifyPayment(customerRef, expectedAmount, myAccount) {
const result = await cheki.verify('cbe', customerRef, myAccount);
if (!result.success || !result.verified)
return { ok: false, reason: 'Receipt not found' };
if (result.amount !== expectedAmount)
return { ok: false, reason: `Amount mismatch: ${result.amount} vs ${expectedAmount}` };
if (result.receiverAccount && !result.receiverAccount.endsWith(myAccount.slice(-5)))
return { ok: false, reason: 'Wrong receiving account' };
const paymentDate = new Date(result.date);
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
if (paymentDate < oneHourAgo)
return { ok: false, reason: 'Payment is too old' };
if (await isDuplicate(result.reference))
return { ok: false, reason: 'Duplicate receipt' };
return { ok: true, data: result };
}Self-host for production
For production use, self-host cheki with Docker. This gives you full control, eliminates dependency on chekiapp.vercel.app, and allows you to add authentication, rate limiting, and logging.