Digital Goods API

Draft Community Group Report,

This version:
https://wicg.github.io/digital-goods/
Issue Tracking:
GitHub
Editors:
(Google)
(Google)
Participate:
GitHub WICG/digital-goods (new issue, open issues)
Tests:
web-platform-tests digital-goods/ (ongoing work)

Abstract

The Digital Goods API allows web applications to get information about their digital products and their user’s purchases managed by a digital store. The user agent abstracts connections to the store and the Payment Request API is used to make purchases.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Usage Examples

Note: This section is non-normative.

1.1. Getting a service instance

Usage of the API begins with a call to window.getDigitalGoodsService(), which might only be available in certain contexts (eg. HTTPS, app, browser, OS). If available, the method can be called with a service provider URL. The method returns a promise that is rejected if the given service provider is not available.
if (window.getDigitalGoodsService === undefined) {
  // Digital Goods API is not supported in this context.
  return;
}
try {
  const digitalGoodsService = await
      window.getDigitalGoodsService("https://example.com/billing");
  // Use the service here.
  ...
} catch (error) {
  // Our preferred service provider is not available.
  // Use a normal web-based payment flow.
  console.error("Failed to get service:", error.message);
  return;
}

1.2. Querying item details

const details = await digitalGoodsService
    .getDetails(['shiny_sword', 'gem', 'monthly_subscription']);
for (item of details) {
  const priceStr = new Intl.NumberFormat(
      locale,
      {style: 'currency', currency: item.price.currency}
    ).format(item.price.value);
  AddShopMenuItem(item.itemId, item.title, priceStr, item.description);
}

The getDetails() method returns server-side details about a given set of items, intended to be displayed to the user in a menu, so that they can see the available purchase options and prices without having to go through a purchase flow.

The returned ItemDetails sequence can be in any order and might not include an item if it doesn’t exist on the server (i.e. there is not a 1:1 correspondence between the input list and output).

The item ID is a string representing the primary key of the items, configured in the store server. There is no function to get a list of item IDs; those have to be hard-coded in the client code or fetched from the developer’s own server.

The item’s price is a PaymentCurrencyAmount containing the current price of the item in the user’s current region and currency. It is designed to be formatted for the user’s current locale using Intl.NumberFormat, as shown above.

For more information on the fields in the ItemDetails object, refer to the [ItemDetails dictionary] section below.

1.3. Purchase using Payment Request API

const details = await digitalGoodsService.getDetails(['monthly_subscription']);
const item = details[0];
new PaymentRequest(
  [{supportedMethods: 'https://example.com/billing',
    data: {itemId: item.itemId}}]);

The purchase flow itself uses the Payment Request API. We don’t show the full payment request code here, but note that the item ID for any items the user chooses to purchase can be sent in the data field of a methodData entry for the given payment method, in a manner specific to the store.

1.4. Checking existing purchases

purchases = await digitalGoodsService.listPurchases();
for (p of purchases) {
  VerifyOnBackendAndGrantEntitlement(p.itemId, p.purchaseToken);
}

The listPurchases() method allows a client to get a list of items that are currently owned or purchased by the user. This might be necessary to check for entitlements (e.g. whether a subscription, promotional code, or permanent upgrade is active) or to recover from network interruptions during a purchase (e.g. item is purchased but not yet confirmed with a backend). The method returns item IDs and purchase tokens, which would typically be verified using a direct developer-to-provider API before granting entitlements.

1.5. Checking past purchases

const purchaseHistory = await digitalGoodsService.listPurchaseHistory();
for (p of purchaseHistory) {
  DisplayPreviousPurchase(p.itemId);
}

The listPurchaseHistory() method allows a client to list the latest purchases for each item type ever purchased by the user. Can include expired or consumed purchases. Some stores might not keep such history, in which case it would return the same data as the listPurchases() method.

1.6. Consuming a purchase

digitalGoodsService.consume(purchaseToken);

Purchases that are designed to be purchased multiple times usually need to be marked as "consumed" before they can be purchased again by the user. An example of a consumable purchase is an in-game powerup that makes the player stronger for a short period of time. This can be done with the consume() method.

It is preferable to use a direct developer-to-provider API to consume purchases, if one is available, in order to more verifiably ensure that a purchase was used up.

1.7. Use with subdomain iframes

<iframe
  src="https://sub.origin.example"
  allow="payment">
</iframe>

