# Payment Request API

> new PaymentRequest(methodData, details, options), canMakePayment(), show(), complete(), every exception the W3C spec defines, and a hosted-checkout fallback.

`PaymentRequest` hands the checkout sheet to the browser: the page lists the payment methods it accepts, the line items and total, and which contact fields it needs, and the browser collects a credential from a stored card, Apple Pay, Google Pay, or an installed payment handler and returns it as a `PaymentResponse`. The page never renders a card form and never sees raw card data unless the chosen method returns it.

Chrome 60 on desktop and Chrome 53 on Android shipped the constructor, Edge 15 and Safari 11.1 followed, and Android WebView added it in 136 (BCD `api.PaymentRequest`). Safari implements it for one method only, `https://apple.com/apple-pay`. Firefox 55 shipped an implementation behind `dom.payments.request.enabled` and `dom.payments.request.supportedRegions` and has not enabled it by default in any release.

## Syntax

```js
new PaymentRequest(methodData, details)
new PaymentRequest(methodData, details, options)

request.canMakePayment()
request.show()
request.show(detailsPromise)
request.abort()

response.complete()
response.complete(result)
response.retry(errorFields)
```

`canMakePayment()` returns a `Promise<boolean>` without showing UI. `show()` returns a `Promise<PaymentResponse>` that fulfils when the user authorises payment; `abort()` and `complete()` return `Promise<undefined>`, and `retry()` returns a promise that fulfils when the user has corrected the flagged fields. The constructor is `[SecureContext]` and requires the `payment` Permissions Policy, whose default allowlist is `'self'`; a cross-origin iframe needs `allow="payment"`. A request object is single-use: after `show()` has been called once its `[[state]]` leaves `"created"`, and a second `show()` or a later `canMakePayment()` rejects.

## Parameters

The constructor takes two required dictionaries and one optional one.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `methodData` | `sequence<PaymentMethodData>` | Yes | One entry per accepted method. `supportedMethods` is a payment method identifier, either a URL such as `https://google.com/pay` or `https://apple.com/apple-pay` or a standardised string such as `secure-payment-confirmation`; `data` is an object the method's own specification defines (merchant ids, allowed networks). The list must not be empty or contain the same identifier twice. |
| `details` | `PaymentDetailsInit` | Yes | `total` (`PaymentItem`: `label`, `amount`) is required; `amount` is a `PaymentCurrencyAmount` with an ISO 4217 `currency` code and a decimal string `value` such as `"29.99"`. Optional members: `id` (generated as a UUID when omitted), `displayItems`, `shippingOptions` (`id`, `label`, `amount`, `selected`), and `modifiers` (`supportedMethods`, `total`, `additionalDisplayItems`, `data`) that change the totals for one method. |
| `options` | `PaymentOptions` | No | `requestPayerName`, `requestPayerEmail`, `requestPayerPhone`, `requestShipping` (booleans, default `false`) and `shippingType` (`"shipping"`, `"delivery"`, or `"pickup"`, default `"shipping"`), which only changes the label the sheet shows. |

`show(detailsPromise)` accepts an optional `Promise<PaymentDetailsUpdate>` so the sheet can open before the server has finished pricing the cart. `complete(result)` takes `"success"`, `"fail"`, or `"unknown"` (the default). `retry(errorFields)` takes a `PaymentValidationErrors` dictionary with `error`, `payer` (`name`, `email`, `phone`), `shippingAddress` (an `AddressErrors` record keyed by address field), and `paymentMethod`.

## Exceptions

The specification defines the following failures. Where Chrome attaches a fixed message it is quoted from `components/payments/core/error_strings.cc`.

| Exception | Condition |
|---|---|
| `SecurityError` | Constructor: the document is not allowed to use the `payment` Permissions Policy. `show()`: the browser requires transient activation and finds none, or rate-limits the call. |
| `TypeError` | Constructor: `methodData` is empty; `details.total` is missing; an `amount.value` is not a valid decimal monetary string or the total is negative; two shipping options share an `id`. Chrome adds limits of 1024 shipping options, 1024 modifiers, and 1024 characters for `details.id`. |
| `RangeError` | Constructor: a payment method identifier is not a valid URL or standardised string, the same identifier appears twice, or a `currency` is not a well-formed ISO 4217 code. |
| `InvalidStateError` | `show()`: the document is not fully active, or `[[state]]` is not `"created"` (Chrome: `Already called show() once`). `abort()`: no `show()` or `retry()` is in progress, an `abort()` is already pending, or the browser cannot abort the current interaction. `canMakePayment()`: `[[state]]` is not `"created"` or the document is not fully active. `complete()` and `retry()`: the response is already complete or a `retry()` is pending. |
| `AbortError` | `show()`: the document is not visible, another payment sheet is already showing (Chrome: `Another PaymentRequest UI is already showing in a different tab or window.`), the user dismisses the sheet (Chrome: `User closed the Payment Request UI.`), or the page calls `abort()` (Chrome: `The website has aborted the payment`). `complete()`: the document stopped being fully active while the sheet was open. |
| `NotSupportedError` | `show()`: no listed method is supported. Chrome also rejects on origins that are neither `localhost`, `file://`, nor a cryptographic scheme with `Only localhost, file://, and cryptographic scheme origins allowed.` |
| `NotAllowedError` | `canMakePayment()`: the browser applies an anti-fingerprinting quota to repeated queries with varying method lists. |
| `OperationError` | `show()`: the selected payment handler reports an internal error, for example the OS terminated it. |

