API Documentation

One POST request returns the full receipt.

No API keyNo signupMIT licensed

Overview

The cheki API verifies Ethiopian bank and mobile money receipts by fetching public bank endpoints. No authentication is required. No API key. Requests are rate limited per IP: verify 30 per minute, batch 5 per minute, parse and ocr 20 per minute each, counted per server instance. Over the limit you get a 429 with a Retry-After header. Just POST to /api/verify with a bank code and reference number.

Base URL

url
https://cheki.et

Quick Start

Verify a CBE receipt with one cURL call:

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

Response

json
{
  "success": true,
  "verified": true,
  "bank": "Commercial Bank of Ethiopia",
  "reference": "FT26140P01YB",
  "amount": 20000,
  "currency": "ETB",
  "senderName": "Mr Mohammed Abdulwasi Reshid",
  "receiverName": "SAMI ADIL ZEKARIA",
  "date": "5/20/2026 7:29:00 PM",
  "sourceUrl": "https://mbreciept.cbe.com.et/fHCxExampleRcpt01"
}

POST /api/verify

POST/api/verify

Verify a single receipt. Send the bank code and the receipt link or reference in the bank's current format (a CBE mbreciept link needs no account number). BOA, and older CBE receipts with only an FT reference, also need the account number. For BOA inter-bank transfers, send the QR payload in qrData instead. A Telebirr receipt QR also goes in qrData: cheki reads the transaction number out of it and checks that number with Telebirr.

Parameters

ParameterTypeRequiredDescription
bankstringYes (unless URL)Bank id (the id field from GET /api/banks). Live: cbe, telebirr, boa, mpesa, dashen, awash, siinqee, ebirr
referencestringYes (unless qrData)Receipt link, Receipt ID or transaction reference, in the bank's current format (e.g. a CBE mbreciept.cbe.com.et link, a Dashen FT Ref, a Telebirr transaction number)
accountNumberstringBOA; older CBE FT receiptsLast 5 digits for BOA. Last 8 digits only for an older CBE receipt that shows just an FT reference (legacy format); a CBE Receipt ID or link needs none
qrDatastringBOA or Telebirr QRRaw QR payload: a BOA inter-bank receipt QR (bank "boa"), or the Payment QR Code on a Telebirr receipt (bank "telebirr" or empty), whose transaction number is checked exactly like a typed one. A QR alone is never proof of payment.
phoneNumberstringCBE BirrPayer phone number, format: 2519XXXXXXXXX

Request body (CBE, current format: receipt link or Receipt ID)

json
{
  "bank": "cbe",
  "reference": "https://mbreciept.cbe.com.et/fHCxExampleRcpt01"
}
Older receipt? (legacy formats)

A CBE receipt that shows only an FT reference uses the legacy format: send the FT reference and the receiving account (cheki uses the last 8 digits). CBE's older receipt host is often unreachable, so prefer the receipt link when you have it. Old Dashen Super App links still work: cheki reads the FT Ref from them. Each bank's formats, current first, are in GET /api/banks and on the endpoint reference.

json
{
  "bank": "cbe",
  "reference": "FT26140P01YB",
  "accountNumber": "1000560536171"
}

Request body (BOA QR code)

json
{
  "bank": "boa",
  "qrData": "3cHRaxVjn/pySp..."
}

Response (200)

json
{
  "success": true,
  "verified": true,
  "bank": "Commercial Bank of Ethiopia",
  "reference": "FT26140P01YB",
  "senderName": "Mr Mohammed Abdulwasi Reshid",
  "senderAccount": "1****1685",
  "receiverName": "SAMI ADIL ZEKARIA",
  "receiverAccount": "1****6171",
  "amount": 20000,
  "currency": "ETB",
  "date": "5/20/2026 7:29:00 PM",
  "durationMs": 1240,
  "sourceUrl": "https://mbreciept.cbe.com.et/fHCxExampleRcpt01"
}
Response fields
ParameterTypeRequiredDescription
successbooleanWhether the request succeeded (not whether the receipt is real)
verifiedbooleanWhether the receipt was found and matches the bank's records. The only field that means the payment is confirmed.
decodedOnlybooleanBOA QR only: decoded from the QR but the bank could not be reached. Always paired with verified: false. Not proof of payment.
parsedboolean/api/parse only: read from content you supplied, never checked with the bank. Always paired with verified: false.
bankstringFull bank name
bankCodestringBank id (e.g. cbe)
cachedbooleanServed from cheki's receipt cache (verified receipts only). durationMs is absent when true.
referencestringTransaction reference number
senderNamestringName of the account that sent the payment
senderAccountstringMasked sender account number
receiverNamestringName of the account that received the payment
receiverAccountstringMasked receiver account number
amountnumberTransfer amount in ETB
currencystringCurrency code (always ETB for domestic)
datestringTransaction date and time
sourceUrlstringThe bank endpoint URL cheki fetched the data from
durationMsnumberTime spent fetching from the bank endpoint, in milliseconds

