After paying, or giving up, the shopper is sent back to your site. BWH Payments does not include these pages. You build them in your own app.
There are two ways to send the shopper back:
| Way | Set | BWH Payments confirms the payment |
|---|---|---|
| Per payment | redirect_to on create_payment |
Yes, before the shopper reaches your page |
| Per gateway | The return URLs in each gateway's settings | No. Your page must confirm it. |
Per payment: redirect_to
Pass a path on your site:
create_payment(order.doctype, order.name, order.grand_total, order.currency, "Stripe", redirect_to=f"/orders/{order.name}")
Every gateway then returns the shopper, after a payment, a failure or a cancel, to this endpoint:
/api/method/bwh_payments.bwh_payments.checkout.return_from_gateway?request=<Gateway Payment Request name>
The endpoint asks the gateway for the status, saves it, and redirects to redirect_to. If the payment is now Paid, your document's on_payment_authorized has already run. Your page reads the status of your own document.
- The settings' return URLs are not used for this payment.
- The endpoint accepts guests, and is rate limited to 60 requests per minute per IP address.
- If the gateway check fails, the shopper still goes to your page. The webhook settles the payment later.
Your page still shows a Pending state, because some payments settle after the shopper comes back. See If the status is still Pending.
Per gateway: settings URLs
You enter the URLs of two pages in each gateway's settings:
| Page | Enter it as | It should |
|---|---|---|
| Confirmation | Success URL (Telr: Authorised URL) | Confirm the payment with the gateway, then show Paid, or We're confirming your payment while it's still Pending |
| Payment failed | Failure URL, Cancelled URL (Telr: Declined URL, Cancelled URL) | Say the payment didn't go through, and offer a retry or another payment method |
Use full https:// URLs, for example https://shop.example.com/en/payment/confirmation.
What the gateway adds to the URL
Each gateway sends the shopper back with a query parameter that identifies the payment:
| Gateway | Parameter | Holds |
|---|---|---|
| Stripe | session_id |
The gateway session id, which is the request's order_ref |
| Razorpay | reference_id |
The Gateway Payment Request's name |
| Telr | reference_id |
The Gateway Payment Request's name |
| Tabby | reference_id |
The Gateway Payment Request's name |
Razorpay and Tabby add their own parameters as well, which you can ignore. Your page should accept either value, and look the request up by order_ref first, then by name.
Confirm the payment
The redirect proves nothing: anyone can open your success URL. Ask the gateway instead, with sync_status(), from a POST request.
# your_app/api.py
import frappe
from frappe import _
@frappe.whitelist(allow_guest=True, methods=["POST"])
def confirm_payment(reference: str) -> dict:
name = frappe.db.get_value(
"Gateway Payment Request", {"order_ref": reference}, "name"
) or frappe.db.exists("Gateway Payment Request", reference)
if not name:
frappe.throw(_("Payment not found"), frappe.DoesNotExistError)
payment_request = frappe.get_doc("Gateway Payment Request", name)
validate_shopper_owns(payment_request) # your check: is ref_docname theirs?
if payment_request.status == "Pending":
payment_request.sync_status()
return {"status": payment_request.status}
Call it when the page loads:
const params = new URLSearchParams(window.location.search);
const reference = params.get('session_id') || params.get('reference_id');
const response = await frappe.call({
method: 'your_app.api.confirm_payment',
args: { reference },
type: 'POST',
});
showPaymentStatus(response.message.status);
If the status is still Pending
The shopper can arrive before the gateway has settled the payment. Show a "confirming your payment" state, and call confirm_payment again after a few seconds. The webhook usually settles it shortly afterwards.
Some shoppers never come back to your site, for example when they close the tab after paying. Run a scheduled job that calls sync_status() on requests that are still Pending, so those payments settle too. Telr has no trustworthy webhook, and Tabby captures payments only on a status check, so both depend on this.
Language
For Razorpay, Telr and Tabby, a language segment at the start of your URL's path is switched to the shopper's current language. https://shop.example.com/en/payment/confirmation becomes /ar/payment/confirmation for an Arabic session. Stripe uses your URLs exactly as entered.
Next steps
- Overview: how a payment moves from Pending to Paid
- Webhooks and security: the other way a payment settles