---
title: "Gateway reference"
space: "BWH Payments"
url: "https://docs.bwh.tech/bwh-payments/build/gateway-reference"
updated: "2026-09-21"
---

Every gateway is a Single DocType whose controller extends both `Document` and `PaymentGatewayBase`:

```python
from frappe.model.document import Document

from bwh_payments.base_class import PaymentGatewayBase


class AcmePayGatewaySettings(Document, PaymentGatewayBase): ...
```

| Method | Required | Called when |
|---|---|---|
| `create_session` | ✅ | A Gateway Payment Request is created |
| `get_payment_status` | ✅ | An app checks a payment without waiting for a webhook |
| `refund_payment` | ✅ | A refund is issued |
| `handle_webhook` | ✅ | The gateway posts to the webhook endpoint |
| `cancel_session` | | Your app calls `release_if_unpaid()` on an open checkout |
| `get_gateway_name` | | Never by BWH Payments. It's for your own use. |

## Rules for every method

- **Money is in major units.** `49.99` means $49.99. Convert with the currency helpers below, and use the same conversion for charges and refunds.
- **Statuses are BWH Payments statuses**, already mapped from the gateway's own wording: `Pending`, `Paid`, `Not Paid`, `Cancelled`, `Expired`.
- **To report failure, throw.** Use `frappe.throw` with a message a person can act on.

---

## create_session

```python
def create_session(
	self,
	amount: float,
	currency: str,
	reference: str | None = None,
	customer: dict | None = None,
) -> dict
```

Opens a hosted checkout. Runs when a Gateway Payment Request is saved without a gateway session, which normally means once, on insert.

**Parameters**

| Name | Type | Description |
|---|---|---|
| `amount` | `float` | Amount to charge, in major units |
| `currency` | `str` | ISO 4217 code, for example `USD` or `KWD` |
| `reference` | `str` | The Gateway Payment Request name. Send it as the gateway's merchant reference. |
| `customer` | `dict` | The shopper. See Customer below. Any field may be empty. |

**Returns**

| Key | Required | Description |
|---|---|---|
| `session_id` | ✅ | The gateway's id for this checkout, stored as the request's `order_ref`. **It must be the id the gateway's webhooks and status endpoint use.** |
| `redirect_url` | ✅ | The hosted checkout URL the shopper is sent to |
| `success_url` | | Where the shopper returns after paying |
| `cancel_url` | | Where the shopper returns after cancelling |
| `failure_url` | | Where the shopper returns after a failed payment |

**Throw when** the currency isn't supported, a required setting is missing, or the gateway refuses to open a checkout. The request isn't saved, and your message is raised to the app that created it.

:::tip
If the gateway can't send its own id back to your success URL, add `reference_id=<reference>` to the return URLs, so your confirmation page can find the request.
:::

---

## get_payment_status

```python
def get_payment_status(self, session_id: str) -> str
```

Reads the current status **from the gateway's API**. It is called:

- when the shopper returns to the site
- by your own scheduled job, if you run one to settle Pending payments no one confirmed
- by `release_if_unpaid()`, before an open checkout is cancelled

For gateways with no trustworthy webhook, this is the only source of truth.

**Parameters**

| Name | Type | Description |
|---|---|---|
| `session_id` | `str` | The `session_id` your `create_session` returned |

**Returns** one of `Pending`, `Paid`, `Not Paid`, `Cancelled`, `Expired`.

- Map anything you don't recognise to `Pending`.
- Return `Paid` only when the money has actually been captured.
- Only a request that is still `Pending` changes. Once it's Paid, Cancelled, Not Paid or Expired, a later answer is ignored.

---

## refund_payment

```python
def refund_payment(self, session_id: str, amount: float, currency: str | None = None) -> dict
```

Refunds part or all of a paid checkout. Before your method runs, BWH Payments has already:

- checked the payment is refundable
- checked `amount` isn't more than what's left
- skipped a refund Payment Entry (or an amendment of one) that was already refunded

**Parameters**

| Name | Type | Description |
|---|---|---|
| `session_id` | `str` | The checkout to refund |
| `amount` | `float` | Amount to refund, in major units |
| `currency` | `str` | The payment's currency |

