> For the complete documentation index, see [llms.txt](https://developer.paddle.com/llms.txt).

# 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.

---

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** {% badge label="Recommended" variant="outline" /%}  
  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:

| Event | Purpose |
|---|---|
| `subscription.created` | A new subscription exists. Save the customer and subscription IDs against your user, and grant access. |
| `subscription.updated` | Something 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](https://developer.paddle.com/webhooks/test-webhooks.md).

## What to store

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

| Field | What it's for |
|---|---|
| `customer.id` | The link between your user and Paddle. Save it once, against the user record. |
| `subscription.id` | Look up or update the subscription via the API. |
| `subscription.status` | Cheap access check without an API call. |
| `subscription.items[].price.id` and `subscription.items[].price.product_id` | Map to features in your app. |
| `subscription.scheduled_change.effective_at` | Know 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()
);
```

{% callout type="note" %}
Use the [customer portal](https://developer.paddle.com/concepts/sell/customer-portal.md) to let customers update their payment details, pause or cancel, and grab invoices. The portal is included with Paddle and handles those flows for you.
{% /callout %}

## Gate subscription access

### Status

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

```mermaid
flowchart LR
    A[trialing] --> B[active]
    B <--> C[past_due]
    B --> D[paused]
    D --> B
    B --> E[canceled]
    C --> E
```

| Status | Access | Notes |
|---|---|---|
| `trialing` | Full | Treat the same as `active`. |
| `active` | Full | Full access to your app. |
| `past_due` | Full | Show a banner and link to the customer portal so the customer can update their payment method. Paddle Retain automatically retries payment for you. |
| `paused` | None or read-only | No transactions are created while paused. |
| `canceled` | None | Revoke access. Only revoke after the scheduled change takes effect and the status chances to canceled. |

{% callout type="warning" %}
You can't update items on a subscription when it has a pending scheduled change. If you let customers self-serve plan changes, gate that UI when `scheduled_change` is set.
{% /callout %}

### 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](https://developer.paddle.com/concepts/sell/customer-portal.md) 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](https://developer.paddle.com/webhooks/how-webhooks-work.md).
- 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:

- [List transactions](https://developer.paddle.com/api-reference/transactions/list-transactions.md) and [list subscriptions](https://developer.paddle.com/api-reference/subscriptions/list-subscriptions.md) accept a `customer_id` query parameter.
- [List transactions](https://developer.paddle.com/api-reference/transactions/list-transactions.md) accepts a `subscription_id` parameter to return a subscription's transactions.
- [Get a transaction](https://developer.paddle.com/api-reference/transactions/get-transaction.md) and [get a subscription](https://developer.paddle.com/api-reference/subscriptions/get-subscription.md) support `include` parameters to return related entities in one request.