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

EndpointMethodPurpose
/api/verifyPOSTVerify a single receipt
/api/verify/batchPOSTVerify up to 50 receipts at once
/api/banksGETList supported banks and status
/api/healthGETCheck 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.

bash
curl -X POST https://chekiapp.vercel.app/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://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:

bash
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:

bash
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

javascript
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

python
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 StatusErrorAction
200success: true, verified: truePayment confirmed
200success: false, fallbackUrl presentGeo-blocked, redirect user to fallbackUrl
404Receipt not foundReference is invalid or fake
400Missing fieldsCheck request body
502Bank endpoint unreachableBank server is down, retry later

Fraud prevention checklist

Before releasing goods or services, run these checks:

1

Verify existence

Check result.verified is true. If false, the receipt doesn't exist on the bank system.

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: '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.

Frequently asked questions

Edit this article on GitHubReport an issue โ†’

Continue reading