For AI agents and LLMs: a structured documentation index is available at /llms.txt. Every page has a Markdown sibling — append .md to any URL.

Skip to content
Docs

Build a pricing page

Get a step-by-step overview of how to build a pricing page that displays localized prices, including taxes and discount calculation. Open a checkout when a prospect wants to sign up.

AI summary

Build a dynamic pricing page that displays localized prices for your products using Paddle.PricePreview(), with a toggle to switch between monthly and annual billing cycles.

  • • Call Paddle.PricePreview({ items }) with an array of priceId and quantity objects; it returns localized prices including currency symbols without requiring any manual currency math.
  • • Unlike opening a checkout, pricing preview accepts prices with different billing periods and trial periods in the same request — making it safe to mix monthly and annual prices.
  • • Use item.formattedTotals.subtotal from result.data.details.lineItems to get the display-ready localized price string for each product tier.

Pricing pages show prospects the subscription plans, addons, or one-time charges that you offer and how much they cost. They're one of the most important pages on your website, and typically play a key part in customer conversion.

You can use Paddle.js to build pricing pages that show prospects prices that are relevant for their country, displayed in their local currency with estimated taxes. If you're running a sale or promo, you can calculate discounts too.

Explore the code for this tutorial and test right away using our pricing page pen.

How it works

Paddle Checkout automatically shows the correct prices for a customer using geolocation to estimate where a customer is buying from. Customers see prices in their local currency, with taxes estimated for their country or region.

You can use the Paddle.PricePreview() method in Paddle.js to get localized prices for pricing pages or other pages on your website. This means you can show the same information on your pricing page that a customer sees when they open checkout to subscribe.

You don't need to do any calculations yourself or manipulate returned data. Paddle returns totals formatted for the country or region you're working with, including the currency symbol.

What are we building?

In this tutorial, we'll create a simple, three-tier pricing page. It includes a toggle to switch between monthly and annual plans.

We'll learn how to:

  • Include and set up Paddle.js using a client-side token
  • Build an items list that we can send to Paddle.PricePreview()
  • Present and update prices on our page
  • Toggle between monthly and annual prices for products

If you like, you can view on CodePen and follow along.

Before you begin

Choose a pricing page

This tutorial walks through creating a simple pricing page. You can also create a cart-style pricing page for more advanced implementations using transaction previews.

Simple pricing page

Pass a batch of price IDs and location information to Paddle.js. Paddle returns localized pricing for each item.

Recommended for most pricing pages. Simply returns localized prices.

Requests and responses mirror the preview prices operation.

Returns item totals formatted for the currency and region, including currency code.

Response only includes calculations for each item included in the request.

You can send prices with different billing periods and trial periods.

Cart-style pricing page

Send a batch of price IDs and location information to Paddle.js. Paddle returns a preview of a transaction.

Recommended for more advanced pricing pages where users can build their own plans.

Requests and responses mirror the preview a transaction operation.

Returns item totals in the lowest denomination for a currency (for example, cents for USD).

Response includes calculations for line items and grand totals.

Requests mirror creating a transaction, so billing and trial periods must match for all items.

Create products and prices

You'll need to create a product and at least one related price for the items that you want to include on your pricing page.

Localize prices

To show localized prices, turn on automatic currency conversion or add price overrides to your prices.

Overview

To build a pricing page:

  1. Include and initialize Paddle.js
    Add Paddle.js to your app or website, so you can securely work with your product catalog.
  2. Pass prices to Paddle.js
    Build a pricing preview request body and pass to Paddle.PricePreview().
  3. Update your page based on the response
    Present information returned by Paddle.js to a customer on your page.

Include and initialize Paddle.js

Paddle.js is a lightweight JavaScript library that lets you build rich, integrated subscription billing experiences using Paddle. We can use Paddle.js to securely work with products and prices in our Paddle system, as well as opening checkouts and capturing payment information.

Include Paddle.js script

Start with a blank webpage, or an existing page on your website. Then, include Paddle.js by adding this script to the <head>:

HTML
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>

Set environment Optional

