inertia start
Features

Billing System

Monetize your application with Stripe or Paddle.

Inertia Start includes a provider-agnostic billing system based on Laravel Cashier. Choose between our two built-in providers (Stripe or Paddle) to receive payments from your customers. You can also switch providers later without breaking billing management for existing paying customers.

You can sell one-time products and subscriptions, accept payments from signed-in customers and guests, and use ready-made billing Vue components. Customers get a self-service account billing page (payment methods, billing details, downloadable invoices and receipts), and admins can configure the whole catalog from a no-code editor — without touching a config file by hand.

See the pricing page live

Explore the ready-made pricing cards, with animated prices and guest checkout.

Supported Providers

Stripe

Inertia Start supports Stripe via Laravel Cashier Stripe.

Stripe supports one-time payments, subscriptions, billing portal access, and an optional Managed Payments mode (its new Merchant of Record service that handles global tax registration and remittance for you).

See their full list of supported payment methods.

Paddle

Inertia Start supports Paddle via Laravel Cashier Paddle.

Paddle is a Merchant of Record (MoR) by default, and offers an all-inclusive fee including payment processing and global sales tax handling.

See their full list of supported payment methods.

Features

  • Easily switch payment provider: choose between Stripe and Paddle with I_S_DEFAULT_PAYMENT_PROVIDER environment variable.
  • One-time payments and subscriptions: supported for both providers.
  • Subscriptions: define your plans in config and get a ready-made account billing page where customers subscribe, switch plans, cancel, and resume.
  • One-time products: define products in config and sell them through a single shareable checkout link.
  • Hosted Buy & Subscribe pages: shareable /buy/{product} and /subscribe/{group}/{plan} links render a ready-made checkout page (plan details, features, price, trial) — no frontend code required.
  • Self-service account billing: signed-in customers get an account billing page with their subscriptions, saved payment methods, billing information, and a paginated billing history with downloadable invoices and receipts.
  • No-code catalog editors: admins can configure products and subscription plans from the admin UI. Inertia Start writes the config files for you and pulls live prices from your provider catalog.
  • Guest checkout: customers can buy without creating an account first. They enter an email at checkout, and Inertia Start creates their account and emails a sign-in link.
  • Stripe embedded checkout: optionally embed Stripe Checkout inside the Buy/Subscribe pages instead of redirecting to Stripe.
  • Shared billing routes and API: use one set of routes while provider resolution is handled for you.
  • Per-customer provider pinning: existing customers stay on their original provider after you switch the active provider (to avoid breaking existing subscriptions and customer portal).
  • Provider-agnostic UI components: use PricingCards, CheckoutButton, and CheckoutSection without changing your page code when you switch providers.
  • Catalog allowlists: restrict checkout to approved prices and discounts with allowlist variables.
  • Component Builder: preview and generate code for CheckoutButton, CheckoutSection, and PricingCards against live provider catalog data from /admin/billing/component-builder.
  • Shared Inertia props: every page receives billingEnabled, accountBillingEnabled, billingSubscriptionsEnabled, and paymentProvider (effective provider for the current user/session).

Setup

1. Enable billing and choose a provider

You can use the interactive setup command:

php artisan inertia-start:billing:setup

This command updates I_S_ENABLE_BILLING, I_S_DEFAULT_PAYMENT_PROVIDER, and I_S_ENABLED_PAYMENT_PROVIDERS in your environment file (it keeps previously enabled providers so existing customers are not left behind). You still need to add provider credentials and any optional billing settings manually.

Or configure manually in .env:

.env
# Billing
I_S_ENABLE_BILLING=true
I_S_ENABLE_SUBSCRIPTIONS=false # set to true to enable the subscriptions and manage them in account billing page
I_S_DEFAULT_PAYMENT_PROVIDER=stripe # stripe or paddle
I_S_ENABLED_PAYMENT_PROVIDERS=stripe # default + previously used providers (keep previous providers enabled for existing customers)
I_S_PURCHASE_LINK_DAYS=15 # days a guest-checkout sign-in link stays valid
CASHIER_CURRENCY=usd

# Stripe
STRIPE_KEY=
STRIPE_SECRET=
STRIPE_WEBHOOK_SECRET=
# Optional: allow `*` for all active Stripe prices/discounts, or a comma-separated allowlist
I_S_STRIPE_ALLOWED_PRICES=*
I_S_STRIPE_ALLOWED_DISCOUNTS=*
# Optional: Stripe Managed Payments (https://docs.stripe.com/payments/managed-payments)
#I_S_STRIPE_MANAGED_PAYMENTS_ENABLED=false
# Optional: embed Stripe Checkout on the Buy/Subscribe pages instead of redirecting
#I_S_STRIPE_EMBEDDED_CHECKOUT_ENABLED=false
# Find your Stripe customer portal URL here: https://dashboard.stripe.com/settings/billing/portal
I_S_STRIPE_CUSTOMER_PORTAL_URL=

# Paddle
PADDLE_CLIENT_SIDE_TOKEN=
PADDLE_API_KEY=
PADDLE_WEBHOOK_SECRET=
PADDLE_SANDBOX=true
# Optional: allow `*` for all active Paddle prices/discounts, or a comma-separated allowlist
I_S_PADDLE_ALLOWED_PRICES=*
I_S_PADDLE_ALLOWED_DISCOUNTS=*
# Find your Paddle customer portal URL here: https://vendors.paddle.com/customer-portal-settings
#I_S_PADDLE_CUSTOMER_PORTAL_URL=

2. Create products and prices in your provider dashboard

If you plan to use discounts in checkout, create those in your provider dashboard too.

3. Configure webhooks

Provider-specific webhook endpoints used by Inertia Start:

  • Stripe: /stripe/webhook
  • Paddle: /paddle/webhook

Only endpoints for enabled providers are registered. If you switch providers but still have existing customers on the old provider, keep the old provider in I_S_ENABLED_PAYMENT_PROVIDERS and keep its webhook endpoint enabled in the provider dashboard.

4. Run migrations

php artisan migrate

Switching providers with existing customers

Changing I_S_DEFAULT_PAYMENT_PROVIDER sets the default provider for new purchases and subscriptions.

Enabled providers are controlled by I_S_ENABLED_PAYMENT_PROVIDERS. This list determines which provider-specific webhook routes are available (alongside the current default provider).

When a user already has billing history and that provider remains enabled, Inertia Start keeps that user on the original provider for:

  • checkout creation
  • customer payload lookup
  • customer portal redirection

