Verify payments API
A single REST endpoint that confirms Ethiopian transactions. Send a transaction reference or a full receipt URL, with a bank code when required, to receive a normalized verification object across CBE, Telebirr, M-Pesa, and every other supported provider.
Previously verified transactions return the same data shape with cached: true and a confirmation object that tracks how many times the payment has been checked.
Authentication
/v1/verify works without any credentials - anonymous calls are permitted for evaluation, subject to a stricter rate limit (see Rate limits). Every other endpoint (saved accounts, token management) requires a bearer token.
Create an account at POST /auth/register, or exchange existing credentials at POST /auth/login - both return a personal access token in the response body. Send it on every subsequent request:
Authenticated requests use your plan's rate limits and appear in your account usage history.
Verifies a single transaction. Accepts either a query string (GET) or a JSON body (POST) - same parameters either way. Supply either url or reference; the rest are optional enrichments.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | conditional | Full receipt URL from the customer's SMS or banking app. Required unless reference is supplied. |
| reference | string | conditional | Transaction or FT reference. Required unless url is supplied. Some institutions also need bank. |
| bank | string | conditional | Institution code - cbe, cbebirr, boa, mpesa, awash, dashen, telebirr, zemen, wegagen, nib, siinqee, abay, amhara, berhan, hibret, oromia, ahadu, kaafiebirr, ahaduebirr, coopebirr, nibebirr, wegagenebirr, siinqeeebirr, ebirr, zamzam. See Supported banks. |
| phone | string | conditional | Required for some mobile wallets when verifying by reference. |
| suffix | string | optional | Additional digits shown on certain receipts, when required by the institution. |
| settlement_account | string | optional | Account number or phone the payment should have landed in. Adds a settlement_match object to the response. |
| expected_amount | number | optional | If set, the result flags any mismatch against the settled amount in amount_match. |
Try it
The verification object
Every successful call returns the same top-level shape, regardless of the underlying institution. The data object holds bank-specific fields.
| Field | Type | Description |
|---|---|---|
| valid | boolean | true only when the transaction exists and has settled. |
| bank | string | Echo of the institution code that was checked. |
| status | enum | verified · invalid · pending. |
| cached | boolean | true if this payment was verified before and returned without a fresh institution lookup. |
| data | object | Bank-specific transaction fields. See Data fields below. |
| confirmation | object | Present on cached responses. Tracks how many times the transaction has been independently verified. |
| settlement_match | object | Present when settlement_account was provided. Indicates whether the receiver matches. |
| amount_match | object | Present when expected_amount was provided. Compares expected vs. settled amounts. |
Response format
Verified transaction
{ "valid": true, "status": "verified", "bank": "boa", "cached": false, "data": { "sender_name": "ABEBE BEKELE", "sender_account": "1******77", "receiver_name": "ACME TRADING PLC", "receiver_account": "1**********08", "receiver_bank": "Commercial Bank of Ethiopia", "sent_amount": "10.00 ETB", "service_charge": "0.06 ETB", "tax_vat": "0.01 ETB", "total_paid": "10.07 ETB", "transfer_date": "14/06/26 12:46", "transaction_id": "FT26166XLKCN", "transaction_type": "Other Bank Transfer", "reason": "P2P Transaction" } }
Pending (institution temporarily slow)
When an institution is temporarily slow or unreachable, a soft pending status is returned instead of a hard error. Retry shortly.
{ "valid": false, "status": "pending", "bank": "cbe", "cached": false, "data": { "error": "Bank service is temporarily unreachable. Please try again in a moment." } }
Invalid transaction
Not-found or malformed transactions are still a 200 OK - the bank was reachable, it just doesn't recognize the reference. status is invalid and valid is false.
{ "valid": false, "status": "invalid", "bank": "boa", "cached": false, "data": { "transaction_id": "", "error": "Invalid reference number" } }
Repeat verification
When a transaction was verified before, the data object is identical but cached is true and a confirmation object is added.
{ "valid": true, "status": "verified", "bank": "boa", "cached": true, "data": { ... }, "confirmation": { "confirmed_count": 4, "first_confirmed_at": "2026-06-01T09:12:00+03:00", "last_confirmed_at": "2026-06-15T14:33:00+03:00" } }
Settlement match
When settlement_account is provided, the response includes a settlement_match object with matched and match_confidence.
{ "valid": true, "data": { ... }, "settlement_match": { "matched": true, "match_confidence": "high" } }
Data fields
All fields live inside data. Not every field is present for every bank - omitted fields are absent from the object rather than null.
| Field | Type | Banks | Description |
|---|---|---|---|
| sender_name | string | All | Full name of the payer / debit account holder. |
| sender_account | string | All | Masked or partial account number of the sender. |
| receiver_name | string | All | Full name of the beneficiary / credit account holder. |
| receiver_account | string | All | Masked or partial account / phone of the receiver. |
| receiver_phone | string | cbe, cbebirr, mpesa, telebirr | Receiver phone number (international format). |
| receiver_bank | string | awash, boa, cbe | Destination bank name (inter-bank transfers). |
| sent_amount | string | All | Amount credited to the receiver, with currency suffix. |
| service_charge | string | All | Bank service / commission fee. |
| tax_vat | string | All | 15% VAT on the service charge. |
| total_paid | string | All | Total debited from sender including fees. |
| transfer_date | string | All | Transaction timestamp. Format varies by bank. |
| transaction_id | string | All | Unique transaction reference for this payment. |
| transaction_type | string | boa, cbe, dashen, awash | Type of transaction (transfer, topup, etc.). |
| transaction_status | string | cbe | Bank transaction status (e.g. COMPLETED). |
| reason | string | cbe, awash, boa, telebirr, mpesa | Payment reason or narrative. |
| channel | string | cbe, telebirr, cbebirr | Payment channel used (e.g. ANDROID, IOS). |
| invoice_no | string | zemen | Invoice number. |
| receipt_number | string | cbebirr | Receipt number. |
| order_id | string | cbebirr | Order ID. |
| receipt_no | string | mpesa | Receipt number. |
| service_type | string | dashen | Service type label. |
| narrative | string | dashen | Free-text narrative. |
| transfer_ref | string | dashen | Secondary transfer reference. |
| error | string | All (when invalid) | Human-readable reason when valid is false. |
| confirmation.confirmed_count | integer | Repeat checks | Number of times this transaction has been verified. |
| confirmation.first_confirmed_at | datetime | Repeat checks | ISO 8601 timestamp of the first verification. |
| confirmation.last_confirmed_at | datetime | Repeat checks | ISO 8601 timestamp of the most recent verification. |
| settlement_match.matched | boolean | With settlement_account | true if the receiver account matches. |
| settlement_match.match_confidence | string | With settlement_account | high or none. |
Supported institutions
Pass the bank code with references when required. Methods: reference = transaction reference, link = full receipt URL.
| Institution | Code | Methods |
|---|---|---|
|
|
cbe | reference · link |
|
|
cbebirr | reference (+ phone) · link |
|
|
boa | reference · link |
|
|
telebirr | reference · link |
|
|
mpesa | reference · link |
|
|
dashen | reference · link |
|
|
awash | link · reference |
|
|
wegagen | reference · link |
|
|
zemen | reference · link |
|
|
nib | reference · link |
|
|
siinqee | reference · link |
|
|
abay | reference |
|
|
amhara | reference · link |
|
BR
Berhan Bank
|
berhan | reference · link |
|
|
hibret | link |
|
|
oromia | reference · link |
|
|
ahadu | link |
|
|
kaafiebirr | reference · link |
|
|
ahaduebirr | reference · link |
|
|
coopebirr | reference · link |
|
|
nibebirr | reference · link |
|
|
wegagenebirr | reference · link |
|
|
siinqeeebirr | reference · link |
|
|
ebirr | reference · link |
|
|
zamzam | reference · link |
Rate limits
Only /v1/verify is rate-limited; every other endpoint is unlimited. See Pricing for public tiers.
| Tier | Limit | Scope |
|---|---|---|
| Anonymous (no bearer token) | 10 requests / hour | Per IP address |
| Free (registered API key) | 50 requests / hour | Per user account |
| Enterprise (custom) | Custom or unlimited | Assigned by admin |
When exceeded, the API returns 429 Too Many Requests with a JSON body explaining the limit. Authenticated users without credits receive 402 Payment Required. Sign in for higher limits on paid plans.
Request examples
Errors
Most verification outcomes - including invalid or not-found transactions - are still a 200 OK with valid: false; check status and data.error rather than the HTTP code. These are the actual non-200 responses across the API:
| HTTP | Meaning | Description |
|---|---|---|
| 401 | Unauthenticated | Missing or invalid bearer token on an endpoint that requires one - everything except /v1/verify, /auth/register, and /auth/login. |
| 404 | Not found | Token ID or saved-account ID in the URL doesn't belong to the caller. |
| 422 | Validation error | On /v1/verify: missing url or reference, unknown bank, or required fields for the selected institution. On /auth/*: standard field validation. |
| 402 | Insufficient credits | Authenticated user has no credits left. Top up at /dashboard/billing. |
| 429 | Rate limited | /v1/verify only. Anonymous callers exceeded 10 requests/hour - see Rate limits. |
| 502 | Verification error | /v1/verify only. We could not read the institution response. Try a different reference or receipt link. |
Register & login
Both endpoints are public and return a personal access token to use as a bearer token on every other endpoint.
Register
{ "name": "Abebe Kebede", "email": "abebe@example.com", "password": "secret123", "password_confirmation": "secret123" }
{ "user": { "id": 1, "name": "Abebe Kebede", "email": "abebe@example.com" }, "token": "LNjMOFktw2rGV5F8u7uAxxxxxxxxxxxxxxxxxxxx" }
Log in
Body: email, password. Returns the same { user, token } shape as register, or 422 with "The provided credentials are incorrect."
Current user
Returns { id, name, email } for the authenticated token.
Log out
Revokes the token used on the request. Returns { "message": "Logged out." }.
Personal access tokens
Issue additional tokens (e.g. one per device or integration) without re-authenticating. All three routes require a bearer token.
| Method | Path | Description |
|---|---|---|
| GET | /auth/tokens | List this user's tokens - id, name, last_used_at, created_at, expires_at (no plaintext token value). |
| POST | /auth/tokens | Body: { "name": "my-device" } for a non-expiring token, or add "expires_at": "2026-12-31" for a dated key. Returns 201 with the plaintext token - shown only this once. |
| DELETE | /auth/tokens/{id} | Revokes one token by id. 404 if it doesn't belong to the caller. |
Saved accounts
A user's own address book of beneficiary accounts - handy for pre-filling settlement_account on repeat payments. All routes require a bearer token and are scoped to the caller; there's no cross-user access.
| Method | Path | Description |
|---|---|---|
| GET | /accounts | List saved accounts, sorted by name. |
| POST | /accounts | Body: name, account_number, provider (all required). Returns 201. |
| PUT | /accounts/{id} | Same fields, all optional - only supplied ones are updated. |
| DELETE | /accounts/{id} | Removes the saved account. 404 if it doesn't exist for this caller. |
[ { "id": 3, "name": "Rent", "account_number": "1000123456789", "provider": "cbe", "created_at": "2026-06-01T09:12:00.000000Z" } ]