Paddle
Configure the Paddle provider — API key, sandbox, the Paddle.js checkout requirement, webhooks, and its limitations.
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 & test mode
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' });
Limitations
- No API-hosted checkout.
hostedCheckoutisfalseandcheckouts.create({ successUrl })throwsunsupported— the returned URL only works through your own Paddle.js page (see below). - No license keys.
licenseKeysisfalseand all fourclient.licenseKeysmethods throwunsupported: Paddle Billing dropped the Classic license feature with no equivalent. See License keys. - No usage API.
usage.reportthrowsunsupported. Paddle’s own guidance is to meter externally and bill the result as a one-time charge —POST /subscriptions/{id}/chargewith a price you compute yourself. See Usage-based billing. - No checkout expiry. A transaction carries no expiry field, so
checkouts.create({ expiresAt })throwsunsupportedandCheckout.expiresAtis alwaysundefined. - No pay-what-you-want.
checkouts.create({ customAmount })throwsunsupported. - No cancellation reasons. Passing
reasonorcommentthrowsunsupported. orders.list({ limit })clamps to 30./transactionsrejectsper_page > 30, unlike every other Paddle collection (200).
Full values for every capability: capability matrix.
Checkout requires Paddle.js on your own domain
const checkout = await client.checkouts.create({
items: [{ product: 'pri_123', quantity: 1 }],
customerEmail: '[email protected]',
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. Verification and parsing come from the subpath:
import { parseWebhookEvent, verifyWebhook } from 'revenue-sdk/paddle';
See Handle webhooks for the full handler.
Events worth subscribing to: subscription.created, subscription.activated, subscription.updated,
subscription.imported, subscription.trialing, subscription.past_due, subscription.paused,
subscription.resumed, subscription.canceled, transaction.completed.
- Scheduling a cancellation emits
subscription.updated(carryingscheduled_change), neversubscription.canceled— that only fires on the effective date. Undoing one arrives the same way, sosubscriptionChangeisundefinedfor both: readevent.subscription.cancelAtPeriodEndinstead. transaction.paidis left unmapped sotransaction.completedis the single “money received” signal and payments don’t fire twice.subscription.importedcovers subscriptions migrated in from another system and normalizes tosubscription.updated— upsert, don’t insert.
Orders
See Orders for the model. A unified Order is a Paddle transaction — the
same entity a unified Checkout maps to, read later in its life. Paddle specifics:
- Only completed transactions are listed.
draftandreadytransactions are abandoned checkouts, so the SDK filters them out and every listed order ispaid; the rest are only visible through a directorders.get. - Invoice URLs live for one hour, so they are fetched per click and never stored.
- A transaction that was never billed, or whose total is zero, has no invoice and
orders.getInvoiceUrlthrowsnot_found.
Provider notes
Price.checkoutRefis the price ID (pri_…), not the product ID (pro_…), and a unifiedCheckoutis a Paddle transaction —checkouts.get({ id })readsGET /transactions/{id}.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 — one or two extra requests.- A taken customer email is a
409.customers.createwith an existing address surfaces asRevenueError { code: 'conflict' }with the existing customer’s ID in the message. Paddle’s own checkout flow silently reuses that customer; the direct API call does not. 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.custom_datais replaced, never merged.customers.update({ metadata })sends the object as given. The SDK deliberately does not read-then-merge: that would make clearing a key impossible and would resurrect entries the caller left out.- Proration is always sent on a plan change, because Paddle requires the field whenever
itemschanges. OmittingprorationBehaviorbehaves asprorate. subscriptions.resumealways takes effect immediately, and Paddle’son_resumeoption is not exposed. Both pause behaviors are supported; a scheduled pause leavesstatusactivewithpauseAtPeriodEnd: trueuntil it takes effect.management_urlsis absent from webhook payloads, so portal links must come fromcustomerPortal.createSession.returnUrlis unsupported.