Paddle Billing API, Webhooks and web (Paddle.js) wrapper with detailed TypeScript types.
The library is available as an npm package.
To install the package run:
npm install paddle-billing
The minimum required Node.js version is v18, as it uses Fetch API. It's possible to use older versions with global.fetch
polyfill, but not recommended.
paddle-billing
wraps all available Paddle Billing API methods, replicating the naming structure. Every API component (query, body, response, etc) is carefully typed, so you can use those as the documentation or read the Paddle API Reference for more details.
To use a method, create a client with authentication details and call the method with it:
import { client, cancelSubscription } from "paddle-billing";
const paddle = client("PADDLE_SECRET");
cancelSubscription(paddle, "SUBCRIPTION_ID").then((subscription) => {
if (subsription.error) {
// The request failed:
console.error(subscription.error); // See PaddleAPI.Error
return;
}
// Do something with the subscription:
subscription.data;
});
To use the Sandbox, pass true
as the second argument to client
:
import { client, cancelSubscription } from "paddle-billing";
const paddle = client("PADDLE_SECRET", true);
// Will send the request to Sandbox:
cancelSubscription(paddle, "SUBCRIPTION_ID");
To add types to custom_data
fields to Price
, Product
, SubscriptionItem
, Subscription
, Transaction
, Customer
, Address
and Business
add the generic argument to client
:
const paddle = client<{
Product: CustomDataProduct;
Price: CustomDataPrice;
Transaction: CustomDataTransaction;
Subscription: CustomDataSubscription;
SubscriptionItem: CustomDataSubscriptionItem;
Customer: CustomDataCustomer;
Address: CustomDataAddress;
Business: CustomDataBusiness;
}>("PADDLE_SECRET");
From now on, all corresponding entities will have custom_data
typed.
⚠️ When specifingSubscription
andTransaction
, you should make sure they overlap. Fields that do not overlap should be optional. It's dictated by the web's custom data-assigning to relevant transaction and subscription simultaneously. Creating an API or web client with incompatible custom data definitions will result in the client function returningnever
.
All custom data fields are optional so that you can type only selected entities:
const paddle = client<{
Product: CustomDataProduct;
Price: CustomDataPrice;
}>("PADDLE_SECRET");
You can also pass a function that returns the key as the key argument, which allows the use of Google Cloud Secrets and calling client
on the module level where the secrets aren't defined in process
:
const paddle = client(() => "PADDLE_SECRET");
To verify and parse the Paddle webhook, use parseWebhookBody
function:
import express from "express";
import { parseWebhookBody } from "paddle-billing/webhooks";
const app = express();
app.use(express.raw());
// Use the webhook's secret that you get when creating it the Paddle admin:
const secret = process.env.PADDLE_WEBHOOK_SECRET;
app.get("/paddle-webhook", (request, response) => {
// Extract the webhook signature from the headers
const signature = request.headers["paddle-signature"];
if (!signature) {
response.status(400).send("Bad Request");
return;
}
// Parse the webhook
const webhook = parseWebhookBody(
null,
secret,
signature,
// ⚠️ the body must be raw string to parse!
request.body.toString()
);
// If the webhook is invalid, it will be null
if (!webhook) {
response.status(400).send("Bad Request");
return;
}
response.send("OK");
});
If you have custom data types defined, pass the client
as the first argument so that the custom data is correctly inferred:
const paddle = client<{
Product: CustomDataProduct;
Price: CustomDataPrice;
Transaction: CustomDataTransaction;
Subscription: CustomDataSubscription;
SubscriptionItem: CustomDataSubscriptionItem;
Customer: CustomDataCustomer;
Address: CustomDataAddress;
Business: CustomDataBusiness;
}>("PADDLE_SECRET");
// ...later:
const webhook = parseWebhookBody(
paddle,
secret,
signature,
request.body.toString()
);
The package also provides the web portion of the Paddle Billing platform replacing the first-party package @paddle/paddle-js
. Unlike the official package, this one provides more elaborate types and integration with custom data that you might use on the backend.
To load the web API (known as Paddle.js), use loadScript
:
import { loadScript } from "paddle-billing/web";
loadScript().then((Paddle) => {
Paddle.Checkout.open({
settings: {
displayMode: "overlay",
theme: "light",
locale: "en",
},
items: [
{
priceId: "pri_01gm81eqze2vmmvhpjg13bfeqg",
quantity: 1,
},
{
priceId: "pri_01gm82kny0ad1tk358gxmsq87m",
quantity: 1,
},
],
});
});
If you have custom data assigned to Paddle entities, use the loadScript
generic param, the same way as when creating the API client:
interface CustomData {
Transaction: AccountData;
Subscription: AccountData;
}
interface AccountData {
accountId: string;
}
const paddle = client<CustomData>("PADDLE_SECRET");
// ...later on web:
loadScript<CustomData>().then((Paddle) => {
Paddle.Checkout.open({
items: [
{
priceId: "pri_01gm81eqze2vmmvhpjg13bfeqg",
quantity: 1,
},
],
customData: {
accountId: "ACCOUNT_ID",
},
});
});
⚠️ When specifingSubscription
andTransaction
, you should make sure they overlap. Fields that do not overlap should be optional. It's dictated by the web's custom data-assigning to relevant transaction and subscription simultaneously. Creating an API or web client with incompatible custom data definitions will result in the client function returningnever
.
Read more about using custom data through API.