This means you can safely start with one provider and later move new sales to the other provider without breaking existing subscriptions, as long as the previous provider is still enabled.

If you have active paying customers on the previous provider, do not remove that provider from I_S_ENABLED_PAYMENT_PROVIDERS, and do not remove its credentials or webhook configuration. Keep it enabled until all legacy subscriptions are terminated and purchase lifecycle events are fully completed. In practice, keep:

  • old provider API keys/tokens
  • old provider webhook secret
  • old provider listed in I_S_ENABLED_PAYMENT_PROVIDERS
  • old provider webhook endpoint enabled in that provider dashboard

Stripe Managed Payments

When using Stripe, you can enable Managed Payments. This mode uses Stripe as a Merchant of Record (global tax compliance is handled by Stripe).

.env
I_S_DEFAULT_PAYMENT_PROVIDER=stripe
I_S_STRIPE_MANAGED_PAYMENTS_ENABLED=true

When enabled, Inertia Start creates Stripe Checkout Sessions with:

  • managed_payments.enabled = true
  • ; managed_payments_preview=v1 appended to stripe_version

Because Stripe is the Merchant of Record in this mode, it always issues receipts and invoices to your customers automatically — for both one-time payments and subscriptions. Stripe sends them directly to the customer (emailed from Link, with PDF copies attached), so you don't need to turn on the per-product invoices option, and Inertia Start does not request invoice_creation when Managed Payments is enabled.

Stripe Embedded Checkout

By default, Stripe checkout sends the customer to Stripe's own hosted page. With embedded checkout, the Stripe Checkout form is rendered inside your own Buy / Subscribe pages instead, so the customer never leaves your domain.

.env
I_S_DEFAULT_PAYMENT_PROVIDER=stripe
I_S_STRIPE_EMBEDDED_CHECKOUT_ENABLED=true

When enabled (Stripe only):

  • The hosted /buy/{product} and /subscribe/{group}/{plan} pages mount Stripe's embedded checkout instead of redirecting to Stripe.
  • The page receives your Stripe publishable key (config('cashier.key')) so the embedded form can load. Stripe.js is downloaded on demand the first time the form mounts — you never add a script tag yourself.
  • The CheckoutButton component exposes stripe-embedded and stripe-key props, and the POST /api/billing/checkout/init endpoint accepts an embedded flag that returns a client_secret instead of a redirect url.

Embedded checkout is ignored when Paddle is the active provider (Paddle always uses its own overlay). It also only applies to the built-in Buy/Subscribe pages; standalone CheckoutButton / PricingCards placements keep redirecting unless you opt in with the stripe-embedded prop.

Catalog Allowlists

Use the provider-specific allowlist variables to restrict which catalog items can be used in checkout:

  • I_S_STRIPE_ALLOWED_PRICES
  • I_S_STRIPE_ALLOWED_DISCOUNTS
  • I_S_PADDLE_ALLOWED_PRICES
  • I_S_PADDLE_ALLOWED_DISCOUNTS

Use * to allow every active provider item, or provide a comma-separated list of price/discounts IDs. When a disallowed price or discount is submitted to checkout, the billing endpoints return a 404.

The admin catalog endpoints include an allowed_for_checkout flag so you can see what is currently selectable.

Customer Portal

Use route('billing.portal') to redirect users to your payment provider’s customer portal.

BillingService::getBillingPortalUrl() returns a customer-authenticated billing portal URL when possible, and falls back to the configured portal URL when necessary.

Stripe

When using Stripe, Inertia Start will:

  • create a Stripe customer automatically if the authenticated user does not have one yet
  • reuse, reactivate, or create a Stripe billing portal configuration as needed
  • redirect to a customer-specific portal session

If a customer-specific session cannot be generated, it falls back to I_S_STRIPE_CUSTOMER_PORTAL_URL.

Paddle

When using Paddle, you can configure your default customer portal URL (it will be used when no authenticated customer is available), using the I_S_PADDLE_CUSTOMER_PORTAL_URL environment variable. Find your dedicated portal URL in your Paddle dashboard under Business Account > Customer Portal.

If this variable is not set, Inertia Start falls back to https://paddle.net/verify-email.

Subscriptions

You describe your plans once in a config file, and Inertia Start gives you a ready-to-use subscriptions management system and a billing page where your customers can:

  • see the pricing with the available plans and billing cycles
  • subscribe, switch, cancel, and resume their plans
  • go to their customer portal (in Stripe or Paddle) to manage their billing information, payment methods and invoices

Subscriptions build on the same provider-agnostic components as the rest of the billing system. You define plans for Stripe, Paddle, or both, and Inertia Start shows the right ones based on the active provider.

Laravel Cashier relies on webhooks to keep subscription and billing data synchronized, and our implementation continues to use them as the primary mechanism for processing billing events. However, we added an extra layer of reliability: whenever a user accesses the billing page, we perform a fresh synchronization with Stripe or Paddle. This complements the webhook-based workflow and ensures the billing page always reflects the current state from the payment provider, even if a webhook is delayed, missed, or temporarily unavailable.

Enable subscriptions

Subscriptions are a separate feature flag layered on top of billing. Enable both:

.env
I_S_ENABLE_BILLING=true
I_S_ENABLE_SUBSCRIPTIONS=true

The subscriptions section of the account billing page only appears when subscriptions are enabled and you have at least one subscription configured.

The account billing page itself shows up whenever billing is enabled and you have configured subscriptions or products. When neither is configured, the "Billing" link keeps pointing customers to the provider customer portal. Two shared Inertia props let you branch on this: accountBillingEnabled (the page is available) and billingSubscriptionsEnabled (the subscriptions section is shown).

Define your plans

Create config/env/subscriptions.php, returning an array of subscription groups. Each group has a slug and a list of plans. A plan holds the display info (name, features, ...) and the provider price IDs.

config/env/subscriptions.php
<?php

