Skip to main content
That is the whole setup. Create your products in the dashboard under Finance > Products, then use the plan ids they give you.
You do not write a webhook. Result receives every payment event, verifies its signature, handles retries and out-of-order delivery, and writes the result into billing_customers and billing_subscriptions in your own backend. Those tables already exist. Do not create them, do not write to them, and do not build a payment webhook endpoint.

Pricing page

In a browser, formattedTotal is display-ready. Paddle accounts localize it to the visitor’s country with tax applied; connected Stripe accounts format the price and currency stored in Stripe. Server-side you get amount and currencyCode to format yourself.
amount is a string in the currency’s lowest denomination. "1000" is $10.00. It is not always hundredths, either: "1200" in JPY is ¥1,200, not ¥12. Use formattedTotal where you can.

Checkout

Opens the configured provider’s checkout. Paddle uses its overlay; connected Stripe accounts use Stripe-hosted Checkout. The signed-in user’s id is attached automatically, which is how the purchase gets matched back to their account. The buyer has to be signed in. With nobody signed in this throws PAYMENTS_SIGN_IN_REQUIRED rather than opening a checkout, because a payment with no user attached can never grant access and there is no way to repair it after the money has moved. Send buyers through sign-in first.

Gating features

Never grant access from a success URL, a redirect, or the checkout callback. The buyer can close the tab before any of them run, and you will have taken money without granting anything. hasAccess() reads what the payment actually wrote.
Access is granted for active, trialing and past_due. past_due means a renewal payment failed and automatic recovery is under way, which usually succeeds. Show a banner linking to the billing portal rather than cutting the customer off:
A scheduled cancellation keeps access until it takes effect, so someone who cancels mid-month keeps what they paid for.

Managing a subscription

The portal handles payment methods, invoices and cancellation.
Portal links are single-use and short-lived. Mint one per click, open it in a new tab, and never cache or iframe it. Changes a customer makes there arrive back in your database on their own.
Plan changes cannot be made in the portal, which is why changePlan() exists. It bills the difference immediately, prorated to the minute.

Test mode

Test-mode accounts use test cards, never a real one: Any future expiry and any name work. Paddle accounts go live with the switch in Finance > Payments. Stripe accounts connect the live Stripe account from Payments > Settings. Your running app follows the account connection, so neither path needs an app environment variable change or redeploy.

Before you can go live

Paddle’s merchant-of-record setup requires three pages, publicly reachable without a login:
  1. Terms and conditions, stating that Paddle.com is the reseller and Merchant of Record for all orders.
  2. A refund policy with a window between 14 and 90 days.
  3. A privacy notice.
Login walls and hidden pricing are the most common reason a site is refused. With Stripe, you remain the merchant and keep your existing Stripe business, tax, dispute and payout configuration. Result only requests the permissions needed to show and operate that account from Finance.

Reading the tables directly

Useful for dashboards and joins:
billing_customers, billing_subscriptions and billing_plans are read-only to signed-in users and scoped to their own rows.

From the CLI