Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
On this page

Paddle

Configure the Paddle provider — API key, sandbox, the Paddle.js checkout requirement, webhooks, capabilities, and quirks.

Paddle is a merchant of record. Import the factory from revenue-sdk/paddle.

import { createClient } from 'revenue-sdk';
import { paddle } from 'revenue-sdk/paddle';

const client = createClient({
  provider: paddle({ apiKey: process.env.PADDLE_API_KEY! }),
});

Factory options

PropType
apiKey?string

Paddle API key, sent as a Bearer credential.

Typestring
server?'production' | 'sandbox'

Selects api.paddle.com or sandbox-api.paddle.com.

Type'production' | 'sandbox'
Defaultproduction
baseUrl?string

Overrides server; used verbatim.

Typestring
fetch?typeof fetch

Custom fetch implementation.

Typetypeof fetch

Authentication

Create an API key in the Paddle dashboard under Developer tools → Authentication. The SDK pins Paddle-Version: 1 on every request.

Sandbox

Paddle’s sandbox is a separate environment with its own dashboard, its own keys, and its own catalog:

paddle({ apiKey: process.env.PADDLE_SANDBOX_API_KEY!, server: 'sandbox' });

Checkout requires Paddle.js on your own domain

const checkout = await client.checkouts.create({
  items: [{ product: 'pri_123', quantity: 1 }],
  customerEmail: 'ada@example.com',
  metadata: { userId: 'user_123' },
});

// Do NOT redirect blindly. Open your own Paddle.js page and pass the transaction.
renderPaddleCheckout(checkout.id);

On your page:

<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
<script>
  Paddle.Environment.set('sandbox');
  Paddle.Initialize({ token: 'live_or_test_client_side_token' });
  Paddle.Checkout.open({
    transactionId: transactionId,
    settings: { successUrl: 'https://example.com/thanks' },
  });
</script>

Gate on the capability in provider-agnostic code:

if (client.capabilities.hostedCheckout) {
  redirect(checkout.url);
} else {
  renderPaddleCheckout(checkout.id);
}

Webhooks

Create a notification destination in Developer tools → Notifications, choose “Webhook”, and copy the secret key Paddle generates for it.

import { parseWebhookEvent, verifyWebhook } from 'revenue-sdk/paddle';

const headers = request.headers;
const body = await request.text();

if (!(await verifyWebhook({ headers, body, secret: env.PADDLE_WEBHOOK_SECRET }))) {
  return new Response('invalid signature', { status: 401 });
}

const event = await parseWebhookEvent({ headers, body });

The Paddle-Signature header has the form ts=…;h1=…, where h1 is a hex HMAC-SHA256 of `${ts}:${rawBody}` keyed by the secret used verbatim. Multiple h1 values can appear during secret rotation, and any match is accepted. Deliveries older than 300 seconds are rejected — a friendlier bound than Paddle’s own 5-second default, which is hostile to serverless clock skew and retries.

Events worth subscribing to: subscription.created, subscription.activated, subscription.updated, subscription.trialing, subscription.past_due, subscription.paused, subscription.resumed, subscription.canceled, transaction.completed.

Capabilities

Capability Value
cancellationReason false
checkoutStatus true
checkoutSuccessUrl false
endTrial true
hostedCheckout false
listSubscriptionsByCustomer true
portalReturnUrl false
prorationBehaviors ['invoice_now', 'none', 'prorate']
revoke true
uncancel true

Quirks

  • Price.checkoutRef is the price ID (pri_…), not the product ID. Product.id is the pro_….
  • A unified Checkout is a Paddle transaction. checkouts.get({ id }) reads GET /transactions/{id}.
  • Amounts are string integers on the wire ("2499") and are parsed into numbers in minor units.
  • customerEmail resolves to a customer. Paddle transactions take a customer_id, so the SDK looks the email up and creates the customer if it doesn’t exist. That costs one or two extra requests.
  • No cancellation reasons. Passing reason or comment throws unsupported.
  • Cancel and revoke are the same endpoint. subscriptions.cancel posts effective_from: next_billing_period; subscriptions.revoke posts effective_from: immediately.
  • Uncancel clears the schedule with PATCH { scheduled_change: null }.
  • PATCH list fields are full replacements. items is replaced wholesale, so a plan change swaps every item for the new price. The unified model targets single-product subscriptions.
  • Proration is always sent on a plan change, because Paddle requires the field whenever items changes. Omitting prorationBehavior behaves as prorate (prorated_next_billing_period).
  • management_urls is absent from webhook payloads, so portal links must come from customerPortal.createSession (POST /customers/{id}/portal-sessions). returnUrl is unsupported.

Last updated on August 6, 2026

Was this page helpful?