PayNow

Carts and Checkout

Your store decides what the customer is buying and hands it to PayNow as a checkout session. PayNow handles payment and tax.

Prices

Every product from GET /v1/store/products carries a pricing object next to the base price. Render pricing.price_final, not price.

FieldMeaning
priceThe base price you set in the dashboard, in minor units of the store currency
pricing.price_originalThe price before any sale, after regional pricing and tax-inclusive adjustment for the customer's location
pricing.price_finalWhat the customer will actually pay per unit
pricing.active_saleThe sale applied, if any, with its type and amount
pricing.regional_pricingThe regional override in effect, if you have configured one

Amounts are integers in minor units (1999 is 19.99), and currency is the ISO code they are in. In regions where PayNow shows tax-inclusive prices, price_final is higher than price by the local rate.

This is expected behaviour - it is the number the customer sees at checkout. Forward the customer's IP and Country Code (If available) as described in the Building a Storefront.

The allow_subscription and allow_one_time_purchase flags tell you which billing modes to offer. When both are true, let the customer choose and send the choice as subscription. subscription_interval_value and subscription_interval_scale describe the renewal period.

Two ways to collect lines

A server-side cart lives in PayNow and follows the customer token, so it survives page loads and devices. Use it when customers buy several things at once.

A direct checkout skips the cart - you build the checkout session from a list of lines in one call. Use it for a "Buy now" button, or when your own app already tracks what the customer picked.

Both produce the same checkout session, and you can offer both at once.

The cart

PUT /v1/store/cart/lines adds or updates one line, keyed by query parameters. GET /v1/store/cart returns the lines with their pricing, and DELETE /v1/store/cart empties it.

TypeScript
await storefront.cart.addLine({ product_id: productId, quantity: 1, subscription: "true", increment: "true" });

The behaviours to know:

  • increment decides whether quantity is added to the existing line (true) or replaces it (false or omitted). Use true for an "Add to cart" button and false for a quantity stepper.
  • Setting quantity to 0 with increment=false removes the line. There is no separate delete-line endpoint.
  • A line is identified by the product plus subscription, the gift target and the selected game server. The same product added once as a subscription and once as a one-time purchase makes two lines. To change a specific line, send the same identifying parameters again.
  • Subscription lines always have quantity 1.
  • The response is 204 with no body. Read the cart afterwards for totals.

The cart holds product IDs and quantities, not prices. Prices are computed when you read the cart and again when you create the checkout, so a sale that starts or ends in between is applied correctly.

Direct checkout

POST /v1/checkouts takes the lines inline. Each line has the same fields as a cart line: product_id, quantity, subscription, selected_gameserver_id, gift_to, custom_variables.

TypeScript
const session = await storefront.checkout.createCheckoutSession({
  lines: [{ product_id: productId, quantity: 1, subscription: false }],
  return_url: "https://store.example.com/checkout/complete",
  cancel_url: "https://store.example.com/cart",
  auto_redirect: true,
});

For a cart, POST /v1/store/cart/checkout takes the same options minus lines.

The checkout session

Either call returns { id, token, url }.

  • url is the hosted checkout page for this session. Sending the browser there is the simplest integration.
  • token is what PayNow.js needs to open the same checkout in an overlay on your page.
  • return_url is where the customer lands after paying, and cancel_url where they land if they back out. With auto_redirect: true the hosted checkout sends them to return_url on its own; with false it shows a button instead.
  • coupon_id, promo_codes and affiliate_code pre-apply discounts and attribution. The customer can still enter codes on the checkout page.

Sessions are single-use and short-lived. Create one when the customer clicks pay, not when the page loads.

Opening it - redirect or embed

Redirecting to url works everywhere. Embedding with PayNow.js keeps the customer on your page, which is the better experience on desktop and the reason most custom stores use it.

TypeScript
import PayNowJS from "@paynow-gg/paynow.js";

PayNowJS.checkout.on("completed", ({ orderId }) => {
  PayNowJS.checkout.close();
  window.location.assign(`/checkout/complete?order=${orderId}`);
});

PayNowJS.checkout.open({ token: session.token, theme: "dark" });

Two behaviours to plan for:

  • Narrow windows redirect. When the window is narrower than 800px, open() navigates to the hosted checkout instead of rendering an overlay, unless you pass disableRedirect: true. The overlay is a fixed-size iframe, so the default is what you want on phones. Because the customer leaves your page, set return_url on the session even when you intend to embed.
  • Completion is an event, not a redirect. In the overlay, listen for completed and take the customer to your success page yourself. closed fires when they dismiss the overlay without paying, so refresh the cart in case they changed it inside the checkout.

Create the session with auto_redirect: false when you know you will embed, and true when you know you will redirect. If you decide at click time based on window width, pass the decision to your server and set auto_redirect accordingly.

The library is browser-only. In a server-rendered framework, import it inside a client component or with a dynamic import, and only call open() from a click handler.

After payment

Delivery starts as soon as the payment has been completed - the customer does not need to do anything on your site.

Your success page can read the order with GET /v1/store/customer/orders/{orderId} using the customer token to show what they bought.

For anything that must happen on your backend when an order completes, such as granting access in your own database, use the ON_ORDER_COMPLETED webhook rather than the browser event. The browser event can be missed if the tab closes - the webhook cannot.