**Returns**

| Key | Description |
|---|---|
| `refund_id` | The gateway's refund id. Every partial refund's id is kept for reconciliation. |
| `status` | Informational: the gateway's own refund status, for example `succeeded` or `pending` |
| `amount` | Informational: the amount refunded, in major units. The refunded total is increased by the amount requested, not this value. |

**Throw when** the gateway refuses the refund. The payment's refund total and status are left unchanged.

---

## handle_webhook

```python
def handle_webhook(self, payload: bytes, headers: dict) -> dict
```

Verifies a delivery to the webhook endpoint and says what it means.

**Parameters**

| Name | Type | Description |
|---|---|---|
| `payload` | `bytes` | The raw request body, exactly as received. Verify signatures against this. |
| `headers` | `dict` | Request headers, with **title-case** names such as `X-Acme-Signature` |

**Returns**

| Return | Meaning | HTTP response |
|---|---|---|
| *(throws)* | Verification failed | 400 |
| `{}` | Genuine, but not something to act on | 200 |
| `{"session_id", "status", "event_id"}` | Apply `status` to the checkout with this `session_id` | 200 |

`event_id` must stay the same when the gateway retries a delivery, and differ between events. A repeated `event_id` is ignored.

See [Webhooks and security](/bwh-payments/build/webhooks-and-security).

---

## cancel_session

```python
def cancel_session(self, session_id: str) -> bool
```

*Optional. The default returns `False`.*

Cancels an open checkout. Called by `release_if_unpaid()`. Return `True` only when the gateway confirms the checkout can no longer be paid. On any gateway error, return `False` instead of throwing.

If this returns `False`, the payment stays Pending until the gateway settles it, and `release_if_unpaid()` returns `False`.

---

## get_gateway_name

```python
def get_gateway_name(self) -> str
```

*Optional. The default is the class name.* Returns a human-readable name, for example `"Acme Pay"`. BWH Payments doesn't call it. It's there for your own code.

---

## Customer

The `customer` argument to `create_session`:

```python
{
	"ref": "CUST-0001",  # linked customer id, or ""
	"email": "shopper@example.com",  # or ""
	"phone": "+15550100",  # or ""
	"name": {"forenames": "Ada", "surname": "Lovelace"},
	"address": {"line1": "1 Main St", "city": "Dubai", "country": "United Arab Emirates"},  # or None
}
```

Guest checkouts often leave fields empty. Drop the empty ones before you send the dict to the gateway.

## Currency helpers

```python
from bwh_payments.currency import (
	from_minor_units,
	get_minor_unit_exponent,
	to_minor_units,
	validate_transaction_currency,
)
```

| Helper | Example | Use it to |
|---|---|---|
| `to_minor_units(amount, currency)` | `(49.99, "USD")` → `4999`, `(12.345, "KWD")` → `12345` | Send amounts to gateways that bill in cents |
| `from_minor_units(amount, currency)` | `(4999, "USD")` → `49.99` | Read the gateway's echoed amounts |
| `get_minor_unit_exponent(currency)` | `"KWD"` → `3`, `"JPY"` → `0`, `"USD"` → `2` | Format major-unit strings, e.g. `f"{amount:.{exponent}f}"` |
| `validate_transaction_currency(currency)` | Throws for an unknown code | Check the currency exists on the site before calling out |

Exponents follow ISO 4217. They aren't read from the site's Currency settings.

:::caution[Three-decimal currencies]
Frappe rounds currency to the site's number format, usually 2 decimals. For 3-decimal currencies, turn on **System Settings → Use Number Format From Currency**, and leave **Currency Precision** blank or set it to 3 or more. Otherwise a Gateway Payment Request for 12.345 KWD is refused rather than charged a rounded amount.
:::

## URL helper

```python
from bwh_payments.bwh_payments.utils import get_localised_url
```

`get_localised_url(url)` swaps a leading language segment, such as `/en/`, for the shopper's current language, so they return to the page in their own language. Use it on every return URL you send the gateway.