We recommend signing up for a sandbox account to test and build your integration, then switching to a live account later when you're ready to go live.

If you're testing with the sandbox, call Paddle.Environment.set() and set your environment to sandbox:

HTML
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
<script type="text/javascript">
Paddle.Environment.set("sandbox");
</script>

Pass a client-side token

Next, go to Paddle > Developer tools > Authentication and create a client-side token. Client-side tokens let you interact with the Paddle platform in frontend code, like webpages or mobile apps. They have limited access to the data in your system, so they're safe to publish.

In your page, call Paddle.Initialize() and pass your client-side token as token. For best performance, do this just after calling Paddle.Environment.set(), like this:

HTML
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
<script type="text/javascript">
Paddle.Environment.set("sandbox");
Paddle.Initialize({
token: "test_7d279f61a3499fed520f7cd8c08" // replace with a client-side token
});
</script>

Pass prices to Paddle.js

Next, we'll pass prices to Paddle.js so that we can get localized prices for them. When previewing prices, Paddle returns calculated totals for line items only — it doesn't include grand totals. This means that we can include prices with different billing cycles and trial periods in our request, unlike when opening a checkout or creating a transaction.

Define lists of prices

Our page includes four prices:

graph TD
  Starter["Starter"]
  Pro["Pro"]
  Starter_Monthly["Starter (monthly)"]
  Starter_Yearly["Starter (yearly)"]
  Pro_Monthly["Pro (monthly)"]
  Pro_Yearly["Pro (yearly)"]

  Starter --> Starter_Monthly
  Starter --> Starter_Yearly
  Pro --> Pro_Monthly
  Pro --> Pro_Yearly

In Paddle, we've set these up as two products called 'Starter' and 'Pro,' each with two prices for monthly and annual.

This is an example from the list products operation in the Paddle API. It shows the two products we're using, including an array of prices for each.

200 OK
{
"data": [
{
"id": "pro_01gsz4t5hdjse780zja8vvr7jg",
"name": "ChatApp Pro",
"tax_category": "standard",
"description": "Everything in starter, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.",
"image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png",
"custom_data": null,
"status": "active",
"created_at": "2023-02-23T12:43:46.605Z",
"prices": [
{
"id": "pri_01gsz8z1q1n00f12qt82y31smh",
"product_id": "pro_01gsz4t5hdjse780zja8vvr7jg",
"description": "Annual (per seat)",
"name": null,
"billing_cycle": {
"interval": "year",
"frequency": 1
},
"trial_period": null,
"tax_mode": "account_setting",
"unit_price": {
"amount": "30000",
"currency_code": "USD"
},
"unit_price_overrides": [],
"custom_data": null,
"status": "active",
"quantity": {
"minimum": 1,
"maximum": 999
}
},
{
"id": "pri_01gsz8x8sawmvhz1pv30nge1ke",
"product_id": "pro_01gsz4t5hdjse780zja8vvr7jg",
"description": "Monthly (per seat)",
"name": null,
"billing_cycle": {
"interval": "month",
"frequency": 1
},
"trial_period": null,
"tax_mode": "account_setting",
"unit_price": {
"amount": "3000",
"currency_code": "USD"
},
"unit_price_overrides": [],
"custom_data": null,
"status": "active",
"quantity": {
"minimum": 1,
"maximum": 999
}
}
]
},
{
"id": "pro_01gsz4s0w61y0pp88528f1wvvb",
"name": "ChatApp Starter",
"tax_category": "standard",
"description": "Ideal for small teams who want to stay connected and organized. Access to all the essential features, including unlimited messaging, file sharing, and integrations with your favorite tools.",
"image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png",
"custom_data": null,
"status": "active",
"created_at": "2023-02-23T12:43:09.062Z",
"prices": [
{
"id": "pri_01gsz8s48pyr4mbhvv2xfggesg",
"product_id": "pro_01gsz4s0w61y0pp88528f1wvvb",
"description": "Annual (per seat)",
"name": null,
"billing_cycle": {
"interval": "year",
"frequency": 1
},
"trial_period": null,
"tax_mode": "account_setting",
"unit_price": {
"amount": "10000",
"currency_code": "USD"
},
"unit_price_overrides": [],
"custom_data": null,
"status": "active",
"quantity": {
"minimum": 1,
"maximum": 100
}
},
{
"id": "pri_01gsz8ntc6z7npqqp6j4ys0w1w",
"product_id": "pro_01gsz4s0w61y0pp88528f1wvvb",
"description": "Monthly (per seat)",
"name": null,
"billing_cycle": {
"interval": "month",
"frequency": 1
},
"trial_period": null,
"tax_mode": "account_setting",
"unit_price": {
"amount": "1000",
"currency_code": "USD"
},
"unit_price_overrides": [],
"custom_data": null,
"status": "active",
"quantity": {
"minimum": 1,
"maximum": 100
}
}
]
}
],
"meta": {
"request_id": "a920d367-69af-453b-a602-a49a2db92527",
"pagination": {
"per_page": 50,
"next": "https://api.paddle.com/products?after=pro_01gsz4s0w61y0pp88528f1wvvb&id=pro_01gsz4t5hdjse780zja8vvr7jg&id=pro_01gsz4s0w61y0pp88528f1wvvb&include=prices",
"has_more": false,
"estimated_total": 2
}
}
}

