Become a Paddle partner
Join the Paddle partner program to read this content. You'll also get access to our partner API and agent tooling. Let us know a few details about your business to get started. Already a partner? Log in to view this page.
After checkout, a seller's app needs to grant customers access to what they paid for, as well as revoke it if they cancel. This is called provisioning or order fulfillment.
You can use webhooks to handle provisioning and keep a seller's app in sync with Paddle.
How it works
When a subscription or transaction changes state, Paddle sends a webhook to the seller's backend. You can keep a small local cache of the seller's customers and subscriptions, and let the seller's app gate access by reading that cache.
You should keep a lean cache, storing only the fields the app needs for access decisions. Fetch richer billing data from the API on demand.
sequenceDiagram
participant PD as Paddle
participant BE as Seller's backend
participant DB as Seller's database
participant App as Seller's app
PD->>BE: subscription.created / updated (webhook)
BE->>BE: Verify signature with the destination secret
BE->>DB: Upsert customer + subscription
App->>DB: Is this customer active?
DB-->>App: status → grant or deny access
Overview
- Create a webhook handler
Create an endpoint in the seller's backend, or create a serverless function, that acceptsPOSTrequests from Paddle. - Create a notification destination
Create a notification destination against the seller's account, pointing at the seller's webhook endpoint. - Create tables for customers and subscriptions
In the seller's database, keep a lean cache: store only the fields the app needs for access decisions. - Handle webhooks
When a subscription or customer lifecycle event occurs, Paddle sends a webhook to the seller's backend. Once an event's signature has been verified, update the cache. - Gate access from the database
To determine whether a customer has access, read the customer and subscription from the database and check the status.
Create a webhook handler
- Layer
- Your agent
Create an endpoint in the seller's backend, or create a serverless function, that accepts POST requests from Paddle. It needs to do two things:
- Respond within 5 seconds.
Return a2xxwithin five seconds to acknowledge receipt, then do the heavy work afterwards. Any other status is retried up to 3 times over 15 minutes in sandbox, and 60 times over 3 days in live. - Verify the signature.
Every webhook includes aPaddle-Signatureheader. Verify it against the destination's secret before trusting the payload.
For example, to verify with the Paddle Node SDK, you can use unmarshal() to check the signature and return a typed event:
import { Paddle, Environment } from "@paddle/paddle-node-sdk";
const paddle = new Paddle(process.env.PADDLE_API_KEY!, { environment: Environment.sandbox, // omit for live});
export async function handleWebhook( rawBody: string, // the raw request body, not parsed JSON signature: string,) { let event; try { event = await paddle.webhooks.unmarshal( rawBody, process.env.PADDLE_WEBHOOK_SECRET!, signature, ); } catch { return { status: 403 }; // signature didn't verify — reject }
await enqueue(event); // acknowledge now, process asynchronously return { status: 200 };}For manual verification and other languages, see Verify webhook signatures.
Create a notification destination
- Layer
- Your agent
- Authentication
- Seller API key
- Environment
- Sandbox, Live
A notification destination tells Paddle which events to send and where. Create one against the seller's account, pointing at the seller's webhook endpoint.
To create a notification destination, send a POST request to the /notification-settings endpoint with the endpoint URL and the events to subscribe to. We recommend subscribing to:
| Event | Why |
|---|---|
subscription.created | A new subscription exists. Grant access and store the IDs. |
subscription.updated | Status, items, or schedule changed. Refresh your cache. Covers renewals, upgrades, pauses, and cancellations. |
customer.created | A new customer exists. Store their ID. |
If successful, Paddle returns the destination with a secret key. Store this securely so the handler can verify webhook signatures.
{ "description": "Provisioning", "type": "url", "destination": "https://aeroedit.com/api/paddle/webhook", "api_version": 1, "traffic_source": "platform", "subscribed_events": [ "subscription.created", "subscription.updated", "customer.created" ]}{ "data": { "id": "ntfset_01gkpjp8bkm3tm53kdgkx6sms7", "description": "Provisioning", "type": "url", "endpoint_secret_key": "pdl_ntfset_01gkpjp8bkm3tm53kdgkx6sms7_6h3qd3uFSi9YCD3OLYAShQI90XTI5vEI", "destination": "https://aeroedit.com/api/paddle/webhook", "active": true, "api_version": 1, "include_sensitive_fields": false, "traffic_source": "platform", "subscribed_events": [ { "name": "subscription.created", "description": "Occurs when a subscription is created.", "group": "Subscription", "available_versions": [1] }, { "name": "subscription.updated", "description": "Occurs when a subscription is updated.", "group": "Subscription", "available_versions": [1] }, { "name": "customer.created", "description": "Occurs when a customer is created.", "group": "Customer", "available_versions": [1] } ] }, "meta": { "request_id": "a8f2e3d1-4b5c-6789-a0b1-c2d3e4f5a6b7" }}subscription.updated intentionally covers a lot: you don't need separate events for renewals, plan changes, or status transitions. See the full event reference for a complete list of events you can subscribe to.
Create tables for customers and subscriptions
- Layer
- Your agent
In the seller's database, keep a lean cache: store only the fields their app needs to make access decisions.
create table customers ( customer_id text primary key, -- Paddle customer ID user_id text not null, -- the buyer in the seller's app email text);
create table subscriptions ( subscription_id text primary key, -- Paddle subscription ID customer_id text not null references customers(customer_id), status text not null, -- drives access checks price_id text, product_id text, -- maps to features / entitlements scheduled_change_at timestamptz, -- when a paused or canceled sub loses access updated_at timestamptz not null default now());For data beyond the data you've stored, you can query the Paddle API on demand using the customer_id or subscription_id.
Handle webhooks
- Layer
- Your agent
- Authentication
- Webhook signature
- Environment
- Sandbox, Live
When a subscription or customer lifecycle event occurs, Paddle sends a webhook to the seller's backend.
Once an event's signature has been verified, update the cache. The SDK gives you typed events, so switch on the event type and upsert:
import { EventName } from "@paddle/paddle-node-sdk";
async function process(event) { switch (event.eventType) { case EventName.SubscriptionCreated: case EventName.SubscriptionUpdated: { const sub = event.data; await upsertSubscription({ subscriptionId: sub.id, customerId: sub.customerId, status: sub.status, priceId: sub.items[0]?.price?.id, productId: sub.items[0]?.price?.productId, scheduledChangeAt: sub.scheduledChange?.effectiveAt ?? null, }); break; } case EventName.CustomerCreated: await upsertCustomer({ customerId: event.data.id, email: event.data.email, }); break; }}When processing webhooks:
- Dedupe using
event_id.
Paddle guarantees at-least-once delivery, so you may see an event more than once if it's retried. Ignore events whoseevent_idyou've already processed. - Order using
occurred_at.
Events can arrive out of order. Compare the event'soccurred_atwith theupdated_atyou last stored, and skip stale updates.
Gate access from the database
- Layer
- Your agent
To determine whether a customer has access, read the customer and subscription from the database and check the status. A small helper is enough:
const ACTIVE = new Set(["active", "trialing", "past_due"]);
async function hasAccess(customerId: string): Promise<boolean> { const sub = await getSubscription(customerId); if (!sub) return false; if (!ACTIVE.has(sub.status)) return false; // A scheduled cancellation keeps access until it takes effect. if (sub.scheduledChangeAt && new Date(sub.scheduledChangeAt) <= new Date()) { return false; } return true;}Map status to access:
| Status | Access |
|---|---|
trialing, active | Full access. |
past_due | Keep access, but show a banner linking to the customer portal to update payment. Paddle Retain retries the payment automatically in live. |
paused | No access, or read-only. |
canceled | No access. |
For feature-level access, map product_id to the features in each plan and grant access accordingly. If a seller sells add-ons, store an array of product IDs per buyer.