BWH Payments is a Frappe app for taking payments through hosted checkout pages. Your app asks for a payment, the shopper pays on the gateway's own page, and BWH Payments tells you when the money has arrived. You can also refund all or part of a payment later.
It ships with four gateways. You can add your own with one settings DocType and a small service client.
Built-in gateways
| Gateway | Best for | Page |
|---|---|---|
| Stripe | Cards and wallets, worldwide | Stripe |
| Razorpay | Cards, UPI, netbanking and wallets in India | Razorpay |
| Telr | Cards and local methods across the GCC | Telr |
| Tabby | Buy now, pay later in the Middle East | Tabby |
How a payment works
Every payment is a Gateway Payment Request document. Its status moves the same way whichever gateway is behind it:
stateDiagram-v2
[*] --> Pending: checkout opened
Pending --> Paid
Pending --> NotPaid
Pending --> Cancelled
Pending --> Expired
Paid --> PartiallyRefunded: refund
Paid --> Refunded: full refund
PartiallyRefunded --> Refunded: refund the rest
NotPaid: Not Paid
PartiallyRefunded: Partially Refunded
- Your app calls
create_payment. BWH Payments asks the gateway for a checkout session and stores the checkout URL on the request. - You redirect the shopper to that URL. They pay on the gateway's own page.
- The request moves out of Pending. This happens when the gateway sends a signed webhook, when the shopper comes back through BWH Payments, or when your app asks for the latest status.
- Your document hears about it. When the request becomes Paid, BWH Payments calls
on_payment_authorizedon the document being paid for.
Refunds work the same way. Each refund is a Gateway Refund, and your document hears each change through on_refund_update, including refunds raised on the gateway's own dashboard.
from bwh_payments.api import create_payment
payment_request = create_payment(
order.doctype, # any DocType: the thing being paid for
order.name,
49.99,
"USD",
"Stripe", # a Payment Gateway Profile
customer_email="shopper@example.com",
)
# Send the shopper here:
payment_request.order_url
Accept a payment covers each step in detail.
Two ways a gateway is built
A gateway is a Frappe Single DocType that holds the gateway's credentials. Its controller extends PaymentGatewayBase and makes every payment decision: amounts, status mapping, capture and refund rules, and webhook checks.
The controller doesn't call the gateway's API itself. It calls a service client, and the client does the HTTP or SDK call. Each service has three clients:
| Client | Used | Does |
|---|---|---|
base.py |
Never directly | Lists the calls, and models the gateway's JSON |
live.py |
On every real site | Makes the call, logs it, and turns a refusal into an error |
stub.py |
Only in tests | Keeps the gateway's state in memory, so tests never touch the network |
Service clients explains why. Build a payment gateway walks through one.
Install
You need a site on Frappe 16 or later.
bench get-app https://github.com/bwhtech/bwh_payments
bench --site your.site install-app bwh_payments
BWH Payments installs the Stripe SDK (12.5 or later) and pydantic 2.
Next steps
- Accept a payment: create a request, and react when it's paid
- Return pages: the confirmation and payment-failed pages every gateway needs
- Move from frappe/payments: keep your gateways and payment history
- Build a payment gateway: add your own gateway, step by step
- Gateway contract: every method, model and return value