return [
    [
        'name' => 'Pro membership',
        'slug' => 'pro',                  // used internally as the subscription "type"
        'description' => 'Everything you need to grow.',
        'selected_billing_cycle' => 'monthly',
        'billing_cycle_badges' => [
            'annually' => 'Save 17%',     // badge shown on the annual tab
        ],
        'plans' => [
            [
                'name' => 'Starter',
                'slug' => 'starter-monthly',
                'features' => [
                    'Unlimited projects',
                    'Email support',
                ],
                'stripe' => [
                    'price_id' => 'price_123',
                    'amount' => 1500,         // $15.00, in minor units
                    'currency_code' => 'USD',
                    'billing_cycle' => 'monthly',
                ],
                'paddle' => [
                    'price_id' => 'pri_123',
                    'amount' => 1500,
                    'currency_code' => 'USD',
                    'billing_cycle' => 'monthly',
                ],
            ],
            [
                'name' => 'Starter',
                'slug' => 'starter-annually',
                'accented' => true,           // highlight this card
                'stripe' => [
                    'price_id' => 'price_456',
                    'amount' => 15000,        // $150.00 / year
                    'currency_code' => 'USD',
                    'billing_cycle' => 'annually',
                ],
                'paddle' => [
                    'price_id' => 'pri_456',
                    'amount' => 15000,
                    'currency_code' => 'USD',
                    'billing_cycle' => 'annually',
                ],
            ],
        ],
    ],
];

By default, config/env/products.php and config/env/subscriptions.php are gitignored (via config/env/.gitignore), so your real price IDs stay out of version control. Inertia Start loads config/env/subscriptions.php automatically when the file exists. If you'd rather track these files in git — for example when the IDs aren't sensitive, or you deploy the same catalog everywhere — just allow them in config/env/.gitignore (e.g. add !subscriptions.php) and commit them.

No-code editor

Rather not write this file by hand? The Subscriptions config editor in your admin area builds it for you: you pick a price from your live provider catalog, and Inertia Start snapshots the amount, currency, and billing cycle automatically.

How it works:

  • The group slug is the subscription type Cashier stores. Keep it stable — switching plans happens within a group.
  • Each plan must resolve a price ID for the active provider, otherwise it is skipped. Provide both stripe and paddle blocks if you support both providers.
  • Shared display values (name, features, ...) go at the plan's top level; provider-specific values (price_id, amount, billing_cycle, ...) go inside the stripe / paddle block. A provider block overrides the matching top-level value.
  • name, description, and features may be plain strings or Laravel translation keys, so plans can be localized.
  • Put several plans (for example monthly + annually) in the same group to get an automatic billing-cycle switcher.

The account billing page

Signed-in customers get a billing page at /account/billing (route account.billing.index), linked from the user menu and the account sidebar. It's the customer's billing hub and is made of up to four sections, each loaded independently with its own skeleton while data is fetched from the provider:

The page appears as soon as you have configured subscriptions or products (see when it appears). The four sections render based on what's available, so a products-only store still gets payment methods, billing information, and history — just without the subscriptions section.

Subscriptions

When subscriptions are configured, the top of the page lets customers:

  • Subscribe to a plan if they don't have one yet.
  • Switch to another plan in the same group (upgrade or downgrade).
  • Cancel — they keep access until the end of the current period, then it stops renewing.
  • Resume a canceled subscription before it ends.
  • Resolve payment when a charge fails, via the provider's hosted invoice or payment page.
  • Open the provider portal to manage payment methods and invoices.

Each subscription shows a clear status: Active, Trial, Scheduled to cancel, Paused, Payment due, Payment pending, or Inactive.

When the page loads it calls POST /api/billing/subscriptions/sync (api.billing.subscriptions.sync) to pull the latest state from Stripe/Paddle, so it stays accurate even if a webhook hasn't arrived yet. A skeleton is shown while syncing.

Switching plans

Plan changes are invoiced immediately, so a paid upgrade is collected before the higher tier is granted.

  • Stripe uses a pending update: the current plan stays active until the new invoice is paid. If the payment needs confirmation, the customer is redirected to confirm it. If they change their mind, Keep current plan voids the pending change.
  • Paddle swaps and invoices right away. If the payment becomes past due, the customer is sent to update their payment method.

Payment methods

This section lists the cards (and other payment methods) the customer has on file — showing the brand, last four digits, expiry, and a Default badge — fetched from GET /api/billing/payment-methods (api.billing.payment-methods.index). For Stripe it reads the customer's payment methods and default; for Paddle it reads the customer's saved payment methods. A Manage payment methods button sends the customer to the provider portal, where adding, removing, and changing the default actually happens. If the customer has no provider record yet, the list is simply empty.

Billing information

The name, address, and tax ID that appear on the customer's invoices, fetched from GET /api/billing/information (api.billing.information.show). For Stripe it reads the Stripe customer and first tax ID; for Paddle it reads the customer, their most recent address, and business tax identifier. The Edit billing information button opens the provider portal (in a new tab), where the customer updates these details directly with the provider.

Billing history

A paginated table of the customer's past billing activity, fetched from GET /api/billing/history (api.billing.history.index). It uses cursor pagination (cursor + per_page, 1–25, default 10) so customers can load more without loading everything at once. Each row shows a date, reference, total, and status, plus an actions menu to view or download the relevant document:

  • Stripe merges two sources into one chronological list: finalized invoices (open, paid, uncollectible, void) and successful one-time payments (Checkout Sessions in payment mode that have no invoice). Invoices link to Stripe's hosted invoice page, the invoice PDF, and — when paid — the receipt. One-time payments link to the Stripe receipt.
  • Paddle lists the customer's transactions, linking to the Paddle-hosted invoice/receipt.

Document links never expose provider URLs directly. Instead they point at signed-in redirect endpoints that re-check ownership before sending the customer to the provider:

  • GET /api/billing/invoices/{invoice}/open (api.billing.invoices.open) — redirects to the hosted invoice, the PDF (disposition=attachment), or the receipt (document=receipt).
  • GET /api/billing/payments/{session}/receipt (api.billing.payments.receipt) — redirects to the Stripe receipt for a one-time payment session.

All four sections return 404 for users who shouldn't see the account billing page, and the history/information/payment-method responses are sent with Cache-Control: no-store, private so sensitive billing data is never cached.

Let customers start a subscription

On the account billing page customers subscribe automatically. To offer a subscription elsewhere, you have two options: a ready-made hosted page, or your own pricing UI.

Hosted subscribe page

The fastest way to sell a plan is to link customers to its built-in subscribe page — the subscription equivalent of the product buy page:

GET /subscribe/{subscription}/{plan}   (route: billing.subscriptions.subscribe)
<template>
  <a :href="route('billing.subscriptions.subscribe', { subscription: 'pro', plan: 'starter-monthly' })">
    Subscribe to Starter
  </a>
</template>

