API Documentation
One POST request returns the full receipt.
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
https://cheki.etQuick Start
Verify a CBE receipt with one cURL call:
curl -X POST https://cheki.et/api/verify \
-H "Content-Type: application/json" \
-d '{"bank":"cbe","reference":"https://mbreciept.cbe.com.et/fHCxExampleRcpt01"}'Response
{
"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
/api/verifyVerify 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| bank | string | Yes (unless URL) | Bank id (the id field from GET /api/banks). Live: cbe, telebirr, boa, mpesa, dashen, awash, siinqee, ebirr |
| reference | string | Yes (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) |
| accountNumber | string | BOA; older CBE FT receipts | Last 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 |
| qrData | string | BOA or Telebirr QR | Raw 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. |
| phoneNumber | string | CBE Birr | Payer phone number, format: 2519XXXXXXXXX |
Request body (CBE, current format: receipt link or Receipt ID)
{
"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.
{
"bank": "cbe",
"reference": "FT26140P01YB",
"accountNumber": "1000560536171"
}Request body (BOA QR code)
{
"bank": "boa",
"qrData": "3cHRaxVjn/pySp..."
}Response (200)
{
"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
| Parameter | Type | Required | Description |
|---|---|---|---|
| success | boolean | Whether the request succeeded (not whether the receipt is real) | |
| verified | boolean | Whether the receipt was found and matches the bank's records. The only field that means the payment is confirmed. | |
| decodedOnly | boolean | BOA QR only: decoded from the QR but the bank could not be reached. Always paired with verified: false. Not proof of payment. | |
| parsed | boolean | /api/parse only: read from content you supplied, never checked with the bank. Always paired with verified: false. | |
| bank | string | Full bank name | |
| bankCode | string | Bank id (e.g. cbe) | |
| cached | boolean | Served from cheki's receipt cache (verified receipts only). durationMs is absent when true. | |
| reference | string | Transaction reference number | |
| senderName | string | Name of the account that sent the payment | |
| senderAccount | string | Masked sender account number | |
| receiverName | string | Name of the account that received the payment | |
| receiverAccount | string | Masked receiver account number | |
| amount | number | Transfer amount in ETB | |
| currency | string | Currency code (always ETB for domestic) | |
| date | string | Transaction date and time | |
| sourceUrl | string | The bank endpoint URL cheki fetched the data from | |
| durationMs | number | Time spent fetching from the bank endpoint, in milliseconds |
POST /api/verify/batch
/api/verify/batchVerify 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| receipts | array | Yes | Array of receipt objects (same fields as verify). Max 20. |
{
"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.
{
"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
/api/banksList 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)
{
"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
/api/healthCheck 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)
{
"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
/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].statusError Codes
{
"success": false,
"error": "[Commercial Bank of Ethiopia] Receipt not found. Check the reference number."
}| Status | Name | Description |
|---|---|---|
| 400 | Bad Request | Missing required fields or malformed reference number |
| 404 | Not Found | Bank not supported: unknown id, or still in development |
| 422 | Unprocessable | Receipt not found at the bank, or the bank's response failed verification (no amount, wrong reference, failed or pending status) |
| 413 | Payload Too Large | Request body is over the limit (16 KB verify, 64 KB batch, 256 KB parse, 10 MB ocr) |
| 429 | Too Many Requests | Per-IP rate limit reached. Wait the number of seconds in the Retry-After header, then retry. |
| 502 | Bad Gateway | Bank endpoint is unreachable, geo-blocked, or timed out. Geo-blocked banks include a fallbackUrl to open in a browser. |
| 500 | Server Error | Internal 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
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