To indicate that a subdomain iframe is allowed to invoke the Digital Goods API, the allow attribute along with the "payment" keyword can be specified on the iframe element. Cross-origin iframes cannot invoke the Digital Goods API. The Permissions Policy specification provides further details and examples.

2. API definition

2.1. Extensions to the Window interface

partial interface Window {
  [SecureContext] Promise<DigitalGoodsService> getDigitalGoodsService(
      DOMString serviceProvider);
};

The Window object MAY expose a getDigitalGoodsService() method. User agents that do not support Digital Goods SHOULD NOT expose getDigitalGoodsService() on the Window interface.

Note: The above statement is designed to permit feature detection. If getDigitalGoodsService() is present, there is a reasonable expectation that it will work with at least one service provider.

2.1.1. getDigitalGoodsService() method

Note: The getDigitalGoodsService() method is called to determine whether the given serviceProvider is supported in the current context. The method returns a Promise that will be resolved with a DigitalGoodsService object if the serviceProvider is supported, or rejected with an exception if the serviceProvider is unsupported or any error occurs. The serviceProvider is usually a url-based payment method identifier.

When the getDigitalGoodsService(serviceProvider) method is called, run the following steps:
  1. Let document be the current settings object's relevant global object's associated Document.

  2. If document is not fully active, then return a promise rejected with an "InvalidStateError" DOMException.

  3. If document’s origin is not same origin with the top-level origin return a promise rejected with a "NotAllowedError" DOMException.

  4. If document is not allowed to use the "payment" permission return a promise rejected with a "NotAllowedError" DOMException.

  5. If serviceProvider is undefined or null or the empty string return a promise rejected with a TypeError.

  6. Let result be the result of performing the can make digital goods service algorithm given serviceProvider and document.

  7. If result is false return a promise rejected with an OperationError.

  8. Return a promise resolved with a new DigitalGoodsService.

2.1.2. Can make digital goods service algorithm

The can make digital goods service algorithm checks whether the user agent supports a given serviceProvider and document context.
  1. The user agent MAY return true or return false based on the serviceProvider or document or external factors.

Note: This allows for user agents to support different service providers in different contexts.

2.2. DigitalGoodsService interface

[Exposed=Window, SecureContext] interface DigitalGoodsService {

  Promise<sequence<ItemDetails>> getDetails(sequence<DOMString> itemIds);

  Promise<sequence<PurchaseDetails>> listPurchases();

  Promise<sequence<PurchaseDetails>> listPurchaseHistory();

  Promise<undefined> consume(DOMString purchaseToken);
};

dictionary ItemDetails {
  required DOMString itemId;
  required DOMString title;
  required PaymentCurrencyAmount price;
  ItemType type;
  DOMString description;
  sequence<DOMString> iconURLs;
  DOMString subscriptionPeriod;
  DOMString freeTrialPeriod;
  PaymentCurrencyAmount introductoryPrice;
  DOMString introductoryPricePeriod;
  [EnforceRange] unsigned long long introductoryPriceCycles;
};

enum ItemType {
  "product",
  "subscription",
};

dictionary PurchaseDetails {
  required DOMString itemId;
  required DOMString purchaseToken;
};

2.2.1. getDetails() method

When the getDetails(itemIds) method is called, run the following steps:
  1. If itemIds is empty, then return a promise rejected with a TypeError.

  2. Let result be the result of requesting information about the given itemIds from the digital goods service.

Note: This allows for different digital goods service providers to be supported by provider-specific behavior in the user agent.

  1. If result is an error, then return a promise rejected with an OperationError.

  2. For each itemDetails in result:

    1. itemDetails.itemId SHOULD NOT be the empty string.

    2. itemIds SHOULD contain itemDetails.itemId.

    3. itemDetails.title SHOULD NOT be the empty string.

    4. itemDetails.price MUST be a canonical PaymentCurrencyAmount.

    5. If present, itemDetails.subscriptionPeriod MUST be be a iso-8601 duration.

    6. If present, itemDetails.freeTrialPeriod MUST be be a iso-8601 duration.

    7. If present, itemDetails.introductoryPrice MUST be a canonical PaymentCurrencyAmount.

    8. If present, itemDetails.introductoryPricePeriod MUST be be a iso-8601 duration.

  3. Return a promise resolved with result.

Note: There is no requirement that the ordering of items in result matches the ordering of items in itemIds. This is to allow for missing or invalid items to be skipped in the output list.

2.2.2. listPurchases() method

When the listPurchases() method is called, run the following steps:
  1. Let result be the result of requesting information about the user’s purchases from the digital goods service.

