Reference
API Reference
Register a webhook, then use these endpoint contracts. The Webhooks section has the setup steps, payload, and signature rules.
Overview
API responses use a predictable envelope with success, message, and data. Verification submissions also include requestId, verification, and links when a status URL is available. The suggested way to receive the finished result is an active webhook. Register it once, then call POST /api/verify without a callback URL.
Start with a webhook
Base URL: https://verify.et
Supported Banks & Required Fields
| Field | Required | Description |
|---|---|---|
| cbe | Required | Receipt ID in reference or referenceNumber. No suffix. A mbreciept.cbe.com.et link is accepted. Older FT references still need an 8-digit suffix. |
| boa | Required | referenceNumber/reference plus accountSuffix/suffix exactly 5 digits. |
| telebirr | Required | transactionNumber or reference. |
| mpesa | Required | transactionNumber or reference. Supported receipt URLs/SMS text can be parsed. |
| cbebirr | Required | receiptNumber/reference plus phone/phoneNumber. |
| dashen | Required | referenceNumber or reference. |
| awash | Required | referenceNumber or reference. Universal detection supports configured receipt host or token. |
| siinqee | Required | referenceNumber or reference. Universal detection supports configured receipt host or token. |
| kaafiebirr | Required | referenceNumber or reference. Full receipt URLs auto-route; bare tokens require explicit bank. phone is optional. |
| coopayebirr | Required | referenceNumber, reference, or receiptUrl: the receipt.ebirr.com/coopay/ link or its case-sensitive token (a full SMS also works). The SMS Ref number cannot be verified alone. phone is optional. |
| zemen | Optional | Enum/status surface only. POST /api/verify returns unsupported for direct Zemen submissions. |
Universal smart-router fields
bank and send reference. A CBE Receipt ID in that field needs no suffix. Use suffix only for an older CBE or Bank of Abyssinia FT reference, and phoneNumber for CBE Birr. Explicit bank payloads always win when bank is present. Full SMS or supported receipt URLs in identifier fields are best-effort sanitized to clean bank fields when the server can extract them confidently.Settlement Account Matching
New FeatureSettlement account matching verifies that a valid receipt was paid into the intended wallet or bank account. This protects checkout, fulfillment, and support workflows from accepting a real receipt that belongs to a different receiver.
How matching works
settlementAccount for a one-off receiver check. If it is omitted, Verify.ET checks the authenticated user's saved /api/bank-accounts records and registered business settlement accounts for the request bank. Wallet-like banks such as Telebirr and M-Pesa use Ethiopian phone canonicalization, while regular banks prefer normalized account-number matching.| Field | Required | Description |
|---|---|---|
| settlementAccount | Optional | Expected receiver account or phone number for this request. Manual values are checked before saved accounts. |
| settlementAccountMatch | Optional | Stable response object returned on completed result items, including matched, confidence, source, candidate count, ambiguity, reason, and safe debug data. |
| receiverAccount | Optional | Provider receiver field used for matching. Masked values such as 2519****0897 or 1****7441 are matched by visible digits. |
{
"bank": "telebirr",
"transactionNumber": "DET8FJGUJ4",
"settlementAccount": "0939080897"
}{
"bank": "telebirr",
"transactionNumber": "DET8FJGUJ4"
}{
"matched": true,
"matchType": "masked_pattern",
"matchConfidence": "high",
"source": "account_registry",
"bank": "telebirr",
"receiverAccount": "2519****0897",
"matchedSettlementAccount": "0939080897",
"matchedUserBankAccountId": "58807b8e-ff38-49c9-a836-304c6b88bc29",
"matchedBusinessBankAccountId": null,
"candidateCount": 1,
"ambiguous": false,
"reason": "receiver_mask_matches_visible_digits",
"debug": {
"receiverAccount": "2519****0897",
"candidateCount": 1,
"matchType": "masked_pattern",
"visiblePrefix": "2519",
"visibleSuffix": "0897",
"visibleCharacterCount": 8,
"matchedUserBankAccountId": "58807b8e-ff38-49c9-a836-304c6b88bc29",
"matchedBusinessBankAccountId": null
}
}{
"matched": false,
"matchType": "unmatched",
"matchConfidence": "none",
"source": "account_registry",
"bank": "telebirr",
"matchedUserBankAccountId": null,
"matchedBusinessBankAccountId": null,
"candidateCount": 0,
"ambiguous": false,
"reason": "no_registered_accounts"
}Stable Reason Codes
receiver_account_exact_matchreceiver_mask_matches_visible_digitsreceiver_suffix_matchesno_registered_accountsmissing_receiver_accountcandidate_account_mismatchambiguous_registered_accountsverification_not_successfulAmbiguous masked matches return matched: false, ambiguous: true, and a safe candidate count instead of silently picking one saved account.
Receipt Image Processing
OptionalSend multipart/form-data with one file named image. Verify.et reads a QR code, then the printed receipt, and continues the normal verification.
- 1
Turn it on
The billing owner enables Image Processing. It starts off.Enable Image Processing - 2
Add image credits
An accepted photo spends one image credit. Verification credits are checked first and billed separately.Top up image credits - 3
Send the photo
Post x-api-key, Idempotency-Key, and the image file. Optional text fields fill a value the photo does not show. 200 and 202 include imageProcessing.
| Field | Required | Description |
|---|---|---|
| image | Required | One JPEG, PNG, or WebP file, up to 8 MB. The part name is image. The declared type must match the file bytes, and the image must decode. |
| Idempotency-Key | Required | Header, 1 to 255 characters. The same key with the same file, text fields, and webhook URL replays the attempt. A different file or field needs a new key. |
| bank | Optional | Fills the bank when the image does not identify it. |
| reference | Optional | Universal identifier. For CBE, this is the Receipt ID when the photo does not already contain it. |
| referenceNumber | Optional | Same value as reference. For CBE, the Receipt ID, a CBE receipt link, or a legacy FT reference. |
| transactionNumber | Optional | Wallet transaction number, such as Telebirr or M-Pesa. |
| receiptNumber | Optional | CBE Birr receipt number. For CBE, an accepted alias of the Receipt ID. Prefer reference. |
| accountSuffix | Optional | Account suffix. suffix is the same field. Required for Bank of Abyssinia and for older CBE FT references. Omit it for a CBE Receipt ID. |
| phone | Optional | Wallet or CBE Birr phone. phoneNumber is the same field. |
| settlementAccount | Optional | Receiver account to match, using the same rules as a JSON verification. |
| webhookUrl | Optional | Optional one-off callback. A dashboard webhook already receives the result when this is omitted. X-Webhook-Url is the header form. |
curl -X POST \
"https://verify.et/api/verify?waitMs=5000" \
-H "x-api-key: $VERIFY_ET_API_KEY" \
-H "Idempotency-Key: receipt-order-123" \
-F "[email protected];type=image/jpeg" \
-F "bank=cbe" \
-F "reference=fHCxyExampleRcpt1"{
"success": true,
"message": "Verification queued.",
"data": [],
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"statusUrl": "/api/verify/550e8400-e29b-41d4-a716-446655440000",
"imageProcessing": {
"attemptId": "img_018f2c4e-9b1a-7d33-a6c0-e5b4c8d21a90",
"method": "qr",
"chargedCredits": 1
},
"verification": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"bank": "cbe",
"processingStatus": "queued",
"status": "pending",
"verified": false
},
"links": {
"statusUrl": "/api/verify/550e8400-e29b-41d4-a716-446655440000",
"pollAfterMs": 1500
}
}How a credit is spent
Idempotency-Key.Image errors
| HTTP | error.code | What to do |
|---|---|---|
| 400 | IMAGE_IDEMPOTENCY_KEY_REQUIRED | Send Idempotency-Key, 1 to 255 characters. |
| 400 | IMAGE_REQUIRED | Send exactly one file part named image. |
| 400 | INVALID_IMAGE | The file is empty or cannot be decoded. |
| 400 | INVALID_IMAGE_FIELDS | Each extra part is one unique text value, at most 2048 characters. |
| 400 | INVALID_MULTIPART | The body is not valid multipart form data. |
| 413 | IMAGE_TOO_LARGE | Keep the image at or under 8 MB. |
| 415 | UNSUPPORTED_IMAGE | Use JPEG, PNG, or WebP, and match the file contents to that type. |
| 402 | INSUFFICIENT_CREDITS | Verification credits are missing. No image credit is spent. |
| 402 | INSUFFICIENT_IMAGE_CREDITS | Image credits are missing. The body includes error.balance and error.required. |
| 403 | IMAGE_PROCESSING_DISABLED | The billing owner turns Image Processing on. |
| 409 | IMAGE_IDEMPOTENCY_CONFLICT | This key already belongs to a different image or fields. |
| 409 | IMAGE_PROCESSING_IN_PROGRESS | The same upload is still running. Retry after the Retry-After header. |
| 422 | IMAGE_FIELD_CONFLICT | A text field disagrees with the image. The image credit was spent. |
| 422 | IMAGE_EXTRACTION_INCOMPLETE | The image and text fields are still short of a verifiable request. The image credit was spent. |
| 422 | IMAGE_UNREADABLE | QR and OCR could not read a payment. Send a clearer image with a new key. |
| 502 | IMAGE_PROVIDER_FAILURE | OCR failed. The image credit was spent. Retry with a new key. |
| 504 | IMAGE_OCR_TIMEOUT | OCR exceeded 30 seconds. The image credit was spent. Retry with a new key. |
| 503 | IMAGE_PROCESSING_UNAVAILABLE | Image processing is temporarily unavailable. No image credit is spent. |
After verification starts, a metadata-stripped review image is kept only for a failed check, for 14 days, and only for the signed-in billing owner at GET /api/image-processing/attempts/:attemptId/review-image. A successful verification removes it. The original upload and the OCR document are not stored.
