Paddle Integration
Receive and route Paddle Billing (v2) webhooks for transactions, subscriptions, customers, and other billing events. Paddle is a Merchant of Record — it handles tax calculation, compliance, and invoicing on your behalf.
Setup
1. Create a Source in Hookbase
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Paddle Production",
"slug": "paddle",
"verificationConfig": {
"type": "hmac",
"secret": "pdl_ntf_...",
"algorithm": "sha256",
"header": "Paddle-Signature",
"encoding": "hex"
}
}'Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/paddle2. Configure Paddle Webhook
- Go to Paddle Dashboard → Developer Tools → Notifications
- Click New destination
- Enter your Hookbase webhook URL
- Select the events you want to receive
- Click Save
- Copy the Secret key (starts with
pdl_ntf_) - Update your Hookbase source with this secret
TIP
Paddle provides separate API keys for sandbox and live environments. Make sure you use the notification secret from the correct environment.
3. Create Routes
# Create destination for your billing handler
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-d '{"name": "Billing Service", "url": "https://api.myapp.com/webhooks/paddle"}'
# Create route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-d '{"name": "Paddle to Billing Service", "sourceId": "src_...", "destinationIds": ["dst_..."]}'Signature Verification
Paddle signs webhooks with HMAC-SHA256 using your notification secret key. The signature is sent in the Paddle-Signature header with the following format:
Paddle-Signature: ts=1671552777;h1=a]b2c3d4e5f6...The header contains two components:
ts— Unix timestamp of when the notification was senth1— HMAC-SHA256 signature computed overts:request_body
Hookbase automatically verifies this when configured:
{
"verificationConfig": {
"type": "hmac",
"secret": "pdl_ntf_...",
"algorithm": "sha256",
"header": "Paddle-Signature",
"encoding": "hex"
}
}Timestamp Tolerance
Paddle signatures include a timestamp. Hookbase rejects webhooks older than 5 minutes by default to prevent replay attacks.
How Verification Works
- Hookbase extracts the
tsandh1values from thePaddle-Signatureheader - Constructs the signed payload as
ts:request_body - Computes HMAC-SHA256 using your notification secret
- Compares the computed signature against
h1
Common Events
transaction.completed
Triggered when a transaction is successfully completed (payment captured).
{
"event_id": "evt_01hv8x3f2yqm5g0t4kwzr6jm8n",
"event_type": "transaction.completed",
"occurred_at": "2024-03-15T14:30:00.000Z",
"notification_id": "ntf_01hv8x3f2yqm5g0t4kwzr6abc",
"data": {
"id": "txn_01hv8x3f2yqm5g0t4kwzr6def",
"status": "completed",
"customer_id": "ctm_01hv8x3f2yqm5g0t4kwzr6ghi",
"currency_code": "USD",
"items": [
{
"price": {
"id": "pri_01hv8x3f2yqm5g0t4kwzr6jkl",
"product_id": "pro_01hv8x3f2yqm5g0t4kwzr6mno",
"unit_price": {
"amount": "2999",
"currency_code": "USD"
},
"tax_mode": "account_setting"
},
"quantity": 1
}
],
"details": {
"totals": {
"subtotal": "2999",
"tax": "570",
"total": "3569",
"grand_total": "3569",
"currency_code": "USD"
},
"tax_rates_used": [
{
"tax_rate": "0.19",
"totals": {
"subtotal": "2999",
"tax": "570",
"total": "3569"
}
}
]
},
"payments": [
{
"payment_method_id": "paymtd_01hv8x3f2yqm5g0t4kwzpqr",
"amount": "3569",
"status": "captured",
"captured_at": "2024-03-15T14:30:00.000Z"
}
],
"created_at": "2024-03-15T14:28:00.000Z",
"updated_at": "2024-03-15T14:30:00.000Z"
}
}subscription.created
Triggered when a new subscription is created.
{
"event_id": "evt_01hv9a2b3c4d5e6f7g8h9i0j1k",
"event_type": "subscription.created",
"occurred_at": "2024-03-15T14:30:00.000Z",
"notification_id": "ntf_01hv9a2b3c4d5e6f7g8h9i0abc",
"data": {
"id": "sub_01hv9a2b3c4d5e6f7g8h9i0def",
"status": "active",
"customer_id": "ctm_01hv8x3f2yqm5g0t4kwzr6ghi",
"currency_code": "USD",
"billing_cycle": {
"interval": "month",
"frequency": 1
},
"current_billing_period": {
"starts_at": "2024-03-15T14:30:00.000Z",
"ends_at": "2024-04-15T14:30:00.000Z"
},
"items": [
{
"price": {
"id": "pri_01hv8x3f2yqm5g0t4kwzr6jkl",
"product_id": "pro_01hv8x3f2yqm5g0t4kwzr6mno",
"unit_price": {
"amount": "2999",
"currency_code": "USD"
}
},
"quantity": 1,
"status": "active"
}
],
"scheduled_change": null,
"started_at": "2024-03-15T14:30:00.000Z",
"created_at": "2024-03-15T14:30:00.000Z",
"updated_at": "2024-03-15T14:30:00.000Z"
}
}subscription.canceled
Triggered when a subscription is canceled. The subscription remains active until the end of the current billing period.
{
"event_id": "evt_01hv9b3c4d5e6f7g8h9i0j1k2l",
"event_type": "subscription.canceled",
"occurred_at": "2024-03-20T09:15:00.000Z",
"notification_id": "ntf_01hv9b3c4d5e6f7g8h9i0j1abc",
"data": {
"id": "sub_01hv9a2b3c4d5e6f7g8h9i0def",
"status": "canceled",
"customer_id": "ctm_01hv8x3f2yqm5g0t4kwzr6ghi",
"currency_code": "USD",
"billing_cycle": {
"interval": "month",
"frequency": 1
},
"current_billing_period": {
"starts_at": "2024-03-15T14:30:00.000Z",
"ends_at": "2024-04-15T14:30:00.000Z"
},
"canceled_at": "2024-03-20T09:15:00.000Z",
"scheduled_change": {
"action": "cancel",
"effective_at": "2024-04-15T14:30:00.000Z"
},
"created_at": "2024-03-15T14:30:00.000Z",
"updated_at": "2024-03-20T09:15:00.000Z"
}
}transaction.payment_failed
Triggered when a payment attempt fails.
{
"event_id": "evt_01hv9c4d5e6f7g8h9i0j1k2l3m",
"event_type": "transaction.payment_failed",
"occurred_at": "2024-03-15T14:35:00.000Z",
"notification_id": "ntf_01hv9c4d5e6f7g8h9i0j1k2abc",
"data": {
"id": "txn_01hv9c4d5e6f7g8h9i0j1k2def",
"status": "past_due",
"customer_id": "ctm_01hv8x3f2yqm5g0t4kwzr6ghi",
"currency_code": "USD",
"items": [
{
"price": {
"id": "pri_01hv8x3f2yqm5g0t4kwzr6jkl",
"product_id": "pro_01hv8x3f2yqm5g0t4kwzr6mno",
"unit_price": {
"amount": "2999",
"currency_code": "USD"
}
},
"quantity": 1
}
],
"payments": [
{
"payment_method_id": "paymtd_01hv8x3f2yqm5g0t4kwzpqr",
"amount": "3569",
"status": "error",
"error_code": "declined"
}
],
"created_at": "2024-03-15T14:28:00.000Z",
"updated_at": "2024-03-15T14:35:00.000Z"
}
}subscription.past_due
Triggered when a subscription enters past-due status due to a failed payment.
{
"event_id": "evt_01hv9d5e6f7g8h9i0j1k2l3m4n",
"event_type": "subscription.past_due",
"occurred_at": "2024-03-15T14:35:00.000Z",
"notification_id": "ntf_01hv9d5e6f7g8h9i0j1k2l3abc",
"data": {
"id": "sub_01hv9a2b3c4d5e6f7g8h9i0def",
"status": "past_due",
"customer_id": "ctm_01hv8x3f2yqm5g0t4kwzr6ghi",
"currency_code": "USD",
"billing_cycle": {
"interval": "month",
"frequency": 1
},
"created_at": "2024-03-15T14:30:00.000Z",
"updated_at": "2024-03-15T14:35:00.000Z"
}
}customer.created
Triggered when a new customer is created in Paddle.
{
"event_id": "evt_01hv9e6f7g8h9i0j1k2l3m4n5o",
"event_type": "customer.created",
"occurred_at": "2024-03-15T14:25:00.000Z",
"notification_id": "ntf_01hv9e6f7g8h9i0j1k2l3m4abc",
"data": {
"id": "ctm_01hv8x3f2yqm5g0t4kwzr6ghi",
"email": "[email protected]",
"name": "Jane Smith",
"locale": "en",
"marketing_consent": false,
"status": "active",
"created_at": "2024-03-15T14:25:00.000Z",
"updated_at": "2024-03-15T14:25:00.000Z"
}
}Paddle Event Types
| Category | Events |
|---|---|
| Transactions | transaction.billed, transaction.canceled, transaction.completed, transaction.created, transaction.paid, transaction.past_due, transaction.payment_failed, transaction.ready, transaction.updated |
| Subscriptions | subscription.activated, subscription.canceled, subscription.created, subscription.imported, subscription.past_due, subscription.paused, subscription.resumed, subscription.trialing, subscription.updated |
| Customers | customer.created, customer.imported, customer.updated |
| Addresses | address.created, address.imported, address.updated |
| Adjustments | adjustment.created, adjustment.updated |
| Discounts | discount.created, discount.imported, discount.updated |
| Payouts | payout.created, payout.paid |
| Reports | report.created, report.updated |
Transform Examples
Slack Alert for New Subscription
function transform(payload) {
const sub = payload.data;
const item = sub.items?.[0];
const price = item?.price?.unit_price;
const amount = price ? (parseInt(price.amount) / 100).toFixed(2) : '0.00';
const currency = price?.currency_code || sub.currency_code || 'USD';
const interval = sub.billing_cycle
? `${sub.billing_cycle.frequency} ${sub.billing_cycle.interval}`
: 'one-time';
return {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "New Paddle Subscription"
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Amount:*\n${currency} ${amount}`
},
{
type: "mrkdwn",
text: `*Billing Cycle:*\n${interval}`
},
{
type: "mrkdwn",
text: `*Customer:*\n${sub.customer_id}`
},
{
type: "mrkdwn",
text: `*Status:*\n${sub.status}`
}
]
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `Subscription ID: ${sub.id} | Event: ${payload.event_type}`
}
]
}
]
};
}Database Format
function transform(payload) {
const data = payload.data;
const item = data.items?.[0];
const price = item?.price?.unit_price;
return {
paddle_event_id: payload.event_id,
event_type: payload.event_type,
object_id: data.id,
customer_id: data.customer_id,
status: data.status,
currency_code: data.currency_code,
amount: price ? parseInt(price.amount) : null,
product_id: item?.price?.product_id || null,
price_id: item?.price?.id || null,
billing_interval: data.billing_cycle?.interval || null,
billing_frequency: data.billing_cycle?.frequency || null,
occurred_at: payload.occurred_at,
created_at: data.created_at,
updated_at: data.updated_at
};
}Alert for Failed Payments
function transform(payload) {
const txn = payload.data;
const payment = txn.payments?.[0];
const item = txn.items?.[0];
const price = item?.price?.unit_price;
const amount = price ? (parseInt(price.amount) / 100).toFixed(2) : '0.00';
return {
channel: "#billing-alerts",
username: "Paddle Bot",
icon_emoji: ":warning:",
attachments: [{
color: "danger",
title: "Payment Failed",
fields: [
{ title: "Customer", value: txn.customer_id, short: true },
{ title: "Amount", value: `$${amount} ${txn.currency_code}`, short: true },
{ title: "Error", value: payment?.error_code || "Unknown", short: true },
{ title: "Transaction", value: txn.id, short: true }
],
footer: `Event: ${payload.event_id}`,
ts: Math.floor(new Date(payload.occurred_at).getTime() / 1000)
}]
};
}Subscription Cancellation Alert
function transform(payload) {
const sub = payload.data;
const effectiveAt = sub.scheduled_change?.effective_at;
return {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "Subscription Canceled"
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Customer:*\n${sub.customer_id}`
},
{
type: "mrkdwn",
text: `*Subscription:*\n${sub.id}`
},
{
type: "mrkdwn",
text: `*Canceled At:*\n${sub.canceled_at}`
},
{
type: "mrkdwn",
text: `*Effective:*\n${effectiveAt || 'Immediately'}`
}
]
}
]
};
}Filter Examples
Transaction Events Only
{
"name": "Transaction Events",
"logic": "and",
"conditions": [
{
"field": "event_type",
"operator": "starts_with",
"value": "transaction"
}
]
}Subscription Events
{
"name": "Subscription Events",
"logic": "and",
"conditions": [
{
"field": "event_type",
"operator": "starts_with",
"value": "subscription"
}
]
}Failed Payments and Past Due
{
"name": "Payment Issues",
"logic": "or",
"conditions": [
{
"field": "event_type",
"operator": "equals",
"value": "transaction.payment_failed"
},
{
"field": "event_type",
"operator": "equals",
"value": "subscription.past_due"
}
]
}High-Value Transactions ($100+)
{
"name": "High Value ($100+)",
"logic": "and",
"conditions": [
{
"field": "event_type",
"operator": "equals",
"value": "transaction.completed"
},
{
"field": "data.details.totals.grand_total",
"operator": "greater_than",
"value": "10000"
}
]
}Headers
Paddle sends these headers with webhook notifications:
| Header | Description |
|---|---|
Paddle-Signature | HMAC-SHA256 signature with timestamp (ts=...;h1=...) |
Content-Type | Always application/json |
User-Agent | Paddle user agent string |
Testing
Using Paddle Sandbox
Paddle provides a full sandbox environment for testing:
- Create a sandbox account at sandbox-vendors.paddle.com
- Set up a separate Hookbase source for sandbox webhooks:
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Paddle Sandbox",
"slug": "paddle-sandbox",
"verificationConfig": {
"type": "hmac",
"secret": "pdl_ntf_sandbox_...",
"algorithm": "sha256",
"header": "Paddle-Signature",
"encoding": "hex"
}
}'- Configure webhook notifications in the sandbox dashboard
- Use Paddle's test cards to trigger events
Simulate Notifications
Use the Paddle API to replay or simulate notifications:
curl -X POST https://sandbox-api.paddle.com/notifications \
-H "Authorization: Bearer your_paddle_api_key" \
-H "Content-Type: application/json" \
-d '{
"notification_setting_id": "ntfset_01hv...",
"type": "transaction.completed",
"filter": "txn_01hv..."
}'Best Practices
Separate sandbox and live: Use different sources for sandbox and production webhooks
Handle idempotency: Use
event_idto prevent duplicate processing — Paddle may retry failed deliveriesTrack notification IDs: Store
notification_idalongsideevent_idfor debugging with Paddle supportUse scheduled changes: When a subscription is canceled, check
scheduled_change.effective_atto know when access should endHandle tax data: Paddle includes full tax breakdowns — store these for your financial records
Monitor past-due subscriptions: Set up alerts for
subscription.past_dueto proactively reach out to customersValidate amounts: Paddle amounts are in the smallest currency unit (e.g., cents) — divide by 100 for display
Troubleshooting
Signature Verification Failed
- Verify you're using the notification secret (starts with
pdl_ntf_) - Don't use the Paddle API key instead of the notification secret
- Use the correct secret for the environment (sandbox vs live)
- Ensure the verification config uses
"algorithm": "sha256"and"encoding": "hex"
Missing Events
- Check Developer Tools → Notifications in the Paddle dashboard for delivery status
- Verify the notification destination is active and not paused
- Confirm the events you need are selected in the notification settings
- Check filters aren't blocking expected events in Hookbase
Duplicate Events
- Paddle retries failed deliveries up to several times
- Implement idempotency using
event_id - Enable deduplication using the event ID:
curl -X PATCH .../sources/src_paddle \
-d '{"dedupEnabled": true, "dedupStrategy": "provider_id"}'Sandbox vs Live Confusion
- Sandbox and live environments have different API keys and secrets
- Sandbox webhook URLs: Use a separate Hookbase source with a distinct slug (e.g.,
paddle-sandbox) - Sandbox events come from
sandbox-api.paddle.com, live events fromapi.paddle.com