# Digital Goods API

> window.getDigitalGoodsService() exposes Google Play Billing to a PWA in a Trusted Web Activity: getDetails(), listPurchases(), consume(), errors, fallbacks.

The Digital Goods API lets a web app read its store catalogue and the user's purchases from a store back end: `window.getDigitalGoodsService(provider)` returns a `DigitalGoodsService` whose `getDetails()`, `listPurchases()`, `listPurchaseHistory()`, and `consume()` talk to that provider. The purchase itself runs through the Payment Request API with the same provider URL as the payment method.

Chrome 101 on Android and ChromeOS is the only implementation, and only while the PWA runs inside a Trusted Web Activity published on Google Play, with `https://play.google.com/billing` as the provider (Chrome for Developers guide). MDN's browser-compat-data has no `DigitalGoodsService` entry; Firefox and Safari do not expose `getDigitalGoodsService`. The specification is a WICG draft and the method is absent from `Window` wherever the API is unsupported, so a plain `in` check is the complete feature test.

## Syntax

```js
window.getDigitalGoodsService(serviceProvider)

digitalGoodsService.getDetails(itemIds)
digitalGoodsService.listPurchases()
digitalGoodsService.listPurchaseHistory()
digitalGoodsService.consume(purchaseToken)
```

`getDigitalGoodsService()` returns `Promise<DigitalGoodsService>`. `getDetails()` returns `Promise<sequence<ItemDetails>>`, `listPurchases()` and `listPurchaseHistory()` return `Promise<sequence<PurchaseDetails>>`, and `consume()` returns `Promise<undefined>`. Everything is `[SecureContext]` and `Window`-only; no method needs a user gesture, but the document must be same-origin with the top-level page and allowed to use the `payment` Permissions Policy feature (a same-site iframe needs `allow="payment"`).

## Parameters

Three methods take an argument; the two list methods take none.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `serviceProvider` | `DOMString` | Yes | A URL-based payment method identifier; for Google Play it is `"https://play.google.com/billing"`. `undefined`, `null`, or `""` rejects. |
| `itemIds` | `sequence<DOMString>` | Yes | Product or subscription IDs as configured in the store console. Must be non-empty; the result may omit unknown IDs and may be in any order. |
| `purchaseToken` | `DOMString` | Yes | A token returned by `listPurchases()` or by the `PaymentResponse.details` of the purchase. Must be non-empty. |

`getDetails()` resolves `ItemDetails` dictionaries with these members:

| Member | Type | Required | Description |
|---|---|---|---|
| `itemId` | `DOMString` | Yes | The ID that was requested. |
| `title` | `DOMString` | Yes | Localized name for display. |
| `price` | `PaymentCurrencyAmount` | Yes | `{ currency, value }` in the user's region; format with `Intl.NumberFormat`. |
| `type` | `"product"` or `"subscription"` | No | Item kind. |
| `description` | `DOMString` | No | Localized long description. |
| `iconURLs` | `sequence<DOMString>` | No | Item images. |
| `subscriptionPeriod` | `DOMString` | No | ISO 8601 duration, subscriptions only. |
| `freeTrialPeriod` | `DOMString` | No | ISO 8601 duration of the free trial. |
| `introductoryPrice` | `PaymentCurrencyAmount` | No | Price during the introductory period. |
| `introductoryPricePeriod` | `DOMString` | No | ISO 8601 duration of that period. |
| `introductoryPriceCycles` | `unsigned long long` | No | Number of billing cycles the introductory price applies. |

`listPurchases()` and `listPurchaseHistory()` resolve `PurchaseDetails` dictionaries with two required members, `itemId` and `purchaseToken`; `listPurchaseHistory()` may include consumed or expired purchases, and a store without history returns the same list as `listPurchases()`.

## Exceptions

Every method rejects rather than throws. Chromium's `OperationError` messages are the literal Play Billing response codes, which is the only way to tell "item already owned" from "store app missing".

| Exception | Condition |
|---|---|
| `InvalidStateError` | `getDigitalGoodsService()` from a document that is not fully active. |
| `NotAllowedError` | `getDigitalGoodsService()` from a document that is not same-origin with the top-level origin, or that the `payment` Permissions Policy does not allow. |
| `TypeError` | `getDigitalGoodsService()` with an `undefined`, `null`, or empty provider; `getDetails([])`; `consume("")`. |
| `OperationError` | `getDigitalGoodsService()` when the provider is unsupported in this context (outside a TWA, or Play Billing not enabled in the package); any of the four service methods when the store reports an error. Chromium message strings: `unsupported payment method`, `unsupported context`, `error`, `itemAlreadyOwned`, `itemNotOwned`, `itemUnavailable`, `clientAppUnavailable`, `clientAppError`. |

A `PaymentRequest.show()` on the same provider rejects with `AbortError` when the user backs out of the Play purchase sheet; that is the Payment Request API's exception, not this one's.