POST /api/verify/batch

POST/api/verify/batch

Verify up to 20 receipts per call (5 at a time). Send an array of receipt objects with the same fields as the single verify endpoint.

Parameters

ParameterTypeRequiredDescription
receiptsarrayYesArray of receipt objects (same fields as verify). Max 20.
json
{
  "receipts": [
    {
      "bank": "cbe",
      "reference": "https://mbreciept.cbe.com.et/fHCxExampleRcpt01"
    },
    {
      "bank": "telebirr",
      "reference": "DET8FJGUJ4"
    }
  ]
}

Response (200). verified counts only verified: true items; failed is everything else, including decoded-only QR results.

json
{
  "success": true,
  "results": [
    {
      "success": true,
      "verified": true,
      "bank": "Commercial Bank of Ethiopia",
      "bankCode": "cbe",
      "reference": "FT26140P01YB",
      "amount": 20000,
      "currency": "ETB",
      "sourceUrl": "https://mbreciept.cbe.com.et/fHCxExampleRcpt01",
      "durationMs": 7782,
      "cached": false
    },
    {
      "success": false,
      "error": "[Telebirr] Receipt not found. Check the reference number.",
      "status": 422
    }
  ],
  "cached": 0,
  "total": 2,
  "verified": 1,
  "failed": 1
}

GET /api/banks

GET/api/banks

List all supported banks with metadata: id (the value to send as bank), name, type (bank, mobile or wallet), status, endpoint, and whether an account (accountDigits) or phone (requiresPhone) is needed. requiresPhone, swift and accountDigits are omitted when not set.

Response (200)

json
{
  "banks": [
    {
      "id": "cbe",
      "name": "Commercial Bank of Ethiopia",
      "swift": "CBETETAA",
      "type": "bank",
      "status": "live",
      "requiresAccount": false,
      "accountDigits": 8,
      "endpoint": "https://mb.cbe.com.et/api/v1/transactions/public/transaction-detail/{ref}",
      "color": "#502878",
      "initials": "CBE",
      "formats": [
        {
          "id": "cbe-receipt-id",
          "status": "current",
          "label": "Receipt ID or mbreciept.cbe.com.et link",
          "inputs": [
            "receiptId"
          ],
          "responseType": "json",
          "supported": true,
          "confidence": "verified"
        },
        {
          "id": "cbe-ft-pdf",
          "status": "legacy",
          "label": "FT reference + last 8 digits of the receiving account",
          "inputs": [
            "reference",
            "account"
          ],
          "accountDigits": 8,
          "responseType": "pdf",
          "supported": true,
          "confidence": "inferred"
        }
      ]
    },
    {
      "id": "telebirr",
      "name": "Telebirr",
      "type": "mobile",
      "status": "live",
      "requiresAccount": false,
      "endpoint": "https://transactioninfo.ethiotelecom.et/receipt/{ref}",
      "color": "#00A0DC",
      "initials": "TB"
    }
  ]
}

GET /api/health

GET/api/health

Check if each bank endpoint is reachable: status (reachable, unreachable, geo-blocked, no-parser, in-development) and latency per bank. Results are cached for up to 5 minutes (cached: true), so this never hammers the banks.

Response (200)

