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

Provision access and handle subscription state

Use webhooks to keep your app in sync with Paddle, then decide what access to grant based on subscription state.

AI summary

Use subscription webhooks to keep your app in sync with Paddle and grant or revoke customer access based on subscription status events like activation, pause, cancellation, and payment failure.

  • • Subscribe to subscription.created, subscription.updated, subscription.paused, subscription.canceled, and subscription.past_due webhooks to handle all key lifecycle state changes.
  • • Always verify webhook signatures before trusting and acting on webhook payloads — use the Paddle webhook verification library or implement HMAC-SHA256 validation manually.
  • • Grant access on subscription.created; restrict it on subscription.paused and subscription.past_due; remove it on subscription.canceled.

Provisioning is how you grant customers access to your app, as well as determining which features they should have access to. It's sometimes called fulfillment. For example:

  • When customers sign up, set them up with an account for your app.
  • If customers add or remove products for additional modules, give them access to relevant features in your app.
  • Where subscriptions are paused or canceled, limit or stop access to your app.

Choose an integration shape

There are two patterns. Pick the one that matches how your app needs to make access decisions:

  • Lean cache Recommended
    Store only the fields needed for access decisions via webhooks. For richer billing data, fetch from the Paddle API on demand. Minimal write volume and works well for most apps.
  • State mirror
    Mirror the full subscription and transaction state into your database. Access checks and billing displays all read locally, so there's no runtime dependency on Paddle's API. Requires a reconciliation job to repair drift if you miss events.

What to listen for

We recommend setting up webhooks for the following events:

EventPurpose
subscription.createdA new subscription exists. Save the customer and subscription IDs against your user, and grant access.
subscription.updatedSomething about the subscription has changed, like its status, items, scheduled changes, or billing period. Re-read the cached fields you store.

You don't need to listen for separate events for renewals, upgrades or downgrades, or status changes. subscription.updated covers them all.

Depending on your setup, you may also want to listen for:

  • customer.created and customer.updated if you don't already capture customer details at signup.
  • transaction.completed if you bill one-off charges and need to record them locally.

To see exactly which events occur for common scenarios, use the webhook simulator.

What to store

For lean cache, store just enough to identify the customer in Paddle and answer simple access checks without an API call:

FieldWhat it's for
customer.idThe link between your user and Paddle. Save it once, against the user record.
subscription.idLook up or update the subscription via the API.
subscription.statusCheap access check without an API call.
subscription.items[].price.id and subscription.items[].price.product_idMap to features in your app.
subscription.scheduled_change.effective_atKnow when a paused or canceled subscription stops giving access.

A minimal schema looks like this:

sql
create table customers (
customer_id text primary key,
user_id uuid not null references users(id),
email text not null
);
create table subscriptions (
subscription_id text primary key,
customer_id text not null references customers(customer_id),
status text not null,
price_id text,
product_id text,
scheduled_change_at timestamptz,
updated_at timestamptz not null default now()
);

Gate subscription access

Status

Your access decision usually comes down to subscription.status and scheduled_change:

flowchart LR
    A[trialing] --> B[active]
    B <--> C[past_due]
    B --> D[paused]
    D --> B
    B --> E[canceled]
    C --> E
StatusAccessNotes
trialingFullTreat the same as active.
activeFullFull access to your app.
past_dueFullShow a banner and link to the customer portal so the customer can update their payment method. Paddle Retain automatically retries payment for you.
pausedNone or read-onlyNo transactions are created while paused.
canceledNoneRevoke access. Only revoke after the scheduled change takes effect and the status chances to canceled.

Entitlements

If you have a SaaS app with good-better-best or single option pricing, you can map a product_id to a set of features in code. For example, when a user purchases the "Pro" product, they get access to the "Pro" features.

If you offer addons or other products that mix and match across plans, store an array of product IDs for each user in your database.

Common scenarios

How to react to the things that actually happen, without reasoning about which event fires when.

  • New signup
    On subscription.created, save the customer_id against your user and grant access.
  • Upgrade or downgrade
    On subscription.updated, refresh the cached price_id and product_id. Update which features the customer has access to.
  • Past due
    Keep access. Show a banner directing the customer to the customer portal to update their payment method.
  • Pause or resume On subscription.updated, gate access by the new status. Resumed subscriptions go straight back to active (or past_due).
  • Cancellation
    If scheduled_change.effective_at is in the future, revoke access at that time. If the subscription is already canceled, revoke now.

Best practices

  • Paddle guarantees at-least-once delivery, so you may receive the same event more than once. Dedupe on event_id. See how webhooks work.
  • Events can arrive out of order. Compare occurred_at with the value you last stored before applying an update.
  • Run a periodic reconciliation job that lists subscriptions for active customers via the API and repairs any drift between Paddle and your cache.

If you can't store anything

For performance and scalability, we strongly recommend storing information about subscriptions in a database and using webhooks to keep that information up-to-date.

If your app can't persist local state, drive everything from the API using the customer's ID:

Was this page helpful?