subscription is the group slug and plan is the plan slug from your subscriptions config. The page renders the plan's name, description, price, billing cycle, free-trial badge, and feature list, then starts a recurring subscription of that type when the customer continues. It needs both the billing and subscriptions features enabled, and returns 404 for an unknown group/plan or a plan marked disabled.

When embedded checkout is off, Stripe customers are redirected straight to Stripe Checkout; otherwise the checkout is mounted on the page. Paddle always opens its overlay on the page.

Your own pricing UI

To build your own pricing page, render a PricingCards grid or a CheckoutButton and pass the group slug as subscription-type. That tells checkout to start a recurring subscription of that type instead of a one-off charge.

<template>
  <!-- starts a subscription of type "pro" -->
  <CheckoutButton
    prices="price_123"
    subscription-type="pro"
    return-to="/account/billing"
  >
    Subscribe
  </CheckoutButton>
</template>

With PricingCards, use the #cta slot to render a CheckoutButton that carries subscription-type (this is exactly what the account billing page does internally).

subscription-type must match a configured subscription slug, and the price must belong to that group — otherwise checkout returns 404.

One-time products

Products are for single, non-recurring purchases (a template pack, a lifetime deal, credits, ...). Like subscriptions, you describe them in a config file and get a shareable checkout link.

Define your products

Create config/env/products.php, returning an array of products keyed by slug:

config/env/products.php
<?php

return [
    'starter-pack' => [
        'name' => 'Starter pack',
        'description' => 'A one-time bundle to get going.',
        'stripe' => [
            'price_id' => 'price_123',
        ],
        'paddle' => [
            'price_id' => 'pri_123',
        ],
    ],
];

A product can bundle several prices and apply a discount:

'pro-bundle' => [
    'name' => 'Pro bundle',
    'stripe' => [
        'price_ids' => ['price_123', 'price_456'],
        'discount_id' => 'di_123',
    ],
    'paddle' => [
        'price_ids' => ['pri_123', 'pri_456'],
        'discount_id' => 'dsc_123',
    ],
],

Supported keys per product: name, description, price_id / price_ids, and discount_id (set per provider inside the stripe / paddle blocks). Stripe products also accept create_invoice to generate an invoice after payment. name and description may be Laravel translation keys.

The provider block can also hold a read-only amount, currency_code, and discount snapshot (same shape as the subscription plan discount snapshot), written automatically by the config editor so checkout pages can render price/discount details without an extra API call:

config/env/products.php
'starter-pack' => [
    'name' => 'Starter pack',
    'stripe' => [
        'price_ids' => ['price_123'],
        'discount_id' => 'disc_123',
        // Written automatically by the config editor — no need to set these by hand.
        'amount' => 1500,
        'currency_code' => 'USD',
        'discount' => [
            'id' => 'disc_123',
            'amount_off' => 500,
            'currency_code' => 'USD',
            'duration' => 'once',
        ],
    ],
],

No-code editor

You don't have to edit config/env/products.php by hand — the Products config editor in your admin area writes this file for you from your live provider catalog.

Sell a product

Link customers to the product buy page:

GET /buy/{product}   (route: billing.products.buy)
<template>
  <a :href="route('billing.products.buy', { product: 'starter-pack' })">
    Buy the starter pack
  </a>
</template>

This renders a ready-made buy page showing the product name, description, and a secure checkout button. When the customer continues:

  • Stripe redirects them straight to Stripe Checkout (or mounts the form on the page when embedded checkout is on).
  • Paddle launches the Paddle overlay on top of the page (Paddle has no full-page redirect checkout).

After payment, the customer lands on the thank-you page and the purchase is recorded in the product_purchases table, linked to their user (or to a guest account created for them).

The buy route is localized, so the URL respects the active locale prefix (e.g. /fr/buy/starter-pack). Always generate it with route('billing.products.buy', ...) rather than hard-coding the path.

Prefer to build your own button? The billing components work with product price IDs too. The product route is just the fastest path, and it's what powers guest product purchases end-to-end.

The buy page (and the subscribe page below) renders your product/plan info with CheckoutSection under the hood. Reach for CheckoutSection directly if you want that same full checkout card on your own custom pages.

Stripe invoices after payment

By default a one-time Stripe payment produces a receipt but not a formal invoice. If a product needs a proper invoice (for customers who expect one for accounting), opt in per product with stripe.create_invoice:

config/env/products.php
'pro-bundle' => [
    'name' => 'Pro bundle',
    'stripe' => [
        'price_ids' => ['price_123'],
        'create_invoice' => true,
    ],
],

When enabled, Inertia Start asks Stripe to create an invoice for the purchase, which then shows up (with a downloadable PDF) in the customer's billing history.

Stripe charges an additional fee for invoices created this way, so leave it off unless you actually need invoices. The option is Stripe-only and ignored by Paddle, which is a Merchant of Record and always issues its own invoices.

This option is also unnecessary when Stripe Managed Payments is enabled: as Merchant of Record, Stripe already issues invoices and receipts for every payment, so Inertia Start skips invoice_creation in that mode.

Guest checkout

Customers don't need an account before paying. When a guest completes a product or subscription checkout, Inertia Start:

  1. Creates a user account from their checkout email (no password, email pre-verified), or reuses a matching one.
  2. Records the purchase or subscription against that account.
  3. Emails them a magic sign-in link so they can reach their account and manage billing.

The sign-in link stays valid for I_S_PURCHASE_LINK_DAYS days (default 15):

.env
I_S_PURCHASE_LINK_DAYS=15

Collecting the guest's email

On the hosted buy / subscribe pages, guests are asked for an email before checkout starts (the CheckoutButton shows this automatically via its show-guest-email-input prop). The email is used to send the receipt and to link the purchase to an account.

This upfront email step is Stripe-only. A Stripe email address isn't a unique identifier — Stripe happily lets you create several customers with the same email, and a guest Stripe checkout session creates a brand-new Stripe customer unless the app already knows which existing customer to attach it to. Asking for the email before creating the checkout session lets Inertia Start look up a matching account first and reuse it, avoiding duplicate Stripe customers for the same person. Paddle doesn't need this: Paddle resolves the matching Paddle customer for a given email itself, reusing an existing one instead of minting a new one.

When a guest enters an email that already belongs to an existing user, Inertia Start reuses that account instead of creating a duplicate. To protect customers who are pinned to the other provider, checkout is blocked with a clear message asking them to sign in first — this avoids accidentally creating a second billing identity for the same person on a different provider.