`canMakePayment()` resolving `false` is not an exception: it means no listed method is available on this device, which is the signal to show a hosted checkout instead.

:::observed
Closing Chrome's payment sheet rejects the `show()` promise with `AbortError: User closed the Payment Request UI.`, and calling `show()` a second time on the same object throws `InvalidStateError: Already called show() once`. Constructing a request inside a cross-origin iframe that lacks `allow="payment"` throws `SecurityError: Must be in a top-level browsing context or an iframe needs to specify allow="payment" explicitly`. A second `show()` in the same page load without a user gesture is rejected with the message `PaymentRequest.show() calls after the first (per page load) require either transient user activation or delegated payment request capability.` The strings are defined in Chromium's [`error_strings.cc`](https://chromium.googlesource.com/chromium/src/+/main/components/payments/core/error_strings.cc) and [`payment_request.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/payments/payment_request.cc) (chromium.googlesource.com).
:::

## Examples

The examples use Google Pay as the method identifier; swap in your processor's identifier and `data` object. Each one checks for the constructor first and falls back to a hosted checkout page when it is missing or no method is available.

### Showing a wallet button only when `canMakePayment()` resolves true

Build the request at page load, ask whether any listed method is available, and reveal the native button only on `true`. A `false` result or a missing constructor leaves the ordinary "Continue to checkout" link in place, which navigates to the processor's hosted page.

```js
const methodData = [{
  supportedMethods: "https://google.com/pay",
  data: { environment: "PRODUCTION", apiVersion: 2, apiVersionMinor: 0 /* merchant config */ },
}];
const details = {
  total: { label: "Total", amount: { currency: "USD", value: "29.99" } },
};

async function prepareCheckout() {
  const hosted = document.querySelector("#hosted-checkout");
  const wallet = document.querySelector("#wallet-button");

  if (!("PaymentRequest" in window)) {
    hosted.hidden = false;
    return;
  }

  const request = new PaymentRequest(methodData, details);
  let available = false;
  try {
    available = await request.canMakePayment();
  } catch (err) {
    console.warn(`${err.name}: ${err.message}`); // rejected when the browser's query quota is exceeded
  }

  wallet.hidden = !available;
  hosted.hidden = available;
}

prepareCheckout();
```

Keep the `request` object you tested with: `canMakePayment()` does not consume it, so the click handler in the next example can call `show()` on the same instance.

### Capturing on the server, then closing the sheet with `complete()`

`show()` must run inside the click handler so transient activation is present. Send `response.toJSON()` to the server, and call `complete("success")` or `complete("fail")` as soon as the server answers; the sheet stays open until you do, and Chrome closes it on its own after a timeout as if `complete()` had been called with no argument.

```js
const wallet = document.querySelector("#wallet-button");

wallet.addEventListener("click", async () => {
  const request = new PaymentRequest(methodData, details);
  let response;
  try {
    response = await request.show();
  } catch (err) {
    if (err.name === "AbortError") return; // user closed the sheet
    location.assign("/checkout/hosted"); // any other rejection: fall back to the hosted page
    return;
  }

  const result = await fetch("/api/charge", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(response.toJSON()),
  });

  await response.complete(result.ok ? "success" : "fail");
  if (result.ok) location.assign("/checkout/thanks");
});
```

`complete("fail")` shows the browser's failure state and closes the sheet; the page is then responsible for telling the user what to do next.

### Repricing when the shipping address changes

With `requestShipping: true` the sheet lets the user pick an address and fires `shippingaddresschange` on the request. Call `event.updateWith()` synchronously inside the listener with a promise for the new details; the sheet shows a spinner until it settles. The specification lets the browser redact parts of the address until the user authorises, so compute shipping from `country`, `region`, and `postalCode` rather than the street lines.

```js
const request = new PaymentRequest(methodData, {
  total: { label: "Total", amount: { currency: "USD", value: "29.99" } },
  shippingOptions: [
    { id: "ground", label: "Ground (5 days)", amount: { currency: "USD", value: "0.00" }, selected: true },
  ],
}, { requestShipping: true });

request.addEventListener("shippingaddresschange", (event) => {
  const { country, postalCode } = event.target.shippingAddress;
  event.updateWith(
    fetch(`/api/shipping?country=${country}&postal=${postalCode}`)
      .then((r) => r.json())
      .then((quote) => ({
        total: { label: "Total", amount: { currency: "USD", value: quote.total } },
        shippingOptions: quote.options,
      }))
  );
});
```

Returning `shippingOptions: []` from the quote tells the sheet that nothing can be shipped to that address; add `error: "We do not ship to this region"` to the update so the user sees why.

## See also

- [Digital Goods API](/reference/capabilities/digital-goods/), the Play Billing method that plugs into `PaymentRequest` inside a Trusted Web Activity
- [Trusted Web Activity (TWA)](/reference/installation/twa/)
- [Monetize a PWA](/guides/monetization/)
- [Payment Request API: show() method](https://www.w3.org/TR/payment-request/#show-method) (w3.org)
- [Payment Request API: constructor](https://www.w3.org/TR/payment-request/#constructor) (w3.org)
- [Introducing the Payment Request API for Apple Pay](https://webkit.org/blog/8182/introducing-the-payment-request-api-for-apple-pay/) (webkit.org)
- [How the Payment Request API works](https://web.dev/articles/how-payment-request-api-works) (web.dev)
- [Chromium payments error strings](https://chromium.googlesource.com/chromium/src/+/main/components/payments/core/error_strings.cc) (chromium.googlesource.com)