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
apiKey?string
Paddle API key, sent as a Bearer credential.
stringserver?'production' | 'sandbox'
Selects api.paddle.com or sandbox-api.paddle.com.
'production' | 'sandbox'productionbaseUrl?string
Overrides server; used verbatim.
stringfetch?typeof fetch
Custom fetch implementation.
typeof fetchAuthentication
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.checkoutRefis the price ID (pri_…), not the product ID.Product.idis thepro_….- A unified
Checkoutis a Paddle transaction.checkouts.get({ id })readsGET /transactions/{id}. - Amounts are string integers on the wire (
"2499") and are parsed into numbers in minor units. customerEmailresolves to a customer. Paddle transactions take acustomer_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
reasonorcommentthrowsunsupported. - Cancel and revoke are the same endpoint.
subscriptions.cancelpostseffective_from: next_billing_period;subscriptions.revokepostseffective_from: immediately. - Uncancel clears the schedule with
PATCH { scheduled_change: null }. PATCHlist fields are full replacements.itemsis 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
itemschanges. OmittingprorationBehaviorbehaves asprorate(prorated_next_billing_period). management_urlsis absent from webhook payloads, so portal links must come fromcustomerPortal.createSession(POST /customers/{id}/portal-sessions).returnUrlis unsupported.