---
title: "Public API"
space: "BWH Payments (Next)"
url: "https://docs.bwh.tech/bwh-payments-next/reference/public-api"
updated: "2026-10-12"
---

`bwh_payments.api` is the supported import surface for other apps. Import from here, not from doctype modules, which can move.

```python
from bwh_payments.api import (
	GatewayOption,
	create_payment,
	from_minor_units,
	get_gateway_options,
	get_payment_gateways,
	record_external_payment,
	record_external_refund,
	refund,
	resolve_payment_mode,
	sync_refunds,
	to_minor_units,
	validate_currency,
)
```

:::danger[Server-side only]
None of these functions check permissions. Never whitelist them as they are, and never pass request arguments to them without checking what the user may do.
:::

## create_payment

```python
def create_payment(
	ref_doctype: str,
	ref_docname: str,
	amount: float,
	currency: str,
	gateway: str,
	**fields,
) -> Document
```

Opens a gateway checkout for a document, and returns its **Gateway Payment Request**.

| Parameter | Description |
|---|---|
| `ref_doctype`, `ref_docname` | The document being paid for. It hears about the payment through `on_payment_authorized`. |
| `amount` | In major units: `49.99` |
| `currency` | ISO 4217 code, for example `USD` |
| `gateway` | A Payment Gateway Profile name, for example `Stripe` |
| `**fields` | Only `company`, `redirect_to` and `customer_*` fields. Anything else raises. |

Send the shopper to the returned request's `order_url`.

`redirect_to` is a path on your site, for example `/orders/ORD-0001`. When you set it, every gateway sends the shopper back through BWH Payments, which confirms the payment and then redirects to that path. [Return pages](/bwh-payments-next/get-started/return-pages) explains this. A full URL, `//host` or `/\host` is refused, so the redirect cannot leave your site.

Raises if the gateway refuses to open a checkout, or the amount cannot be stored exactly on this site (for example KWD without *Use Number Format From Currency*).

## on_payment_authorized

Not a function you call: a method BWH Payments calls on your document.

```python
def on_payment_authorized(self, status: str): ...
```

When a Gateway Payment Request becomes **Paid**, BWH Payments runs `ref_doc.run_method("on_payment_authorized", "Completed")` once. This is the `frappe/payments` convention. It runs inside the transaction that settled the payment: a webhook job, the shopper's return or a status check. Enqueue heavy work.

## on_refund_update

Not a function you call: a method BWH Payments calls on your document.

```python
def on_refund_update(self, refund): ...
```

BWH Payments calls it each time a **Gateway Refund** on your document's payment changes status: when the refund is created as `Pending`, and when it becomes `Processed` or `Failed`. `refund` is the Gateway Refund document:

| Field | Holds |
|---|---|
| `status` | `Pending`, `Processed` or `Failed` |
| `amount` | The refunded amount, in major units |
| `refund_id` | The gateway's refund id. It is empty until the gateway answers. |
| `reference` | The `reference` you passed to `refund()`. It is empty for a refund raised on the gateway's dashboard. |
| `payment_request` | The Gateway Payment Request |

Use `reference` to find your own record of the refund. A refund raised on the gateway's dashboard reaches you with no reference, so create a record for it.

## get_payment_gateways

```python
def get_payment_gateways() -> list[str]
```

The names of the enabled Payment Gateway Profiles, for example `["Stripe", "Tabby"]`. The list is cached in Redis for one hour, and is cleared when a profile changes.

## get_gateway_options

```python
def get_gateway_options(currency: str | None = None) -> list[GatewayOption]
```

The enabled gateways that can charge `currency`, with their logos. Without `currency`, it returns every enabled gateway.

```python
class GatewayOption(BaseModel):
	name: str  # the Payment Gateway Profile name
	title: str
	logo: str | None  # a site path, for example /assets/bwh_payments/images/gateways/razorpay.svg
	logo_dark: str | None  # for a dark page
```

## validate_currency