Purchases are recorded both when the customer returns to the thank-you page and from provider webhooks, so nothing is lost if the customer closes the tab. Recording is idempotent — a purchase is never double-counted.

Thank-you page

After any checkout, customers return to a built-in confirmation page at /billing/thank-you (route billing.thank-you). It:

  • verifies the checkout with the provider (Stripe session_id, Paddle _ptxn) and records the purchase,
  • adapts its wording to the purchase type — "Thank you for your purchase" for a product, "Thank you for subscribing" for a subscription — and labels the summary line accordingly,
  • shows a summary (item/plan, amount, email),
  • for signed-in customers, links to their account billing page ("Manage my subscription" for a subscription, "Go to my account" otherwise); for guests, tells them a sign-in link was emailed.

The billing components and the buy/subscribe routes set the correct return URL automatically, so you usually don't build this yourself. If a checkout can't be resolved, the page redirects to the account billing page (signed in) or the home page (guest). The route is localized.

Component Builder

Inertia Start comes with a component builder to quickly create checkout buttons and pricing cards.

The Component Builder is available in your admin area, in Billing > Component Builder (route admin.billing.component-builder, path /admin/billing/component-builder).

  • Access is restricted to authenticated, verified admins.
  • It loads live provider catalog data through the Stripe/Paddle API.
  • It previews CheckoutButton, CheckoutSection, and PricingCards UI components.
  • It shows which prices and discounts are currently allowed for checkout (based on your configuration) .

No-code config editors

