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; requests are rate limited per IP. We'll cover single verification, batch verification, error handling, and fraud prevention patterns.

API overview

EndpointMethodPurpose
/api/verifyPOSTVerify a single receipt
/api/verify/batchPOSTVerify up to 20 receipts at once
/api/banksGETList supported banks and status
/api/healthGETCheck API and per-bank endpoint latency

Base URL: https://cheki.et

✓

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.

bash
curl -X POST https://cheki.et/api/verify \
  -H "Content-Type: application/json" \
  -d '{
    "bank": "cbe",
    "reference": "FT26140P01YB",
    "accountNumber": "1000560536171"
  }'

Response:

json
{
  "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://mbreciept.cbe.com.et/fHCxExampleRcpt01"
}

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:

bash
curl -X POST https://cheki.et/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 20 receipts in a single request (5 are checked at a time, and each one counts against the per-IP verify limit):

bash
curl -X POST https://cheki.et/api/verify/batch \
  -H "Content-Type: application/json" \
  -d '{
    "receipts": [
      {"bank": "cbe", "reference": "https://mbreciept.cbe.com.et/fHCxExampleRcpt01"},
      {"bank": "telebirr", "reference": "DET8FJGUJ4"},
      {"bank": "boa", "reference": "AB12345678", "accountNumber": "12345"}
    ]
  }'

JavaScript integration

javascript
class ChekiAPI {
  constructor(baseUrl = 'https://cheki.et') {
    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

python
import requests

class ChekiAPI:
    def __init__(self, base_url='https://cheki.et'):
        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 StatusErrorAction
200success: true, verified: truePayment confirmed
200success: true, verified: false, decodedOnly: trueBOA QR decoded but not confirmed by the bank. Not proof of payment
400Missing fields or malformed referenceCheck request body
404Bank not supported (unknown id or still in development)Check the bank id against /api/banks
422Receipt not found, or the bank's response failed verificationTreat as unverified; ask the customer to check the reference
429Per-IP rate limit (Retry-After header)Wait Retry-After seconds, then retry once
502Bank unreachable or geo-blocked (fallbackUrl for geo-blocked banks)Retry later, or open fallbackUrl in a browser

Fraud prevention checklist

Before releasing goods or services, run these checks:

1

Check verified

Accept only result.verified === true. verified: false does not always mean the receipt is fake: it can mean the bank has no such receipt, the bank's answer failed cheki's verification gate (no amount, wrong reference, failed or pending status), a BOA QR code was only decoded (decodedOnly), or the content was only parsed from what you sent (parsed, /api/parse). None of these is proof of payment.

2

Check amount

Compare result.amount with the expected payment amount. Reject if different.

3

Check receiver

Compare result.receiverAccount with your account number. Reject if different.

4

Check freshness

Parse result.date and reject if the payment is older than your acceptable window (e.g. 1 hour).

5

Check duplicates

Store result.reference in your database. Before accepting, check if it already exists.

javascript
async function verifyPayment(customerRef, expectedAmount, myAccount) {
  const result = await cheki.verify('cbe', customerRef, myAccount);

  if (!result.success || !result.verified)
    return { ok: false, reason: result.error || 'Not verified by the bank' };

  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 cheki.et, and allows you to add authentication, rate limiting, and logging.

Frequently asked questions

Edit this article on GitHubReport an issue →

Continue reading