:::observed
In Chrome on Android, `window.getDigitalGoodsService('')` rejects with `TypeError: Empty payment method`, and a call from a cross-site iframe rejects with `NotAllowedError: Access denied from cross-site frames`; an iframe without `allow="payment"` gets `NotAllowedError: Payment permissions policy not granted`. When the provider is refused, the `OperationError` message is one of the code strings above, for example `OperationError: unsupported context` in a normal browser tab outside a Trusted Web Activity. The strings are in Chromium's [`dom_window_digital_goods.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/payments/goods/dom_window_digital_goods.cc) and [`digital_goods_type_converters.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/payments/goods/digital_goods_type_converters.cc) (chromium.googlesource.com).
:::

## Examples

Every example starts from one helper that resolves `null` outside a Play-backed Trusted Web Activity, so the rest of the app can show its web checkout (Stripe, PayPal, or a plain form) instead.

### Getting the Play service or falling back to web checkout

The `in` check tells you the browser implements the API; only the resolved promise tells you Play Billing is reachable from this package. Treat either failure the same way.

```js
const PLAY_BILLING = 'https://play.google.com/billing';

async function getPlayService() {
  if (!('getDigitalGoodsService' in window)) return null;
  try {
    return await window.getDigitalGoodsService(PLAY_BILLING);
  } catch (err) {
    console.info(`Play Billing unavailable (${err.name}: ${err.message})`);
    return null;
  }
}

const service = await getPlayService();
document.querySelector('#web-checkout').hidden = Boolean(service);
document.querySelector('#play-checkout').hidden = !service;
```

Logging `err.message` keeps the Play response code (`unsupported context`, `clientAppUnavailable`) in your telemetry without changing what the user sees.

### Rendering prices from getDetails() in the user's locale

`price` arrives as a `PaymentCurrencyAmount`; format it with `Intl.NumberFormat` instead of concatenating `value` and `currency` yourself. Missing IDs are simply absent from the result, so iterate the response rather than the request.

```js
async function renderCatalogue(service, ids) {
  if (!service) return renderWebPrices(ids); // fetched from your own server
  const items = await service.getDetails(ids);
  const money = new Intl.NumberFormat(navigator.language, { style: 'currency', currency: items[0]?.price.currency ?? 'USD' });
  for (const item of items) {
    const row = document.createElement('li');
    row.textContent = `${item.title}: ${money.format(item.price.value)}`;
    if (item.type === 'subscription' && item.freeTrialPeriod) {
      row.textContent += ` (trial ${item.freeTrialPeriod})`;
    }
    document.querySelector('#catalogue').append(row);
  }
}
```

A trial period such as `P7D` is an ISO 8601 duration string; format it yourself, `Intl` has no duration formatter for it in Chrome 101.

### Buying with PaymentRequest, then restoring entitlements with listPurchases()

The purchase flow is Payment Request with `supportedMethods` set to the provider URL and the SKU in `data`; Play ignores the `total` you must still supply. Acknowledge the `purchaseToken` on your server: Google Play refunds and revokes any purchase left unacknowledged for three days (Chrome for Developers guide). On later launches, `listPurchases()` returns what the user already owns, including purchases made on another device.

```js
async function buy(service, sku) {
  if (!service) return startWebCheckout(sku);
  const request = new PaymentRequest(
    [{ supportedMethods: PLAY_BILLING, data: { sku } }],
    { total: { label: 'Total', amount: { currency: 'USD', value: '0' } } },
  );
  try {
    const response = await request.show();
    const { purchaseToken } = response.details;
    const verified = await postJson('/play/acknowledge', { sku, purchaseToken });
    await response.complete(verified ? 'success' : 'fail');
    if (verified) await service.consume(purchaseToken); // consumables only
  } catch (err) {
    if (err.name !== 'AbortError') throw err; // the user left the Play purchase sheet
  }
}

async function restoreEntitlements(service) {
  if (!service) return [];
  const owned = await service.listPurchases();
  return (await postJson('/play/verify', owned)).filter((p) => p.valid).map((p) => p.itemId);
}
```

`consume()` is only for items designed to be bought repeatedly: the specification states that the user is expected to lose the entitlement once a purchase is consumed, so do not call it on a subscription or a permanent upgrade.

## See also

- [Payment Request API](/reference/capabilities/payment-request/), the purchase half of the flow
- [Trusted Web Activity (TWA): PWAs in the Play Store](/reference/installation/twa/), the container the API requires
- [Monetize a PWA](/guides/monetization/)
- [Digital Goods API: getDigitalGoodsService() method](https://wicg.github.io/digital-goods/#getdigitalgoodsservice-method) (wicg.github.io)
- [Receive Payments via Google Play Billing with the Digital Goods API and the Payment Request API](https://developer.chrome.com/docs/android/trusted-web-activity/receive-payments-play-billing/) (developer.chrome.com)
- [Chrome Platform Status: Digital Goods API](https://chromestatus.com/feature/5339955595313152) (chromestatus.com)