Defining products and subscriptions by hand in config/env/*.php works well, but Inertia Start also ships visual editors in the admin area so you (or a non-developer teammate) can manage the catalog without touching code. They are next to the Component Builder, under Billing:

EditorPathWrites
Products/admin/billing/products (admin.billing.products)config/env/products.php
Subscriptions/admin/billing/subscriptions (admin.billing.subscriptions)config/env/subscriptions.php

Both editors are admin-only and work the same way:

  • They load your live provider catalog (products and prices) from the active provider, so you pick a real product/price from a dropdown instead of pasting IDs.
  • When you save, Inertia Start rewrites the matching config/env file for you (PUT to admin.billing.products.update / admin.billing.subscriptions.update).
  • They edit only the active provider's block (stripe or paddle). The other provider's block for each item is preserved untouched, so editing while Stripe is active never erases your Paddle IDs (and vice-versa) — important when you support both providers.
  • Provider-derived values you shouldn't set by hand — product ID, amount, currency, billing cycle, and the selected discount's details (percent/amount off, duration, ...) — are snapshotted from the catalog automatically when you pick a price or discount. You pick both from dropdowns backed by your live provider catalog: there's no free-text discount code field. You only edit display fields (name, slug, description, features, trial days, badges, ...).

If your configuration is cached (php artisan config:cache), the editor warns you: saving rewrites the file, but the cached config keeps serving the old values until you rebuild the cache (php artisan config:cache again, or clear it). The editor shows this banner whenever php artisan config:cache is in effect.

By default these files are gitignored (via config/env/.gitignore). The editors create the file on first save, so a fresh project starts with an empty catalog and an Add button. You're free to commit the files instead if you'd like them version-controlled — allow them in config/env/.gitignore (e.g. add !products.php) and commit as usual.

Usage

For most apps you only ever need three components:

  • CheckoutButton — a single Buy / Subscribe button.
  • CheckoutSection — a full checkout card (name, price, features, button, reassurance) for a single product or plan.
  • PricingCards — a full pricing table with one or more plans.

All three are provider-agnostic: you write your page once, and Inertia Start renders the correct Stripe or Paddle checkout automatically based on the active provider (shared with every page as page.props.paymentProvider). Switching providers later only means changing your billing environment variables — your page code stays the same.

The only thing that differs between Stripe and Paddle is the price IDs you pass:

  • Stripe price IDs look like price_123
  • Paddle price IDs look like pri_123

You don't need to write any backend code to take a payment. These components call Inertia Start's built-in billing routes for you. Reach for a custom backend checkout only when you need fully custom server-side logic.

Before you start

Make sure you have:

  1. Enabled billing and chosen a provider (see Setup).
  2. Created your products and prices in the provider dashboard, and copied their price IDs.

There is nothing else to wire up on the frontend. When Paddle is the active provider, Paddle.js is loaded for you automatically — you never add a script tag yourself.

Component Builder

Don't want to write the code by hand? The Component Builder in your admin area generates ready-to-paste CheckoutButton and PricingCards snippets from your live catalog.

Show a checkout button

The fastest way to charge a customer: drop a CheckoutButton anywhere and pass the ID of the price they are buying.

<script setup lang="ts">
import CheckoutButton from '@/components/billing/CheckoutButton.vue';
</script>

<template>
  <CheckoutButton prices="price_123" return-to="/billing/success">
    Subscribe
  </CheckoutButton>
</template>

When clicked, the customer is redirected to Stripe's hosted Checkout page. After paying (or cancelling) they are returned to return-to.

<script setup lang="ts">
import CheckoutButton from '@/components/billing/CheckoutButton.vue';
</script>

<template>
  <CheckoutButton prices="pri_123" return-to="/billing/success">
    Subscribe
  </CheckoutButton>
</template>

When clicked, Paddle's checkout opens in an overlay on top of your page (there is no full-page redirect). After a successful payment the customer is sent to return-to.

Paddle has a few optional props to fine-tune the overlay (locale, checkout-variant, customer, custom-data, transaction). See the props table.

Notice the two snippets are identical except for the price ID. Because the component is provider-agnostic, you just pass the price IDs of the active provider.

Buying several items or quantities

prices accepts more than a single string. Use whichever shape fits your case:

<template>
  <!-- A single price -->
  <CheckoutButton prices="price_123" return-to="/billing/success">Buy</CheckoutButton>

  <!-- A single price, quantity 3 -->
  <CheckoutButton prices="price_123" :quantity="3" return-to="/billing/success">
    Buy 3 seats
  </CheckoutButton>

  <!-- Several prices at once -->
  <CheckoutButton :prices="['price_123', 'price_456']" return-to="/billing/success">
    Checkout
  </CheckoutButton>

  <!-- Several prices with explicit quantities -->
  <CheckoutButton :prices="{ price_123: 2, price_456: 1 }" return-to="/billing/success">
    Checkout
  </CheckoutButton>
</template>

Props

CheckoutButton also forwards any shadcn-vue Button props (variant, size, disabled, loading, ...) to the underlying button.

PropTypeDefaultDescription
pricesstring | string[] | Record<string, number>undefinedProvider price ID(s) to purchase.
quantitynumber1Quantity, used when a single price is passed.
returnTostringundefinedURL the customer returns to after checkout.
successUrlstringundefinedAlias for returnTo; used when returnTo is not set.
discountCodestring | nullnullDiscount code to apply. Ignored when discountId is also passed.
discountIdstring | nullnullDiscount ID to apply. Preferred over discountCode when both are passed.
subscriptionTypestring | nullnullStart a recurring subscription of this type (a configured subscription slug) instead of a one-time charge.
localestring | nullnullPaddle only. Checkout locale (falls back to the app locale).
checkoutVariantCheckouVariantundefinedPaddle only. Checkout layout variant.
customerCustomer | nullnullPaddle only. Customer passed to the Paddle overlay.
transactionTransaction | nullnullPaddle only. Transaction for transaction-based checkout.
customDataRecord<string, any> | nullnullPaddle only. Custom metadata payload.
checkoutConsentErrorTextstring | nullnullCustom validation message shown in the consent dialog when a required checkbox is left unchecked. Falls back to the default localized message when unset.
showGuestEmailInputbooleanfalseStripe only. Ask guests for an email before checkout (used to send the receipt and match/create their account). Ignored under Paddle — CheckoutButton never forwards it to PaddleCheckoutButton.
stripeEmbeddedbooleanfalseStripe only. Mount embedded checkout instead of redirecting to Stripe.
stripeKeystring | nullnullStripe only. Publishable key used to load Stripe.js for embedded checkout.

Paddle-only props are simply ignored when Stripe is the active provider, and Stripe-only props are ignored under Paddle, so it is safe to leave them in place.

Show a full checkout section

CheckoutSection renders a complete "buy" section: product name, description and badge, a price block (with discount strikethrough, trial badge, and a loading skeleton), a feature checklist, guest-checkout copy, a CheckoutButton, and reassurance / secure-checkout notes. It's what renders the built-in product buy page and subscribe page — reach for it directly when you want that same ready-made card on your own page instead of assembling PricingCards / CheckoutButton and the surrounding copy yourself.

<script setup lang="ts">
import CheckoutSection from '@/components/billing/CheckoutSection.vue';
</script>

<template>
  <CheckoutSection
    product-name="Starter pack"
    product-description="A one-time bundle to get going."
    :price="{ amount: 1500, currencyCode: 'USD' }"
    :features="[{ name: 'Unlimited projects', included: true }]"
    :price-ids="['price_123']"
    success-url="/billing/success"
  />
</template>

If price.amount / price.currencyCode are omitted, CheckoutSection fetches them itself from priceIds (the same way PricingCards does), and if price.discountedAmount isn't provided it resolves the discount from discountId automatically.

Props

PropTypeDefaultDescription
productNamestringProduct/plan name shown as the card title.
productDescriptionstring | nullnullOptional description under the name.
groupNamestring | nullnullOptional subscription group name (for subscription checkouts).
badgestring | nullnullOptional badge label (for example a plan or billing-cycle badge).
priceCheckoutSectionPriceSummary | nullnullPrice to display (amount, currencyCode, discountedAmount, cycleLabel, trialLabel, loading). Fetched automatically from priceIds when omitted.
featuresCheckoutSectionFeature[][]Feature checklist ({ name, included }) rendered under the price.
featuresTitlestring | nullnullOptional heading above the feature list.
titlestring | nulltranslated defaultHeading above the checkout button. Pass null or '' to hide it.
descriptionstring | nulltranslated defaultText above the checkout button. Pass null or '' to hide it.
ctastringtranslated defaultCheckout button label.
priceIdsstring[]Provider price ID(s) to purchase (forwarded to CheckoutButton).
successUrlstringURL the customer returns to after checkout.
discountCodestring | nullnullDiscount code to apply. Ignored when discountId is also passed.
discountIdstring | nullnullDiscount ID to apply. Preferred over discountCode.
subscriptionTypestring | nullnullStart a recurring subscription of this type instead of a one-time charge.
stripeEmbeddedbooleanfalseStripe only. Mount embedded checkout instead of redirecting to Stripe.
stripeKeystring | nullnullStripe only. Publishable key used to load Stripe.js for embedded checkout.
transactionTransaction | nullnullPaddle only. Transaction for transaction-based checkout.
customerCustomer | nullnullPaddle only. Customer passed to the Paddle overlay.
customDataRecord<string, any> | nullnullPaddle only. Custom metadata payload.
reassurancestring | nulltranslated defaultReassurance line under the button (for example a refund policy). Pass null or '' to hide it.
secureCheckoutstring | nulltranslated defaultSecure-checkout note under the button. Pass null or '' to hide it.

CheckoutSection has no slots — it's fully props-driven, so it doesn't support the checkout-consent dialog. If you need that, compose PricingCards / CheckoutButton yourself instead.

Show a pricing table

PricingCards renders a full pricing table from an array of plans. Each card shows the plan name, price, features and its own checkout button, and a billing-cycle switcher appears automatically when your plans use different cycles (for example monthly vs annually).

When you pass real provider price IDs (price_… for Stripe, pri_… for Paddle), the component fetches the live amount and currency from the provider and merges them with the metadata you define locally (name, description, features, highlight). Your local data is mostly there to describe and present each plan.

<script setup lang="ts">
import PricingCards from '@/components/billing/PricingCards.vue';
import type { Price } from '@/components/billing/PricingCards.vue';

const prices: Price[] = [
  {
    id: 'price_monthly',
    productId: 'prod_basic',
    name: 'Basic',
    amount: 1500, // $15.00, in cents
    currencyCode: 'USD',
    billingCycle: 'monthly',
    features: [
      { name: 'Unlimited projects', included: true },
      { name: 'Priority support', included: false },
    ],
  },
  {
    id: 'price_yearly',
    productId: 'prod_basic',
    name: 'Basic',
    amount: 15000, // $150.00 / year
    currencyCode: 'USD',
    billingCycle: 'annually',
    accented: true, // highlight this card
  },
];
</script>

<template>
  <PricingCards :prices="prices" success-url="/billing/success" />
</template>
<script setup lang="ts">
import PricingCards from '@/components/billing/PricingCards.vue';
import type { Price } from '@/components/billing/PricingCards.vue';

const prices: Price[] = [
  {
    id: 'pri_monthly',
    productId: 'pro_basic',
    name: 'Basic',
    amount: 1500, // 15.00, in minor units
    currencyCode: 'USD',
    billingCycle: 'monthly',
    features: [
      { name: 'Unlimited projects', included: true },
      { name: 'Priority support', included: false },
    ],
  },
  {
    id: 'pri_yearly',
    productId: 'pro_basic',
    name: 'Basic',
    amount: 15000, // 150.00 / year
    currencyCode: 'USD',
    billingCycle: 'annually',
    accented: true, // highlight this card
  },
];
</script>

<template>
  <PricingCards :prices="prices" success-url="/billing/success" />
</template>

amount is always expressed in the currency's minor units (for example cents — 1500 means $15.00). The two examples above differ only in their price and product IDs.

Props

PropTypeDefaultDescription
pricesPrice[]undefinedPlans to render. Merged with live provider data when real IDs are used.
successUrlstringundefinedDefault return URL for checkout when a price does not provide its own.
immediateDisplaybooleanfalseShow cards immediately without waiting for API validation.
selectedBillingCycleBillingCycleundefinedBilling cycle selected by default in the UI.
displayedPriceBillingCycleBillingCycleundefinedBilling cycle used to calculate and display normalized prices.
billingCycleSelector'tabs' | 'switch''tabs'Selector UI style. The switch only renders when exactly two cycles exist.
billingCycleBadgesRecord<BillingCycle, string>undefinedOptional badge labels for billing cycle tabs.
animatedPricesbooleanfalseAnimate price changes when switching cycles.
currencyDisplayCurrencyDisplay'short'How the currency is shown: 'short' | 'long' | 'code' | 'name'.
showBillingCycleDiscountbooleantrueShow the savings badge when comparing billing cycles.
ctaTextstringundefinedDefault CTA label for cards.
accentedTextstringundefinedDefault badge text for accented (highlighted) cards.
discountCodestring | nullnullGlobal discount code applied when a price has no override.
discountIdstring | nullnullGlobal discount ID applied when a price has no override.
checkoutVariantCheckouVariantundefinedPaddle only. Checkout layout variant passed through to checkout.
customerCustomer | nullundefinedPaddle only. Customer; fetched automatically when omitted and Paddle is active.
checkoutConsentErrorTextstring | nullnullCustom validation message shown in each plan's consent dialog when a required checkbox is left unchecked. Falls back to the default localized message when unset.

Events

NameDescription
selectEmitted when a price CTA is clicked. Receives the selected price ID.
billingCycleChangeEmitted when the selected billing cycle changes. Receives the new cycle or null.

Apply a discount or coupon

All three components accept a discount. Pass either a discount code (the code a customer would type) or a discount ID from your provider dashboard. If you pass both, discountId wins.

<template>
  <!-- Apply by code -->
  <CheckoutButton prices="price_123" discount-code="LAUNCH20" return-to="/billing/success">
    Subscribe
  </CheckoutButton>

  <!-- Apply by ID -->
  <CheckoutButton prices="price_123" discount-id="promo_123" return-to="/billing/success">
    Subscribe
  </CheckoutButton>
</template>

On PricingCards, discount-code / discount-id apply to every plan, and any individual Price can override them with its own discountCode / discountId.

Discounts must exist in your provider dashboard, and — if you use catalog allowlists — be allowed for checkout. Otherwise the billing endpoints return 404.

Sometimes you need the customer to tick a box (terms of service, immediate digital access, ...) before they can pay.

CheckoutButton, PricingCards, and the provider-specific checkout components support an optional checkout-consent slot. When the slot is present, clicking the button first opens a confirmation dialog, and every enabled checkbox inside the slot must be checked before checkout continues.

This feature is entirely slot-driven:

  • If you don't pass the slot, checkout starts immediately, as usual.
  • The slot accepts any content you need, including several required acknowledgements.
  • It detects native checkboxes and the built-in shadcn-vue Checkbox component.

The slot receives these props:

PropTypeDescription
invalidbooleantrue after the customer tries to continue while at least one required checkbox is unchecked.
errorbooleanAlias for invalid.
clearError() => voidClears the invalid state after the customer changes a checkbox.
pricePriceOnly available when the slot is rendered by the default PricingCards CTA.
<script setup lang="ts">
import CheckoutButton from '@/components/billing/CheckoutButton.vue';
import { Checkbox } from '@/components/ui/checkbox';
</script>

<template>
  <CheckoutButton prices="price_123" return-to="/billing/success">
    Subscribe

    <template #checkout-consent="{ invalid, clearError }">
      <div class="flex items-start gap-3 text-sm">
        <Checkbox
          id="checkout-terms"
          :invalid="invalid"
          @update:model-value="clearError"
        />
        <label for="checkout-terms">
          I accept the terms of service and privacy policy.
        </label>
      </div>

      <div class="flex items-start gap-3 text-sm">
        <Checkbox
          id="checkout-digital-access"
          :invalid="invalid"
          @update:model-value="clearError"
        />
        <label for="checkout-digital-access">
          I acknowledge that digital access starts immediately.
        </label>
      </div>
    </template>
  </CheckoutButton>
</template>
<script setup lang="ts">
import PricingCards from '@/components/billing/PricingCards.vue';
import { Checkbox } from '@/components/ui/checkbox';

const prices = [
  {
    id: 'price_123',
    productId: 'prod_basic',
    name: 'Basic',
    amount: 1500,
    currencyCode: 'USD',
    billingCycle: 'monthly',
  },
];
</script>

<template>
  <PricingCards :prices="prices" success-url="/billing/success">
    <template #checkout-consent="{ price, invalid, clearError }">
      <div class="flex items-start gap-3 text-sm">
        <Checkbox
          :id="`checkout-terms-${price.id}`"
          :invalid="invalid"
          @update:model-value="clearError"
        />
        <label :for="`checkout-terms-${price.id}`">
          I accept the purchase terms for {{ price.name }}.
        </label>
      </div>
    </template>
  </PricingCards>
</template>

With PricingCards, the checkout-consent slot is applied to the default checkout CTA of each price. If you replace the CTA with a custom #cta slot, render your own CheckoutButton there and pass the same checkout-consent slot to it.

When the customer tries to continue with a required checkbox left unchecked, the dialog shows a validation message. By default, this is the billing.checkout_legal.acceptance_error translation ("Check all boxes before continuing.").

Pass checkout-consent-error-text to CheckoutButton or PricingCards to override it with your own message:

<template>
  <CheckoutButton
    prices="price_123"
    return-to="/billing/success"
    checkout-consent-error-text="Please accept all terms before subscribing."
  >
    Subscribe

    <template #checkout-consent="{ invalid, clearError }">
      <!-- consent checkboxes -->
    </template>
  </CheckoutButton>
</template>

The same prop works on PricingCards, where it applies to every plan's consent dialog. Leave it unset (or pass an empty string) to keep the default localized message.

Going further

Provider-specific components

The provider-agnostic components above cover almost every case. For advanced integrations you can use the provider-specific components directly:

  • StripePricingCards.vue
  • PaddlePricingCards.vue
  • StripeCheckoutButton.vue
  • PaddleCheckoutButton.vue

BasePricingCards

BasePricingCards.vue (in resources/js/components/billing) is the shared visual pricing-card UI. It does not perform any provider API request or checkout action on its own. Prefer PricingCards in your pages unless you are building a custom provider-specific component.

Create a checkout session from your backend

The billing components already use the built-in routes and API endpoints to create checkout sessions, so you rarely need to do this yourself. But if you want custom behavior (per-campaign provider forcing, A/B experiments, dedicated private endpoints, ...), call BillingService::createCheckoutSession() directly.

Using the resolved payment provider (user-pinned when authenticated, otherwise the default):

<?php

use App\Services\BillingService;

$checkout = BillingService::createCheckoutSession(
    $request->user(),
    $prices,
    $quantity,
    route('checkout.complete')
);

Overriding the provider and applying a discount:

<?php

use App\Services\BillingService;

$checkout = BillingService::createCheckoutSession(
    $request->user(),
    $prices,
    $quantity,
    route('checkout.complete'),
    [
        'provider' => $provider,                  // force a specific enabled provider
        'discount_code' => $discountCode,
        'discount_id' => $discountId,             // takes precedence over discount_code
        'subscription_type' => $subscriptionType, // start a recurring subscription of this type
        'customer_email' => $email,               // pre-fill / match a guest by email
        'embedded' => true,                       // Stripe embedded checkout (returns a client_secret)
    ]
);

Pass subscription_type (a configured subscription slug) to start a recurring subscription instead of a one-time charge. The price must belong to that subscription group, or the call returns 404.

The returned payload shape is provider-specific:

  • Stripe returns a redirect url (send the customer there) — or, when embedded is true, a client_secret to mount embedded checkout.
  • Paddle returns transaction, items, and customer data for Paddle.js to open the overlay.

Routes and API endpoints

You normally interact with billing through the components above, but the underlying routes are available if you need them.

Web routes

  • GET /billing/checkout (billing.checkout): redirect to checkout for the resolved provider (user-pinned provider when authenticated, otherwise the active/default provider).
  • GET /billing/portal (billing.portal): redirect to the resolved Stripe/Paddle customer portal.
  • GET /buy/{product} (billing.products.buy): the product buy page / checkout for a configured product.
  • GET /subscribe/{subscription}/{plan} (billing.subscriptions.subscribe): the subscribe page / checkout for a configured plan.
  • GET /billing/thank-you (billing.thank-you): the thank-you page customers return to after checkout.

buy, subscribe, and thank-you are registered with Route::localized(...), so their URLs carry the active locale prefix. Generate them with route() (which adds the prefix for you) rather than hard-coding paths.

The account billing page lives under /account/billing (authenticated users):

  • GET /account/billing (account.billing.index): the account billing page.
  • POST /account/billing/subscription (account.billing.subscription.update): switch to another plan in a group.
  • POST /account/billing/subscription/cancel (account.billing.subscription.cancel): cancel a subscription.
  • POST /account/billing/subscription/cancel-pending-change (account.billing.subscription.cancel-pending-change): void a pending Stripe plan change.
  • POST /account/billing/subscription/resume (account.billing.subscription.resume): resume a subscription scheduled to cancel.

API routes

Billing API routes live under /api/billing:

  • Public:
    • POST /api/billing/checkout/init (api.billing.checkout.init) — create a checkout session. Accepts an optional email (used to match/create a guest account) and an embedded flag for Stripe embedded checkout.
    • GET /api/billing/prices (api.billing.prices.index) — list prices for the resolved provider.
    • GET /api/billing/discounts (api.billing.discounts.index) — look up a single discount by discount_id or discount_code.
  • Authenticated users:
    • POST /api/billing/portal (api.billing.portal) — get the customer portal URL.
    • GET /api/billing/customer (api.billing.customer) — get the current customer payload.
    • POST /api/billing/subscriptions/sync (api.billing.subscriptions.sync) — refresh the user's subscriptions from the provider and return the account billing data.
    • GET /api/billing/history (api.billing.history.index) — cursor-paginated billing history (cursor, per_page 1–25).
    • GET /api/billing/invoices/{invoice}/open (api.billing.invoices.open) — redirect to a hosted invoice, PDF, or receipt (document, disposition), after checking ownership.
    • GET /api/billing/payments/{session}/receipt (api.billing.payments.receipt) — redirect to a Stripe one-time payment receipt, after checking ownership.
    • GET /api/billing/information (api.billing.information.show) — the customer's billing information.
    • GET /api/billing/payment-methods (api.billing.payment-methods.index) — the customer's saved payment methods.
  • Authenticated admins only:
    • GET /api/billing/products/all (api.billing.products.all)
    • GET /api/billing/discounts/all (api.billing.discounts.all)
    • GET /api/billing/prices/all (api.billing.prices.all)

The no-code config editors add admin-only web routes too: GET/PUT /admin/billing/products (admin.billing.products / .update) and GET/PUT /admin/billing/subscriptions (admin.billing.subscriptions / .update).

Provider resolution rules:

  • Built-in checkout, customer, portal, history, information, and payment-methods endpoints resolve by user billing history when authenticated, otherwise they use the configured default provider.
  • Public/admin catalog endpoints (prices, discounts, products/all, discounts/all, prices/all) use the active/default provider from I_S_DEFAULT_PAYMENT_PROVIDER.
  • Provider-specific webhook routes are registered only for providers enabled through I_S_ENABLED_PAYMENT_PROVIDERS (plus the default provider).

Types and interfaces

On this page