---
title: "Return pages"
space: "BWH Payments"
url: "https://docs.bwh.tech/bwh-payments/get-started/return-pages"
updated: "2026-09-21"
---

After paying, or giving up, the shopper is sent back to your site. BWH Payments doesn't include these pages. You build them in your own app, then enter their URLs in each gateway's settings.

You need two pages:

| 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.

```python
# 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:

```js
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);
```

:::danger[Don't sync inside get_context]
Frappe only saves database changes on POST, PUT and DELETE requests. If your page's `get_context` calls `sync_status()` during a normal page load (a GET), the shopper sees *Paid*, but the new status is thrown away when the request ends. Confirm from a POST, as shown above.
:::

:::caution[Check who's asking]
`confirm_payment` accepts guests, so anyone can call it with any reference. Before you show anything, check that the payment belongs to the current shopper, for example that `ref_docname` is their order. `sync_status()` only brings the status up to date from the gateway, but order details are private.
:::

## 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](/bwh-payments/get-started/overview): how a payment moves from Pending to Paid
- [Webhooks and security](/bwh-payments/build/webhooks-and-security): the other way a payment settles