Note: This allows for different digital goods service providers to be supported by provider-specific behavior in the user agent.

  1. If result is an error, then return a promise rejected with an OperationError.

  2. For each itemDetails in result:

    1. itemDetails.itemId SHOULD NOT be the empty string.

    2. itemDetails.purchaseToken SHOULD NOT be the empty string.

  3. Return a promise resolved with result.

2.2.3. listPurchaseHistory() method

When the listPurchaseHistory() method is called, run the following steps:
  1. Let result be the result of requesting information about the latest purchases for each item type ever purchased by the user.

  2. If result is an error, then return a promise rejected with an OperationError.

  3. For each itemDetails in result:

    1. itemDetails.itemId SHOULD NOT be the empty string.

    2. itemDetails.purchaseToken SHOULD NOT be the empty string.

  4. Return a promise resolved with result.

2.2.4. consume() method

Note: Consume in this context means to use up a purchase. The user is expected to no longer be entitled to the purchase after it is consumed.

When the consume(purchaseToken) method is called, run the following steps:
  1. If purchaseToken is the empty string, then return a promise rejected with a TypeError.

  2. Let result be the result of requesting the digital goods service to record purchaseToken as consumed.

Note: This allows for different digital goods service providers to be supported by provider-specific behavior in the user agent.

  1. If result is an error, then return a promise rejected with an OperationError.

  2. Return a promise resolved with undefined.

2.3. ItemDetails dictionary

This section is non-normative.

An ItemDetails dictionary represents information about a digital item from a serviceProvider.

2.4. PurchaseDetails dictionary

This section is non-normative.

A PurchaseDetails dictionary represents information about a digital item from a serviceProvider which the user has purchased at some point.

3. Permissions Policy integration

This specification defines a policy-controlled feature identified by the string "payment". Its default allowlist is 'self'.

Note: A document’s permissions policy determines whether any content in that document is allowed to get DigitalGoodsService instances. If disabled in any document, no content in the document will be allowed to use the getDigitalGoodsService() method (trying to call the method will throw).

4. Additional Definitions

The "payment" permission is a [permissions-policy] feature defined in the payment-request spec.

A canonical PaymentCurrencyAmount is a PaymentCurrencyAmount amount that can be run through the steps to check and canonicalize amount without throwing any errors or being altered.

iso-8601 is a standard for date and time representations.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Conformant Algorithms

Requirements phrased in the imperative as part of algorithms (such as "strip any leading space characters" or "return false and abort these steps") are to be interpreted with the meaning of the key word ("must", "should", "may", etc) used in introducing the algorithm.

Conformance requirements phrased as algorithms or specific steps can be implemented in any manner, so long as the end result is equivalent. In particular, the algorithms defined in this specification are intended to be easy to understand and are not intended to be performant. Implementers are encouraged to optimize.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[PAYMENT-REQUEST]
Marcos Caceres; Rouslan Solomakhin; Ian Jacobs. Payment Request API. URL: https://w3c.github.io/payment-request/
[PERMISSIONS-POLICY]
Ian Clelland. Permissions Policy. URL: https://w3c.github.io/webappsec-permissions-policy/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Informative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[PAYMENT-METHOD-ID]
Marcos Caceres. Payment Method Identifiers. URL: https://w3c.github.io/payment-method-id/

IDL Index

partial interface Window {
  [SecureContext] Promise<DigitalGoodsService> getDigitalGoodsService(
      DOMString serviceProvider);
};

[Exposed=Window, SecureContext] interface DigitalGoodsService {

  Promise<sequence<ItemDetails>> getDetails(sequence<DOMString> itemIds);

  Promise<sequence<PurchaseDetails>> listPurchases();

  Promise<sequence<PurchaseDetails>> listPurchaseHistory();

  Promise<undefined> consume(DOMString purchaseToken);
};

dictionary ItemDetails {
  required DOMString itemId;
  required DOMString title;
  required PaymentCurrencyAmount price;
  ItemType type;
  DOMString description;
  sequence<DOMString> iconURLs;
  DOMString subscriptionPeriod;
  DOMString freeTrialPeriod;
  PaymentCurrencyAmount introductoryPrice;
  DOMString introductoryPricePeriod;
  [EnforceRange] unsigned long long introductoryPriceCycles;
};

enum ItemType {
  "product",
  "subscription",
};

dictionary PurchaseDetails {
  required DOMString itemId;
  required DOMString purchaseToken;
};