To define these, create variables for the products in your script section and set them to the Paddle IDs for the products. We'll use these later to determine which products returned prices are for.

Then, create arrays for your prices. Each array should contain an object that includes the Paddle ID for a price (priceId) and a quantity. We've created two arrays:

  • monthItems, which contains monthly prices for our products.
  • yearItems, which contains yearly prices for our products.

We'll present localized prices for monthItems when the monthly toggle is selected, and yearItems when the yearly toggle is selected.

HTML
<script type="text/javascript">
Paddle.Environment.set("sandbox");
Paddle.Initialize({
token: 'test_7d279f61a3499fed520f7cd8c08' // replace with a client-side token
});
// define products and prices
var starterProduct = 'pro_01gsz4s0w61y0pp88528f1wvvb';
var proProduct = 'pro_01gsz4t5hdjse780zja8vvr7jg';
var monthItems = [{
quantity: 1,
priceId: 'pri_01gsz8ntc6z7npqqp6j4ys0w1w',
},
{
quantity: 1,
priceId: 'pri_01gsz8x8sawmvhz1pv30nge1ke',
}
];
var yearItems = [{
quantity: 1,
priceId: 'pri_01gsz8s48pyr4mbhvv2xfggesg',
},
{
quantity: 1,
priceId: 'pri_01gsz8z1q1n00f12qt82y31smh',
}
];
</script>

Get prices

Next, we'll create a function to get prices. This should pass our list of monthly or yearly items to Paddle.js.

In our sample, we've created a function called getPrices() that takes a parameter called cycle. Here's how it works:

  1. We create a variable called billingCycle and set this to year. This is the billing cycle that we'd like to show when customers first visit our page.
  2. We check to see if cycle is month, then set a variable called itemsList to either monthItems or yearItems. We also set a variable called billingCycle to the value of cycle for later.
  3. We define a variable called request. This is what we're going to send to Paddle.js. It includes an object with an items key. The format of our request should match the request body for the pricing preview operation in the Paddle API, except with camelCase names for fields.
  4. We call Paddle.PricePreview(), passing in request as a parameter.
  5. Paddle.PricePreview() returns a promise that contains a pricing preview object. We use the .then() method to attach a callback that logs the resolved value to the console, and the .catch() method to log errors to the console.
