<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: cheki</title>
    <description>The latest articles on DEV Community by cheki (@cheki).</description>
    <link>https://dev.to/cheki</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4197373%2Fa46ef48f-9a08-4085-a5dc-30f0925133bf.png</url>
      <title>DEV Community: cheki</title>
      <link>https://dev.to/cheki</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cheki"/>
    <language>en</language>
    <item>
      <title>How to Check a Bank Receipt Online in Ethiopia (CBE, Telebirr, BOA, Dashen, Awash and More)</title>
      <dc:creator>cheki</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:15:29 +0000</pubDate>
      <link>https://dev.to/cheki/how-to-check-a-bank-receipt-online-in-ethiopia-cbe-telebirr-boa-dashen-awash-and-more-2pk2</link>
      <guid>https://dev.to/cheki/how-to-check-a-bank-receipt-online-in-ethiopia-cbe-telebirr-boa-dashen-awash-and-more-2pk2</guid>
      <description>&lt;p&gt;"I've sent it." Then a screenshot arrives.&lt;/p&gt;

&lt;p&gt;To know the money is real, you have to ask the bank. The good news: every Ethiopian bank and wallet publishes each receipt on a public web page, with no login needed. The annoying part: every bank wants something slightly different to find it.&lt;/p&gt;

&lt;p&gt;This guide is the cheat sheet. For each bank: what to ask the sender for, and the one mistake that trips people up. (All examples are made up.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for the QR code or the receipt link, not a screenshot and not just a number.&lt;/strong&gt; The QR and the link carry everything the bank needs. A bare reference number often isn't enough, because several banks also want the last digits of an account, and that's where most failed checks come from.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bank&lt;/th&gt;
&lt;th&gt;Best thing to ask for&lt;/th&gt;
&lt;th&gt;If you only have a number&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CBE&lt;/td&gt;
&lt;td&gt;Receipt link or QR&lt;/td&gt;
&lt;td&gt;FT reference &lt;strong&gt;plus&lt;/strong&gt; last 8 digits of the receiving account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telebirr&lt;/td&gt;
&lt;td&gt;SMS link or QR&lt;/td&gt;
&lt;td&gt;10-character transaction number works alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bank of Abyssinia&lt;/td&gt;
&lt;td&gt;Slip link or QR&lt;/td&gt;
&lt;td&gt;Reference &lt;strong&gt;plus&lt;/strong&gt; last 5 digits of the account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashen&lt;/td&gt;
&lt;td&gt;FT Ref&lt;/td&gt;
&lt;td&gt;The 16-character FT Ref works alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Awash&lt;/td&gt;
&lt;td&gt;The full share link&lt;/td&gt;
&lt;td&gt;Can't be rebuilt from a number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CBE Birr&lt;/td&gt;
&lt;td&gt;Receipt link or QR&lt;/td&gt;
&lt;td&gt;Receipt number &lt;strong&gt;plus&lt;/strong&gt; the payer's phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M-Pesa&lt;/td&gt;
&lt;td&gt;Transaction number&lt;/td&gt;
&lt;td&gt;Works alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eBirr / Siinqee&lt;/td&gt;
&lt;td&gt;The full link&lt;/td&gt;
&lt;td&gt;Can't be rebuilt from a number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZamZam&lt;/td&gt;
&lt;td&gt;The receipt link&lt;/td&gt;
&lt;td&gt;The number at the end of the link, not the printed reference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then, whatever the bank, check three things: &lt;strong&gt;the amount, the receiver, and the date.&lt;/strong&gt; A real receipt to someone else's account is still not your money. And keep a list of references you've accepted, because a real receipt shown twice passes every other check.&lt;/p&gt;

&lt;h2&gt;
  
  
  CBE (Commercial Bank of Ethiopia)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for:&lt;/strong&gt; the receipt link from the CBE app, or its QR.&lt;/p&gt;

&lt;p&gt;New receipts come as a link like &lt;code&gt;https://mbreciept.cbe.com.et/fHCxExampleRcpt01&lt;/code&gt;. That ending is the Receipt ID, and it's all CBE needs. (Yes, CBE spells it "reciept".)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; older receipts only show an FT reference, like &lt;code&gt;FT26SAMPLE01&lt;/code&gt;. Those need the last 8 digits of the receiving account too, and they go through CBE's older receipt service, which often doesn't answer. If an FT check stalls, ask for the link from the app instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telebirr
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for:&lt;/strong&gt; the link in the SMS, the QR, or the transaction number.&lt;/p&gt;

&lt;p&gt;The number is 10 letters and digits, starting with a short prefix like DET, CHQ, DAB, DEL or ADQ (for example &lt;code&gt;DET0SAMPLE&lt;/code&gt;). The receipt lives at &lt;code&gt;https://transactioninfo.ethiotelecom.et/receipt/&lt;/code&gt; plus that number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; Telebirr's page only opens on Ethiopian networks. On your phone with Ethiopian data it's fine. Abroad or on a VPN it won't load. That's why cheki.et, hosted outside Ethiopia, hands Telebirr checks to your phone's browser with a one-tap button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bank of Abyssinia (BOA)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for:&lt;/strong&gt; the slip link or the QR.&lt;/p&gt;

&lt;p&gt;A BOA link looks like &lt;code&gt;https://cs.bankofabyssinia.com/slip/?trx=FT26SAMPLE0712345&lt;/code&gt;: the reference (&lt;code&gt;FT26SAMPLE07&lt;/code&gt;) with five account digits (&lt;code&gt;12345&lt;/code&gt;) glued on the end. BOA needs both, which is why the link or QR beats typing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; if the receiving account's digits aren't found, try the last 5 digits of the account the money came &lt;em&gt;from&lt;/em&gt;. BOA files some receipts that way. Transfers from BOA to other banks carry an encrypted QR. cheki.et decodes it and then asks BOA to confirm. If BOA can't be reached, the result says "decoded only", which is not verified. QRs can be copied, so don't hand over goods on that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashen Bank
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for:&lt;/strong&gt; the &lt;strong&gt;FT Ref&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's 16 characters, like &lt;code&gt;B22WDTI00SAMPLE1&lt;/code&gt;. It works on its own, with no account number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; Dashen receipts also show a longer "Transaction Ref". That one won't find anything. If a Dashen check says "not found", this is almost always why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Awash Bank
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for:&lt;/strong&gt; the whole link from the Awash SMS or app, copied rather than retyped.&lt;/p&gt;

&lt;p&gt;It looks like &lt;code&gt;https://awashpay.awashbank.com:8225/-2SAMPLE0X1-4U0000&lt;/code&gt;. Both parts after the dashes matter, and you can't rebuild the link from the transaction number alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; one mistyped character and the check fails. Copy and paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  CBE Birr
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for:&lt;/strong&gt; the receipt link or QR. Failing that, the receipt number (TID) &lt;strong&gt;and&lt;/strong&gt; the payer's phone number.&lt;/p&gt;

&lt;p&gt;CBE Birr is CBE's mobile wallet, separate from the CBE banking app. The TID is 10 or 11 characters, like &lt;code&gt;DTS41AB2C90&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; it needs the &lt;em&gt;payer's&lt;/em&gt; phone, not yours. An empty result usually means the number and phone don't match. On cheki.et this bank is in beta: confirmed on real wallet and bank transfers, not yet on merchant, airtime or bill payments.&lt;/p&gt;

&lt;h2&gt;
  
  
  M-Pesa Ethiopia
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for:&lt;/strong&gt; the transaction number from the M-Pesa SMS (usually two letters and six or more digits, like &lt;code&gt;SE12345678&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; like Telebirr, it only answers on Ethiopian networks. It's in beta on cheki.et, as it hasn't been confirmed on a real receipt yet. If you have one that fails, a report helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  eBirr and Siinqee Bank
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for:&lt;/strong&gt; the full receipt link, like &lt;code&gt;receipt.ebirr.com/nib/abc123def&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first part after the slash names the institution. Siinqee uses the same platform under &lt;code&gt;receipt.ebirr.com/siinqee/…&lt;/code&gt;. Support for other eBirr partner banks (Nib, Wegagen, Ahadu, KAAFI) is still in development on cheki.&lt;/p&gt;

&lt;h2&gt;
  
  
  ZamZam Bank
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ask for:&lt;/strong&gt; the receipt link, like &lt;code&gt;https://receipt.zamzambank.com.et/receipt/12345678&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; the 8-digit number at the end of the link is what finds the receipt, &lt;em&gt;not&lt;/em&gt; the reference printed on it. That mix-up causes most failed ZamZam checks. Beta on cheki.et.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not on the list yet?
&lt;/h2&gt;

&lt;p&gt;cheki.et has 10 banks and wallets live today. Zemen, Nib, Wegagen, Ahadu, KAAFI and others are on the &lt;a href="https://cheki.et/banks" rel="noopener noreferrer"&gt;banks page&lt;/a&gt; as in development. Each goes live once a real receipt confirms it. Until then, use the bank's own receipt page or check your balance. For every bank's receipt address in one place, see &lt;a href="https://cheki.et/guides/ethiopian-bank-receipt-formats" rel="noopener noreferrer"&gt;Ethiopian bank receipt formats&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Not found" doesn't always mean fake
&lt;/h2&gt;

&lt;p&gt;Most failed checks are real payments checked with the wrong piece of information. Before you call it fake, run through this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bank&lt;/th&gt;
&lt;th&gt;Usual mix-up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CBE&lt;/td&gt;
&lt;td&gt;FT reference without the account digits. Ask for the link.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telebirr&lt;/td&gt;
&lt;td&gt;A tight crop cut off the last character. It should be 10.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BOA&lt;/td&gt;
&lt;td&gt;Wrong 5 digits. Try the sender's account.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashen&lt;/td&gt;
&lt;td&gt;The long Transaction Ref instead of the 16-character FT Ref.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Awash&lt;/td&gt;
&lt;td&gt;A retyped link with a typo.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CBE Birr&lt;/td&gt;
&lt;td&gt;Your phone instead of the payer's.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZamZam&lt;/td&gt;
&lt;td&gt;The printed reference instead of the link's number.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Still not found with the right information? Treat it as unpaid until the money shows up in your account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skip the cheat sheet
&lt;/h2&gt;

&lt;p&gt;Remembering ten banks' quirks is a lot. A checker does it for you. On &lt;a href="https://cheki.et" rel="noopener noreferrer"&gt;cheki.et&lt;/a&gt;, scan the receipt's QR with your phone camera or paste the link, and it works out the bank, asks it, and shows what the bank has on record. It only shows green when the bank itself confirmed. A decoded QR or a bank that didn't answer never counts.&lt;/p&gt;

&lt;p&gt;Other checkers, like check.et, verify.et and Veritas, read the same public pages, so the tips above apply whichever you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cheki is free, and why that matters
&lt;/h2&gt;

&lt;p&gt;The banks publish every receipt for free, yet checking them is often sold back to businesses as monthly plans and per-check fees. cheki started from a simple idea: &lt;strong&gt;you shouldn't pay to read a page your own bank already made public.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So it's open source under the MIT license, with no signup, no key and no database. The website, the API, every bank's parser and SDKs for five languages live in one public repo at &lt;a href="https://github.com/1RB/cheki" rel="noopener noreferrer"&gt;github.com/1RB/cheki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That openness keeps it working. Banks change their receipt pages without warning, and the people who notice first are the shops and developers using them. With the code public, whoever spots a break can fix it, and anyone can add a missing bank with the &lt;a href="https://cheki.et/guides/contribute-new-bank" rel="noopener noreferrer"&gt;add-a-bank guide&lt;/a&gt;. Got a real receipt that fails? &lt;a href="https://github.com/1RB/cheki/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; with personal details removed.&lt;/p&gt;

&lt;p&gt;cheki isn't affiliated with any bank or Ethio Telecom.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>ethiopia</category>
      <category>tutorial</category>
      <category>payments</category>
    </item>
    <item>
      <title>How to Tell if a CBE or Telebirr Payment Screenshot Is Fake</title>
      <dc:creator>cheki</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:08:43 +0000</pubDate>
      <link>https://dev.to/cheki/how-to-tell-if-a-cbe-or-telebirr-payment-screenshot-is-fake-1nii</link>
      <guid>https://dev.to/cheki/how-to-tell-if-a-cbe-or-telebirr-payment-screenshot-is-fake-1nii</guid>
      <description>&lt;p&gt;A customer holds up their phone. Green tick, "Transfer successful", 5,000 birr. There's a queue behind them and they're in a hurry.&lt;/p&gt;

&lt;p&gt;Do you hand over the goods?&lt;/p&gt;

&lt;p&gt;Here's the rule to remember: &lt;strong&gt;a screenshot is a claim. The bank's record is the proof.&lt;/strong&gt; Everything below is about getting to that proof in under a minute, while the customer waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fakes are easy to make
&lt;/h2&gt;

&lt;p&gt;Type "fake cbe" into Google and autocomplete suggests "fake cbe screenshot template". Type "fake telebirr" and you get "fake telebirr app for android". Whole tools exist to draw a convincing success screen without any money moving.&lt;/p&gt;

&lt;p&gt;Most fake payments are one of five kinds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Edited.&lt;/strong&gt; A real 500 birr receipt becomes 5,000, or last week's date becomes today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reused.&lt;/strong&gt; A real receipt, already used once, comes back for a new order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Misdirected.&lt;/strong&gt; The money really moved, just not to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invented.&lt;/strong&gt; A fake app draws a receipt for a payment that never happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fake SMS.&lt;/strong&gt; A message that looks like it's from the bank or 127, with a made-up number.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Staring harder at the picture only catches the sloppy ones. Three habits catch all five.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three habits: scan it, match it, log it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Scan it
&lt;/h3&gt;

&lt;p&gt;Don't judge the picture. Ask the bank.&lt;/p&gt;

&lt;p&gt;Every Ethiopian bank and wallet publishes each receipt on its own public web page. You just need the receipt's &lt;strong&gt;QR code&lt;/strong&gt; or its &lt;strong&gt;link&lt;/strong&gt;. Both carry everything the bank needs to find it.&lt;/p&gt;

&lt;p&gt;A bare transaction number often isn't enough. For some banks, like Bank of Abyssinia and older CBE receipts, the bank also wants the last few digits of an account number, and a customer at the counter rarely has those handy. So ask for the QR or the link first.&lt;/p&gt;

&lt;p&gt;The quickest way: open &lt;a href="https://cheki.et" rel="noopener noreferrer"&gt;cheki.et&lt;/a&gt; on your phone and scan the receipt's QR with the camera. It asks the bank and shows you the amount, sender, receiver and date the bank has on record. &lt;strong&gt;Green means the bank confirmed it.&lt;/strong&gt; Anything else, whether amber or an error, means not yet.&lt;/p&gt;

&lt;p&gt;Prefer to do it by hand? That works too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CBE:&lt;/strong&gt; the app shares a link starting with &lt;code&gt;https://mbreciept.cbe.com.et/&lt;/code&gt;. (Yes, the bank spells it "reciept".)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telebirr:&lt;/strong&gt; the SMS links to &lt;code&gt;https://transactioninfo.ethiotelecom.et/receipt/&lt;/code&gt; followed by a 10-character number like &lt;code&gt;DET0SAMPLE&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two cautions. Check the web address is exactly the bank's, not a lookalike. And Telebirr's page only opens on Ethiopian networks, so use mobile data or local Wi-Fi with any VPN off. (That's also why cheki.et, hosted abroad, gives you a one-tap button to Telebirr's own page instead of checking it on its server.)&lt;/p&gt;

&lt;p&gt;Or skip all of it: if the money came to you, open your own app or dial &lt;code&gt;*127#&lt;/code&gt; (Telebirr) or &lt;code&gt;*889#&lt;/code&gt; (CBE). If it's in your balance, it's real. That just gets hard at a busy counter, with five payments of 300 birr in ten minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Match it
&lt;/h3&gt;

&lt;p&gt;A real receipt only proves &lt;em&gt;a&lt;/em&gt; payment happened. Make sure it's &lt;em&gt;your&lt;/em&gt; payment. Check three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amount:&lt;/strong&gt; the full amount, not "close enough".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Receiver:&lt;/strong&gt; your name and the last digits of your account or phone. This catches kind 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date:&lt;/strong&gt; today, around when they say they paid.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Log it
&lt;/h3&gt;

&lt;p&gt;The hardest fake is a real receipt shown twice, because every other check passes. The fix is boring and it works: write down every reference you accept, in a notebook, a spreadsheet or your POS. Seen it before? Say no.&lt;/p&gt;

&lt;p&gt;This matters most when several people work the counter. The same receipt can visit one cashier in the morning and another after lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warning signs
&lt;/h2&gt;

&lt;p&gt;None of these prove a fake, but each is a reason to slow down and scan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The crop cuts off the transaction number or the receiver's name.&lt;/li&gt;
&lt;li&gt;They'll send a picture but not the link or QR.&lt;/li&gt;
&lt;li&gt;The amount's font, spacing or alignment looks slightly off.&lt;/li&gt;
&lt;li&gt;The phone's clock doesn't fit the time on the receipt.&lt;/li&gt;
&lt;li&gt;It's a photo of another screen, or forwarded many times.&lt;/li&gt;
&lt;li&gt;"It's processing, I'm late, just let me go." Pressure is a tactic. A real payment checks in seconds.&lt;/li&gt;
&lt;li&gt;An SMS "from 127". Sender names can be faked. Treat it as a pointer, not proof.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more: a QR on its own proves nothing either. Anyone can make a QR that holds a transaction number. Its value is that it gets you to the bank's answer faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  If the check fails
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The bank has no record:&lt;/strong&gt; don't hand anything over. Ask for the link again. A tight crop often drops the last character of a Telebirr number. Still nothing? Treat it as unpaid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bank isn't answering:&lt;/strong&gt; banks go down sometimes. Wait, retry, or check your balance. A slow check is never a reason to accept the picture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The receiver isn't you:&lt;/strong&gt; then it isn't your payment, however real it is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a longer list of tricks, with examples, in the guide to &lt;a href="https://cheki.et/guides/payment-fraud-ethiopia" rel="noopener noreferrer"&gt;payment fraud in Ethiopia&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sticky note
&lt;/h2&gt;

&lt;p&gt;Print this for the counter:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scan it. Match it. Log it.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scan the QR or open the link, never just the screenshot.&lt;/li&gt;
&lt;li&gt;Amount, receiver, date: all three must match.&lt;/li&gt;
&lt;li&gt;Write the reference down. Seen it before? No.&lt;/li&gt;
&lt;li&gt;No answer from the bank? No goods yet.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Which checker?
&lt;/h2&gt;

&lt;p&gt;Any tool that asks the bank works. cheki.et, check.et, verify.et, Veritas and others all read the same public bank pages, and the bank's own page is always free. Pick one that shows you the bank's source link, so you can open it yourself, and that never shows green unless the bank confirmed.&lt;/p&gt;

&lt;p&gt;On privacy, cheki.et has no accounts and no database. A confirmed receipt sits in server memory for up to six hours so a repeat check doesn't hit the bank again. Failed checks aren't kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cheki is free, and why that matters
&lt;/h2&gt;

&lt;p&gt;A small shop can lose a week's profit to one fake screenshot. The banks already publish every receipt for free, yet checking them is often sold back to businesses as monthly plans and per-check fees. cheki started from a simple idea: &lt;strong&gt;you shouldn't pay to read a page your own bank already made public.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So it's open source under the MIT license, with no signup and no key. The website, the API, every bank's parser and SDKs for five languages live in one public repo at &lt;a href="https://github.com/1RB/cheki" rel="noopener noreferrer"&gt;github.com/1RB/cheki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That openness is practical, not just principled. Banks change their receipt pages without warning, and the first people to notice are the shops and developers using them. With the code public, whoever spots a break can fix it, and anyone can add a missing bank by following the &lt;a href="https://cheki.et/guides/contribute-new-bank" rel="noopener noreferrer"&gt;add-a-bank guide&lt;/a&gt;. If a real receipt from your bank fails, &lt;a href="https://github.com/1RB/cheki/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; with the personal details removed.&lt;/p&gt;

&lt;p&gt;cheki isn't affiliated with any bank or Ethio Telecom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A screenshot is a claim. The bank's record is the proof.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>fintech</category>
      <category>ethiopia</category>
      <category>payments</category>
    </item>
    <item>
      <title>Verify CBE and Telebirr payments in your app with a free API</title>
      <dc:creator>cheki</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:08:07 +0000</pubDate>
      <link>https://dev.to/cheki/verify-cbe-and-telebirr-payments-in-your-app-with-a-free-api-phm</link>
      <guid>https://dev.to/cheki/verify-cbe-and-telebirr-payments-in-your-app-with-a-free-api-phm</guid>
      <description>&lt;p&gt;If you sell online in Ethiopia, you know the flow. The customer pays by bank transfer or Telebirr, sends a screenshot, and someone on your team squints at it and decides whether to ship.&lt;/p&gt;

&lt;p&gt;That decision is a guess. Screenshots are easy to edit and easy to reuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; you can check the payment against the bank's own record with one free API call. No API key, no signup, no contract with the bank. Here's how, in Node or Python, plus the two checks most people forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, which "Telebirr API" do you mean?
&lt;/h2&gt;

&lt;p&gt;Search "telebirr api" or "cbe api" and you'll mostly find &lt;strong&gt;payment collection&lt;/strong&gt;: APIs where you start a payment and wait for a callback. Those need a merchant agreement with the bank.&lt;/p&gt;

&lt;p&gt;This post is about the other half: &lt;strong&gt;payment verification.&lt;/strong&gt; The customer already paid the normal way, and you want to confirm the receipt they sent. That needs no agreement with anyone, because Ethiopian banks and wallets publish every receipt at a public URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Every bank publishes receipts differently: an HTML page for Telebirr, a link for CBE, a JSON endpoint for Bank of Abyssinia, a PDF for CBE Birr. &lt;a href="https://cheki.et" rel="noopener noreferrer"&gt;cheki.et&lt;/a&gt;, a free and open-source checker, fetches the right one, reads it, sanity-checks it (positive amount, matching reference, not failed or pending) and gives you &lt;strong&gt;the same JSON shape for every bank.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;10 banks and wallets are live: CBE, Telebirr, BOA, M-Pesa, Dashen, Awash, CBE Birr, Siinqee, eBirr and ZamZam. M-Pesa, ZamZam and CBE Birr are marked beta, as they're confirmed on few or no real receipts so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  One request
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://cheki.et/api/verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"bank": "dashen", "reference": "B22WDTI00SAMPLE1"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No signup and no API key. The body takes &lt;code&gt;bank&lt;/code&gt; plus whatever that bank needs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reference&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Almost always: a receipt link, Receipt ID, transaction number or FT Ref&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accountNumber&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BOA (last 5 digits) and older CBE FT receipts (last 8 digits)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;phoneNumber&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CBE Birr (the payer's phone)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qrData&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A scanned QR payload (BOA, CBE Birr, Telebirr)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That reference is made up, so you'll get a &lt;code&gt;422&lt;/code&gt; back. A confirmed receipt looks like this (names and numbers are fake):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Commercial Bank of Ethiopia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bankCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cbe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FT26SAMPLE01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"senderName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ABEBE KEBEDE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receiverName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TIGIST ALEMU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receiverAccount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1****6171"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1250.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ETB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://apps.cbe.com.et:100/?id=FT26SAMPLE0100006171"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cached"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one rule to remember: &lt;strong&gt;only &lt;code&gt;verified: true&lt;/code&gt; means the bank confirmed it.&lt;/strong&gt; &lt;code&gt;success: true&lt;/code&gt; only means the request was handled. A BOA QR that BOA couldn't confirm comes back with &lt;code&gt;success: true, verified: false, decodedOnly: true&lt;/code&gt;, and a QR can be copied. Don't ship on that.&lt;/p&gt;

&lt;p&gt;The error codes are what you'd expect: &lt;code&gt;400&lt;/code&gt; bad input, &lt;code&gt;404&lt;/code&gt; unknown bank, &lt;code&gt;413&lt;/code&gt; body too large, &lt;code&gt;422&lt;/code&gt; the bank has no such receipt, &lt;code&gt;429&lt;/code&gt; rate limited (with &lt;code&gt;Retry-After&lt;/code&gt;), &lt;code&gt;502&lt;/code&gt; the bank couldn't be reached. A &lt;code&gt;502&lt;/code&gt; for a geo-blocked bank includes a &lt;code&gt;fallbackUrl&lt;/code&gt;, which is the bank's own receipt page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node: cheki-verify
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;cheki-verify   &lt;span class="c"&gt;# 1.6.0, no dependencies, Node 20+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before marking an order paid, ask two questions: did the bank confirm it, and is it &lt;em&gt;this&lt;/em&gt; order's payment?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Cheki&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isVerified&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ChekiAPIError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cheki-verify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cheki&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Cheki&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// use a UNIQUE column in your database instead&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cheki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ChekiAPIError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// 422: the bank has no such receipt. 502 + fallbackUrl: bank not reachable from the server.&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serverMessage&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;openAtBank&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fallbackUrl&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// network error or timeout: try again later, don't accept&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isVerified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not confirmed by the bank&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`amount is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receiverAccount&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountLast4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid to another account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bankCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;receipt already used&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;senderName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkPayment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1250.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;accountLast4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;6171&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dashen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B22WDTI00SAMPLE1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last two checks are the ones people skip. A real receipt for money sent to someone else passes &lt;code&gt;isVerified&lt;/code&gt;. So does a real receipt reused from last week. The bank knows nothing about your orders, so &lt;strong&gt;“confirmed” isn't the same as “paid to you, for this order”.&lt;/strong&gt; That part lives in your code.&lt;/p&gt;

&lt;p&gt;In the TypeScript SDK, any non-2xx response throws a &lt;code&gt;ChekiAPIError&lt;/code&gt; (with &lt;code&gt;statusCode&lt;/code&gt;, &lt;code&gt;fallbackUrl&lt;/code&gt; and &lt;code&gt;checkedUrl&lt;/code&gt;), and a 429 throws &lt;code&gt;ChekiRateLimitError&lt;/code&gt;. It retries at most once, and only where that's safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python: cheki
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;cheki   &lt;span class="c"&gt;# 1.6.0, needs only requests, Python 3.10+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python client returns a result for API errors instead of raising, so it reads as one &lt;code&gt;if&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cheki&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChekiClient&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ChekiClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cheki&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cheki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cbe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://mbreciept.cbe.com.et/fHCxExampleRcpt01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sender_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receiver_account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fallback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bank not reachable from the server. Open:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fallback_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Not verified (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also installs a small CLI, which is handy in a terminal: &lt;code&gt;cheki-verify telebirr DET0SAMPLE --json&lt;/code&gt;. It exits 0 only when the bank confirmed the receipt.&lt;/p&gt;

&lt;h2&gt;
  
  
  End-of-day reconciliation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;POST /api/verify/batch&lt;/code&gt; takes up to 20 receipts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://cheki.et/api/verify/batch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"receipts": [
        {"bank": "dashen", "reference": "B22WDTI00SAMPLE1"},
        {"bank": "boa", "reference": "FT26SAMPLE07", "accountNumber": "12345"}
      ]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back &lt;code&gt;total&lt;/code&gt;, &lt;code&gt;verified&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;cached&lt;/code&gt; and one result per receipt. Both SDKs wrap it as &lt;code&gt;verifyBatch&lt;/code&gt; / &lt;code&gt;verify_batch&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and what's kept
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server side only.&lt;/strong&gt; The API sends no CORS headers, so call it from your backend, not the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits per IP:&lt;/strong&gt; 30 checks a minute (each batch item counts), and 5 batch requests a minute. The SDKs export &lt;code&gt;RECOMMENDED_BATCH_CHUNK&lt;/code&gt; (15) for long lists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; no accounts and no database. A receipt the bank confirmed stays in the server's memory for up to six hours, so a repeat check returns &lt;code&gt;cached: true&lt;/code&gt; without asking the bank again. Failed checks are never kept.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full reference is in the &lt;a href="https://cheki.et/docs" rel="noopener noreferrer"&gt;API docs&lt;/a&gt;, and the &lt;a href="https://cheki.et/developers" rel="noopener noreferrer"&gt;developer page&lt;/a&gt; has the Go, PHP and Dart SDKs as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telebirr and M-Pesa: self-host it
&lt;/h2&gt;

&lt;p&gt;Telebirr's and M-Pesa's receipt pages only answer from Ethiopian networks. The hosted API runs in Frankfurt, so for those two it returns a &lt;code&gt;502&lt;/code&gt; with a &lt;code&gt;fallbackUrl&lt;/code&gt;. A person at a counter can just tap that link on their phone. An automated checkout can't.&lt;/p&gt;

&lt;p&gt;The fix is to run your own copy on a server in Ethiopia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/1RB/cheki.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cheki
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;   &lt;span class="c"&gt;# serves on http://localhost:3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point the SDK at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cheki&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Cheki&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://your-server:3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cheki&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChekiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://your-server:3000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every option is an environment variable, and all are optional. For example, &lt;code&gt;CHEKI_CACHE=off&lt;/code&gt; turns off the in-memory cache if you'd rather keep nothing. There's a longer walkthrough in the &lt;a href="https://cheki.et/guides/self-hosting-docker-guide" rel="noopener noreferrer"&gt;self-hosting guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other options
&lt;/h2&gt;

&lt;p&gt;cheki isn't the only way to do this. check.et, verify.et and qbirr offer hosted APIs with keys and dashboards, and there are open-source libraries if you'd rather parse receipts inside your own app, such as &lt;code&gt;tx-verify&lt;/code&gt; for Python and &lt;code&gt;telebirr-receipt&lt;/code&gt; for Node. They all read the same public receipt pages. Pick based on which banks you need, whether you want an account and a dashboard, and whether you can run something on an Ethiopian network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help wanted
&lt;/h2&gt;

&lt;p&gt;The weakest spots are the beta banks. If you have a real M-Pesa, ZamZam or CBE Birr receipt that cheki gets wrong, an issue on &lt;a href="https://github.com/1RB/cheki/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; with the names and accounts redacted helps a lot. The repo has &lt;code&gt;npm run probe -- &amp;lt;receipt-url&amp;gt; --redact&lt;/code&gt; for exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cheki is free, and why that matters
&lt;/h2&gt;

&lt;p&gt;The banks publish every receipt for free, yet checking them is often sold back to businesses as API keys, monthly plans and per-check quotas. cheki started from a simple idea: &lt;strong&gt;you shouldn't pay to read a page your own bank already made public.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So it's MIT licensed: the web app, the API, every bank's parser and the TypeScript, Python, Go, PHP and Dart SDKs all live in one repo at &lt;a href="https://github.com/1RB/cheki" rel="noopener noreferrer"&gt;github.com/1RB/cheki&lt;/a&gt;. Read exactly how a receipt is checked, self-host it, or send a fix.&lt;/p&gt;

&lt;p&gt;For a tool like this, open source is practical. Banks change their receipt pages without warning, and developers like you are usually the first to notice. Whoever spots a broken parser can patch it, and new banks get added the same way, via the &lt;a href="https://cheki.et/guides/contribute-new-bank" rel="noopener noreferrer"&gt;add-a-bank guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;cheki isn't affiliated with any bank or Ethio Telecom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A screenshot is a claim. &lt;code&gt;verified: true&lt;/code&gt; is the proof.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>javascript</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
