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, and you can add your own by writing one Python class.
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 creates a Gateway Payment Request. 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, or when your app asks for the latest status.
- You react to the status change, for example by creating an order when the request becomes Paid.
payment_request = frappe.get_doc(
{
"doctype": "Gateway Payment Request",
"gateway": "Stripe", # a Payment Gateway Profile
"amount": 49.99,
"currency_code": "USD",
"ref_doctype": order.doctype, # any DocType: the thing being paid for
"ref_docname": order.name,
"customer_email": "shopper@example.com",
}
).insert(ignore_permissions=True)
# Send the shopper here:
payment_request.order_url
Only System Manager can create Gateway Payment Requests. Accounts User can read and edit them. A website checkout therefore inserts with ignore_permissions=True, as above. Check what the shopper is allowed to buy before you do that.
To react when the payment settles, add a doc_events hook on Gateway Payment Request → on_update:
def on_payment_request_update(doc, method=None):
if doc.has_value_changed("status") and doc.status == "Paid":
frappe.enqueue(create_order, payment_request=doc.name, enqueue_after_commit=True)
Check has_value_changed, because the hook fires on every save, not only when the status changes. Enqueue heavy work. The hook runs inside the webhook request, and if it throws, the status change is rolled back.
When the shopper comes back to your site, call payment_request.sync_status() from a POST request. That reads the real status from the gateway. Never trust the redirect itself, because anyone can load a success URL. Return pages shows how to build the pages shoppers come back to.
Build your own gateway
A gateway is a Frappe Single DocType that holds the gateway's credentials. Its controller class extends PaymentGatewayBase and implements four methods:
| Method | What it does |
|---|---|
create_session |
Opens a hosted checkout and returns its URL |
get_payment_status |
Asks the gateway whether the shopper paid |
refund_payment |
Refunds all or part of a payment |
handle_webhook |
Verifies a gateway callback and reports what happened |
The status rules, webhook endpoint, replay protection and refund bookkeeping are already done. Your class only talks to the gateway.
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
Next steps
- Return pages: the confirmation and payment-failed pages every gateway needs
- Build a payment gateway: add your own gateway, step by step
- Gateway reference: every method, parameter and return value
- Webhooks and security: verify callbacks and debug deliveries