HTML
<script type="text/javascript">
Paddle.Environment.set("sandbox");
Paddle.Initialize({
token: 'test_7d279f61a3499fed520f7cd8c08' // replace with a client-side token
});
// define products and prices
var starterProduct = 'pro_01gsz4s0w61y0pp88528f1wvvb';
var proProduct = 'pro_01gsz4t5hdjse780zja8vvr7jg';
var monthItems = [{
quantity: 1,
priceId: 'pri_01gsz8ntc6z7npqqp6j4ys0w1w',
},
{
quantity: 1,
priceId: 'pri_01gsz8x8sawmvhz1pv30nge1ke',
}
];
var yearItems = [{
quantity: 1,
priceId: 'pri_01gsz8s48pyr4mbhvv2xfggesg',
},
{
quantity: 1,
priceId: 'pri_01gsz8z1q1n00f12qt82y31smh',
}
];
// set initial billing cycle
var billingCycle = 'year'
// get prices
function getPrices(cycle) {
var itemsList = cycle === "month" ? monthItems : yearItems;
var billingCycle = cycle;
var request = {
items: itemsList
}
Paddle.PricePreview(request)
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
});
}
</script>

Test your work

Save your page, then open your browser console and type getPrices('year') or getPrices('month'). You should see a promise that contains a pricing preview object from Paddle returned the console.

Short animation showing Google Chrome with the browser console open. "getPrices('year')" is typed into the console, which returns a line saying data and meta. This section is expanded to show the full object.

Update page

Our function doesn't do anything to our page yet. We'll update getPrices() so that it displays pricing information returned by Paddle.js on our page.

Create HTML for pricing table

First, we need to add some HTML for a simple pricing table with options for monthly and yearly. We'll add some CSS to the <head> of the page, too.

Add this to the <body> of your page.

In this sample, there are radio buttons for our pricing toggle, then a <div> with three <div> elements for each product that we offer. The radio buttons have an onclick attribute that runs our getPrices() function when clicked, passing either month or year as the parameter for cycle.

It sets ids <p> elements that contain prices. We'll use these IDs to replace the contents of these elements with returned prices from Paddle.js later.

HTML
<div class="pricing-page-container">
<h1>Choose your plan</h1>
<div class="pricing-toggle">
<input type="radio" name="plan" value="month" id="month" onclick="getPrices('month')"><label for="month">Monthly</label>
<input type="radio" name="plan" value="year" id="year" onclick="getPrices('year')" checked><label for="year">Yearly <sup>save 20%</sup></label>
</div>
<div class="pricing-grid">
<div class="starter-plan">
<h3>Starter</h3>
<p id="starter-price">$100.00</p>
<p><small>per user</small></p>
<button>Sign up now</button>
</div>
<div class="pro-plan">
<h3>Pro</h3>
<p id="pro-price">$300.00</p>
<p><small>per user</small></p>
<button>Sign up now</button>
</div>
<div class="enterprise-plan">
<h3>Enterprise</h3>
<p>Contact us</p>
<p><small>bespoke pricing</small></p>
<button>Inquire now</button>
</div>
</div>
</div>

Add this to the <head> of your page. It applies some styling to the HTML so that the <div> elements are arranged in three columns.

For this sample, we also include MVP.css, which applies light styling to HTML elements. You don't need to do this if you're updating an existing page in your app or website that already has its own styling.

HTML
<style>
.pricing-page-container {
max-width: 900px;
margin: auto;
text-align: center;
margin-top: 2em;
padding-left: 1em;
padding-right: 1em;
}
.pricing-grid {
display: block;
margin-bottom: 1em;
}
.pricing-grid .starter-plan {
background-color: AliceBlue
}
.pricing-grid .pro-plan {
background-color: HoneyDew
}
.pricing-grid .enterprise-plan {
background-color: LavenderBlush
}
.pricing-grid > * {
padding: 1rem;
}
@media (min-width: 768px) {
.pricing-grid {
display: grid;
grid-auto-rows: 1fr;
grid-template-columns: 1fr 1fr 1fr;
}
}
</style>
<!-- MVP.css: https://andybrewer.github.io/mvp/ -->
<link rel="stylesheet" href="https://unpkg.com/mvp.css@1.12.0/mvp.css" media="print" onload="this.media='all'">

Update elements using JavaScript

Next, we'll change our script to update the starter-price and pro-price elements so they return pricing from Paddle.

First, we'll get elements in our pricing table using their id and assign them to variables that we can use later.

Then, we'll update our getPrices() function to iterate through result.data.details.lineItems. This array contains calculated totals for the prices that we passed to Paddle.js.