json
{
  "status": "ok",
  "checkedAt": "2026-10-11T09:00:00.000Z",
  "banks": [
    {
      "id": "cbe",
      "name": "Commercial Bank of Ethiopia",
      "status": "reachable",
      "latencyMs": 340
    },
    {
      "id": "telebirr",
      "name": "Telebirr",
      "status": "geo-blocked",
      "latencyMs": 5004
    },
    {
      "id": "nib",
      "name": "Nib International Bank",
      "status": "in-development",
      "latencyMs": 0
    }
  ],
  "cached": false
}

GET /api/receipt

GET/api/receipt?bank=cbe&reference={Receipt ID}

Open the bank's own copy of a receipt. Supported: cbe, dashen, zemen. CBE Receipt ID and Dashen FT Ref (current formats, HTML at the bank): a 302 redirect to the bank's own receipt page, so bank HTML is never served from cheki. Older CBE FT reference (legacy, add account with at least the last 8 digits) and Zemen: the bank's PDF, returned only if it is a PDF (application/pdf, attachment). Reference and account are validated like /api/verify. Errors are JSON with 400, 404 or 502, and the verify rate limit (429) applies.

SDKs

SDKs are available in 5 languages. All wrap the same REST API and share one contract (sdks/fixtures/api-contract.json): bank id, the verified, decodedOnly and parsed fields, batch counts, typed 429 (with Retry-After) and 413 errors, and a client-side limit of 20 receipts per batch. Each attempt waits up to 65 seconds (the server may take up to 60). They never retry a 4xx or a timeout, and retry at most once on a network error, 5xx or 429 (after Retry-After). The server makes at most 2 attempts per bank fetch, so one SDK call reaches a bank at most 4 times.

npm install cheki-verify

import { Cheki, isVerified, ChekiAPIError, ChekiRateLimitError } from "cheki-verify";

const cheki = new Cheki(); // 65 s timeout, at most 1 retry

try {
  const result = await cheki.verify("cbe", "https://mbreciept.cbe.com.et/fHCxExampleRcpt01");
  // Older receipt with only an FT reference (legacy format):
  // await cheki.verify("cbe", "FT26140P01YB", { accountNumber: "1000560536171" });
  // verified is the only success signal (a decoded-only BOA QR is success: true, verified: false)
  if (isVerified(result)) console.log(result.amount, result.senderName);
} catch (err) {
  if (err instanceof ChekiRateLimitError) console.log("retry in", err.retryAfterSeconds, "s");
  else if (err instanceof ChekiAPIError) console.log(err.statusCode, err.serverMessage, err.fallbackUrl);
}

// Max 20 receipts per batch (checked before sending)
const batch = await cheki.verifyBatch([
  { bank: "cbe", reference: "https://mbreciept.cbe.com.et/fHCxExampleRcpt01" },
  { bank: "telebirr", reference: "DET8FJGUJ4" },
]);
console.log(batch.verified, batch.failed, batch.total);

const { banks } = await cheki.getBanks();   // banks[i].id
const health = await cheki.getHealth();     // health.banks[i].status

Error Codes

json
{
  "success": false,
  "error": "[Commercial Bank of Ethiopia] Receipt not found. Check the reference number."
}
StatusNameDescription
400Bad RequestMissing required fields or malformed reference number
404Not FoundBank not supported: unknown id, or still in development
422UnprocessableReceipt not found at the bank, or the bank's response failed verification (no amount, wrong reference, failed or pending status)
413Payload Too LargeRequest body is over the limit (16 KB verify, 64 KB batch, 256 KB parse, 10 MB ocr)
429Too Many RequestsPer-IP rate limit reached. Wait the number of seconds in the Retry-After header, then retry.
502Bad GatewayBank endpoint is unreachable, geo-blocked, or timed out. Geo-blocked banks include a fallbackUrl to open in a browser.
500Server ErrorInternal server error. Check /api/health for endpoint status.

Bank endpoints

The endpoint reference lists every bank receipt URL cheki reads, with placeholders, unknown-reference error shapes and a dated changelog. It is generated from the manifest. Daily health checks are on the status page.

Self-hosting with Docker

bash
git clone https://github.com/1RB/cheki.git
cd cheki
docker-compose up -d

# API at http://localhost:3000/api/verify
# Self-hosting on an Ethiopian IP bypasses geo-blocks