Adding a payment option to a website looks like a small feature and behaves like a big one. Money introduces failure cases that ordinary forms never face: payments that succeed but are not recorded, customers who pay twice, transfers that arrive with no reference, and people who claim to have paid when they have not. This guide covers what developers should design for when taking payments on Ethiopian websites, whether the flow is a manual transfer or an automated integration.
Start from the payment methods people actually use
International gateways are built around cards, which a large share of local customers do not use day to day. Mobile money and bank transfers, such as TeleBirr and CBE Birr, are the rails people already trust. Design your checkout around them. We cover the specifics in Accepting TeleBirr and CBE Birr on a Website. Always check each provider's current requirements and documentation, because merchant rules and APIs change.
Choose the integration level honestly
| Level | How it works | Good for |
|---|---|---|
| Manual confirmation | Customer sends money to a displayed account, then submits a reference; you verify it | Giving pages, low volume, small shops |
| Semi-automated | Customer pays through the provider; you confirm by checking statements or a provider dashboard | Moderate volume |
| Fully automated | Provider notifies your server of each payment automatically | E-commerce, subscriptions, high volume |
Do not build the most complex option first. Manual confirmation done well is reliable and well understood by customers. Move to automation when volume makes manual checking a bottleneck.
Never trust the browser
The most common payment bug is believing the customer's browser. A "payment successful" page that a user can reach by editing a URL is not proof of payment. Rules to follow:
- Confirm payments on the server, using a provider notification or by checking a trusted record. Never mark an order paid because a return URL or JavaScript callback said so.
- Calculate prices on the server. Never accept an amount sent from the page.
- Verify notifications are genuine: use whatever signature, secret or lookup mechanism the provider offers, and reject anything that does not match.
- Match the amount and currency of the payment against the order before fulfilling it.
Model an order's life explicitly
Give every order a status that moves through a defined set of states, for example pending to awaiting_verification to paid or failed, with a timestamp for each change. This makes problems visible. A pile of orders stuck in awaiting_verification tells you where to look. Store a unique order reference and show it to the customer, so both of you can refer to the same transaction.
CREATE TABLE payments (
id INTEGER PRIMARY KEY,
order_ref TEXT NOT NULL UNIQUE,
method TEXT NOT NULL, -- telebirr, cbe, bank
amount_birr INTEGER NOT NULL,
provider_ref TEXT UNIQUE, -- transaction code; UNIQUE prevents reuse
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL,
verified_at TEXT
);
Making the provider's transaction code unique is a simple, effective defence against the same payment being claimed for two orders.
Plan for failure, duplicates and retries
- Failed payments: tell the customer clearly what happened and how to retry, without creating a second order.
- Duplicate notifications: providers may send the same notification more than once. Make your handler idempotent, meaning that processing it twice has the same effect as once.
- Paid but not recorded: keep a way for customers to report this and for you to look up their transaction manually.
- Timeouts: if a customer pays but closes the page, they must still be able to come back and complete or confirm the order.
- Refunds and corrections: decide the process before you need it, and record each one.
Design for clarity and trust
Most abandoned payments are caused by confusion, not technology.
- Show account numbers and names in a form that is easy to copy and hard to mistype.
- Make clear which account is for which purpose (general giving, a building fund, a specific product).
- Give numbered instructions in plain language, in English and Amharic where your audience needs it.
- Show the exact amount to pay, including any fees, before the customer starts.
- Make it easy to submit a reference or screenshot afterwards, and tell them what happens next and how long it takes.
Protect customer and payment information
- Use HTTPS everywhere, especially on any page that collects personal details.
- Collect the minimum. Do not store what you do not need. Avoid storing card numbers; use a provider's hosted payment page if you ever take cards.
- Protect the admin side: payment lists show names, phone numbers and amounts, so use strong passwords, session timeouts and access limits. See PHP Security Basics.
- Keep secrets secret: provider keys belong in server-side configuration, never in client-side code or a public repository.
- Log carefully. Record events for reconciliation, but not secrets or full personal details.
Reconcile regularly
Reconciliation means comparing your records against the provider's statements. Do it on a schedule, not only when something looks wrong: daily for active shops, weekly for giving pages. Look for payments with no order, orders marked paid with no payment, and mismatched amounts. Build an admin view that shows these differences, because spreadsheets do not scale and mistakes hide in them.
Be clear about legal and policy matters
Publish clear terms: prices, delivery, refund policy and contact details. If you are accepting money as a business or organisation, make sure you meet the registration, licensing and tax requirements that apply to you, and check the provider's merchant terms. We are not lawyers or financial advisers, so confirm these points with the relevant authority or a qualified professional.
Pre-launch checklist
- Payments are confirmed on the server, not by the browser.
- Prices and amounts are calculated server-side.
- Every order has a unique reference and clear status history.
- Provider transaction codes cannot be reused.
- Duplicate notifications are handled safely.
- Customers can recover from a closed tab or failed attempt.
- Instructions are clear in every language you serve.
- HTTPS, secrets and admin access are secured.
- A reconciliation routine is in place and scheduled.
The short version
A payment feature is a record-keeping system with a checkout attached. Verify on the server, track every state, expect failure and duplicates, and reconcile on a schedule.