To make sure we show the correct prices for our products, we check to see if the Paddle ID of the related product of a price matches the product IDs we defined earlier:

  • If the product for a returned price is starterProduct, we replace the contents of the starter-price element with item.formattedTotals.subtotal
  • If the product for a returned price is proProduct, we replace the contents of the pro-price element with item.formattedTotals.subtotal.

For this sample, we also log item.formattedTotals.subtotal to console. This can be useful for debugging.

HTML
<script type="text/javascript">
Paddle.Environment.set("sandbox");
Paddle.Initialize({
token: 'test_7d279f61a3499fed520f7cd8c08' // replace with a client-side token
});
// define products and prices
var starterProduct = 'pro_01gsz4s0w61y0pp88528f1wvvb';
var proProduct = 'pro_01gsz4t5hdjse780zja8vvr7jg';
var monthItems = [{
quantity: 1,
priceId: 'pri_01gsz8ntc6z7npqqp6j4ys0w1w',
},
{
quantity: 1,
priceId: 'pri_01gsz8x8sawmvhz1pv30nge1ke',
}
];
var yearItems = [{
quantity: 1,
priceId: 'pri_01gsz8s48pyr4mbhvv2xfggesg',
},
{
quantity: 1,
priceId: 'pri_01gsz8z1q1n00f12qt82y31smh',
}
];
// DOM queries
var starterPriceLabel = document.getElementById("starter-price");
var proPriceLabel = document.getElementById("pro-price");
// set initial billing cycle
var billingCycle = 'year'
// get prices
function getPrices(cycle) {
var itemsList = cycle === "month" ? monthItems : yearItems;
var billingCycle = cycle;
var request = {
items: itemsList
}
Paddle.PricePreview(request)
.then((result) => {
console.log(result);
var items = result.data.details.lineItems;
for (item of items) {
if (item.product.id === starterProduct) {
starterPriceLabel.innerHTML = item.formattedTotals.subtotal
console.log('starter ' + item.formattedTotals.subtotal)
} else if (item.product.id === proProduct) {
proPriceLabel.innerHTML = item.formattedTotals.subtotal
console.log('pro ' + item.formattedTotals.subtotal)
}
}
})
.catch((error) => {
console.error(error);
});
}
</script>

Set getPrices() to run on page load

Right now, our function only runs when the monthly or annual radio buttons are clicked.

We can add onLoad to our <body> tag to run our getPrices() function immediately after the page has loaded:

HTML
<body onLoad="getPrices(billingCycle)">

Test your work

Save your page, then open it in your browser. You should see prices from Paddle.js in your pricing table. Selecting the monthly or annual toggle should change the prices that you see.

Short animation showing toggling between monthly and annual pricing. The prices change when the monthly and annual radio buttons are clicked.

Next steps

That's it. Now we've built a simple pricing page, you might like to add other fields to your page, pass a discount, or open a checkout.

Add other fields to your pricing page

Paddle.PricePreview() returns a pricing preview object for the prices and location passed. We show details.lineItems.formattedTotals.subtotal in our sample. This is the calculated total for an item before estimated taxes and discounts, formatted for a particular currency.

You might like to use another value for the price you show on your page, or include other values.

Here are some fields in the response that you might like to use on your page:

details.address.countryCodeCountry code for the pricing preview. If you sent an IP address, Paddle returns the detected country.
details.line_items[].formattedTotalsTotals for a particular line item, formatted as a string for the currency you're working with.
details.line_items[].formattedUnitTotalsTotals for one unit of a particular line item, formatted as a string for the currency you're working with.
details.line_items[].price.trialPeriodDetails of the trial period for a price.
details.line_items[].discounts[].formattedTotalTotal amount discounted for a discount applied to a line item, formatted as a string for the currency you're working with.

For a full list of values, see Pricing preview object

Pass a discount

Extend your pricing page by passing discountId in your request to Paddle.PricePreview(). The response includes a discount array that has information about the discount applied. Calculated totals in details.lineItems include discounts, where applicable.

Open a checkout

Pass items to Paddle.Checkout.open() or use HTML data attributes to open a checkout.

Was this page helpful?