```python
def validate_currency(gateway: str, currency: str) -> None
```

Throws if the site has no such currency, or if `gateway` cannot charge in it. For example, Tabby charges only SAR, AED, KWD, BHD and QAR. Call it before you create anything for the payment.

## resolve_payment_mode

```python
def resolve_payment_mode(payment_mode: str) -> str | None
```

Matches a mode sent by the browser, such as `"stripe"`, to an enabled profile name, ignoring case and spaces at either end. Returns `None` if no enabled gateway matches. Use it to check a shopper's choice before you call `create_payment`.

## refund

```python
def refund(payment_request: str, amount: float | None = None, reference: str | None = None) -> Document
```

Refunds `amount`, or the rest of the payment when `amount` is `None`, and returns the **Gateway Refund**. `reference` is your own id for the refund. It comes back on every `on_refund_update`.

| The gateway answers | Gateway Refund status |
|---|---|
| The refund went through | `Processed` |
| The refund is pending | `Pending`, until a refund webhook or `sync_refunds` settles it |
| It refuses the refund | `Failed`, with the error, and `refund` raises |

A refund larger than what is left is refused. A `Pending` refund counts against what is left, so two refunds at the same time cannot refund more than the payment.

:::note[It commits]
`refund` commits the `Pending` refund before it calls the gateway. That commit keeps the reservation in place while the gateway answers. Anything your request wrote before the call is committed with it.
:::

## sync_refunds

```python
def sync_refunds(payment_request: str) -> None
```

Asks the gateway about each `Pending` refund on the payment, and applies the answer. Use it when a refund webhook did not arrive. Stripe and Razorpay can report a refund. Telr and Tabby settle refunds at once, so they never leave one `Pending`.

## record_external_payment, record_external_refund

```python
def record_external_payment(
	ref_doctype: str,
	ref_docname: str,
	amount: float,
	currency: str,
	gateway: str,
	status: str,
	order_ref: str,
	transaction_ref: str | None = None,
) -> Document

def record_external_refund(
	payment_request: str, refund_id: str, status: str, amount: float, reference: str | None = None
) -> Document
```

Record a payment, or a refund, that another app took. Use them to move history into BWH Payments. Both:

- open no checkout and call no gateway
- do not call `on_payment_authorized` or `on_refund_update`
- return the existing record when you call them again with the same `order_ref` or `refund_id`

`transaction_ref` is the gateway's id for the settled payment. A refund webhook for a refund raised on the gateway's dashboard finds the payment through it. For Razorpay, a payment whose `order_ref` starts with `pay_` is refunded against that payment id, so old payments stay refundable. [Move from frappe/payments](/bwh-payments-next/get-started/move-from-frappe-payments) shows both in a patch.

## to_minor_units, from_minor_units

```python
to_minor_units(49.99, "USD")  # 4999
to_minor_units(12.345, "KWD")  # 12345
from_minor_units(4999, "USD")  # 49.99
```

Exponents follow ISO 4217, not the site's Currency settings. JPY has 0 decimals, KWD has 3.

## Gateway Payment Request methods

| Method | Does |
|---|---|
| `sync_status()` | Reads the real status from the gateway. Whitelisted. |
| `sync_refunds()` | The same as `api.sync_refunds`. Whitelisted, and needs write permission on the request. |
| `create_refund(amount=None, reference=None)` | The same as `api.refund`, on the document |
| `refund(amount=None)` | Refunds and returns a summary dict. Kept for older callers. |
| `release_if_unpaid()` | Cancels an open checkout. Returns `True` once the payment can no longer be paid. |

`refund`, `create_refund` and `release_if_unpaid` are not whitelisted. Frappe's `run_doc_method` checks only read permission, so a whitelisted refund would let any reader refund the payment.

## Testing helpers

`bwh_payments.testing` lets your app's tests run the real payment, refund and webhook code against an in-memory gateway. [Test with stubs](/bwh-payments-next/build/test-with-stubs#test-an-app-that-takes-payments) shows how to use it.
