Author picture Gareth Wilson

Guide to Paddle Webhooks: Features and Best Practices

Published


Paddle is a merchant of record platform that handles payments, subscriptions, taxes, and compliance for SaaS and software businesses. Unlike traditional payment processors, Paddle takes on the complexity of global tax calculations and remittance, allowing developers to focus on building their products. Webhooks are central to integrating Paddle into your application, enabling real-time synchronization between Paddle's billing events and your own systems.

This guide covers everything you need to know about Paddle webhooks, from initial setup to production best practices, along with common limitations developers face and practical solutions to overcome them.

Paddle webhook features

FeatureDetails
Event Types30+ event types covering transactions, subscriptions, customers, addresses, businesses, products, prices, discounts, adjustments, payment methods, payouts, and reports
Payload FormatJSON with consistent structure including event_id, event_type, occurred_at, and full entity data
SecurityHMAC-SHA256 signature verification via Paddle-Signature header
DeliveryAt-least-once delivery with automatic retries using exponential backoff
Retry PolicySandbox: 3 retries over 15 minutes · Live: 60 retries over 3 days
Timeout5-second response requirement
DestinationsUp to 10 active notification destinations per account
ConfigurationDashboard UI or API-based setup with per-destination event filtering
TestingBuilt-in webhook simulator for individual events and predefined scenarios
MonitoringDelivery logs and status tracking in dashboard and via API
Official SDKsNode.js, Python, Go, and PHP with built-in signature verification

What are Paddle webhooks?

Webhooks let you subscribe to events in Paddle. When a subscribed event occurs, Paddle sends an HTTP POST request to a webhook endpoint you specify, delivering a JSON payload containing information about the event and the affected entity. You can use these notifications to keep your application in sync with Paddle, trigger fulfilment workflows, or integrate with third-party systems.

For example, when a subscription cancels, Paddle sends a subscription.canceled webhook. When you receive this notification, you can update your application to revoke access for the canceled customer.

Paddle webhook event types

Paddle Billing organizes webhooks around a centralized event stream. Events follow the format entity.event_type and cover all major entities in the Paddle ecosystem.

Transaction events

Transactions represent individual payment attempts and completions:

  • transaction.created – A new transaction has been initiated
  • transaction.updated – Transaction details have changed
  • transaction.ready – Transaction is ready for payment collection
  • transaction.paid – Payment has been successfully collected
  • transaction.completed – Transaction has been fully processed
  • transaction.billed – Transaction has been marked as billed (for manual collection)
  • transaction.canceled – Transaction has been canceled
  • transaction.past_due – Payment collection has failed

Subscription events

Subscriptions are the core entity for recurring billing:

  • subscription.created – A new subscription has been created
  • subscription.updated – Subscription details have changed
  • subscription.trialing – Subscription has entered a trial period
  • subscription.activated – Subscription has become active
  • subscription.paused – Subscription has been paused
  • subscription.resumed – Subscription has resumed from a paused state
  • subscription.canceled – Subscription has been canceled
  • subscription.past_due – Subscription payment has failed
  • subscription.imported – Subscription was imported (during migration)

Customer events

  • customer.created – A new customer record has been created
  • customer.updated – Customer details have changed
  • customer.imported – Customer was imported (during migration)

Additional event categories

Paddle also provides webhooks for:

  • Addresses: address.created, address.updated, address.imported
  • Businesses: business.created, business.updated, business.imported
  • Products: product.created, product.updated, product.imported
  • Prices: price.created, price.updated, price.imported
  • Discounts: discount.created, discount.updated, discount.imported
  • Adjustments: adjustment.created, adjustment.updated (for refunds and credits)
  • Payment Methods: payment_method.saved, payment_method.deleted
  • Payouts: payout.created, payout.paid
  • Reports: report.created, report.updated

You can retrieve the complete list of event types by calling the Paddle API at GET https://api.paddle.com/event-types.

Webhook payload structure

Every Paddle webhook notification follows a consistent structure:

{
  "event_id": "evt_01hv8x2acma2gz7he8kg2s0hna",
  "event_type": "subscription.created",
  "occurred_at": "2024-04-12T10:18:49.621022Z",
  "notification_id": "ntf_01hv8x2af22vrrz7k67g06x1kq",
  "data": {
    "id": "sub_01hv8x29xpj8kp2r6d3fgm5nhk",
    "status": "active",
    "customer_id": "ctm_01hv8wz3k2e6bv9cj7rk4pqn1m",
    "address_id": "add_01hv8wzg6n4m2t8c3k5b7fjp2r",
    "business_id": null,
    "currency_code": "USD",
    "created_at": "2024-04-12T10:18:49.000000Z",
    "updated_at": "2024-04-12T10:18:49.000000Z",
    "started_at": "2024-04-12T10:18:49.000000Z",
    "first_billed_at": "2024-04-12T10:18:49.000000Z",
    "next_billed_at": "2024-05-12T10:18:49.000000Z",
    "billing_cycle": {
      "interval": "month",
      "frequency": 1
    },
    "items": [...],
    "custom_data": null,
    "transaction_id": "txn_01hv8x29wq8j6m2n3k4p5r7s9t"
  }
}

Key fields in the payload:

FieldDescription
event_idUnique identifier for the event (prefixed with evt_)
event_typeThe type of event in entity.event_type format
occurred_atRFC 3339 timestamp of when the event occurred
notification_idUnique identifier for this notification attempt (prefixed with ntf_)
dataThe complete entity object that was created or changed

Entity IDs use consistent prefixes: sub_ for subscriptions, txn_ for transactions, ctm_ for customers, add_ for addresses, biz_ for businesses, and so on.

Setting up Paddle webhooks

Creating a notification destination

You can configure webhook endpoints through the Paddle dashboard or via the API.

Using the Dashboard:

  1. Navigate to Developer Tools → Notifications in your Paddle dashboard
  2. Click Add Destination
  3. Enter your webhook endpoint URL (must be HTTPS for production)
  4. Select the event types you want to receive
  5. Save the destination

Using the API:

curl -X POST https://api.paddle.com/notification-settings \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Production webhook endpoint",
    "destination": "https://your-app.com/webhooks/paddle",
    "subscribed_events": [
      "subscription.created",
      "subscription.updated",
      "subscription.canceled",
      "transaction.completed",
      "transaction.paid"
    ],
    "active": true
  }'

You can create up to 10 active notification destinations, allowing you to route different event types to different endpoints or services.

Responding to webhooks

Your webhook endpoint must respond with an HTTP 2xx status code within five seconds. Paddle interprets any other status code or a timeout as a delivery failure.

Critical best practice: Such a short response time means you really must respond immediately, then process asynchronously.

Verifying webhook signatures

Every Paddle webhook includes a Paddle-Signature header that you should verify to ensure the request genuinely came from Paddle and hasn't been tampered with.

The signature header format is: ts=1671552777;h1=eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151

The signature is an HMAC-SHA256 hash of the timestamp concatenated with the raw request body, using your endpoint's secret key.

Getting your secret key:

  1. Go to Developer Tools → Notifications in your Paddle dashboard
  2. Edit your webhook destination
  3. Copy the Secret key value

Important: Do not transform, parse, or modify the raw request body before verification. Any changes (including pretty-printing or reordering JSON fields) will cause signature verification to fail.

Paddle SDKs

Paddle provides official SDKs that handle signature verification and payload parsing:

Retry behavior and delivery guarantees

Paddle implements automatic retries with exponential backoff when webhook delivery fails.

Retry schedule

EnvironmentTotal RetriesWindow
Sandbox3 retries15 minutes
Live60 retries3 days

For live accounts, the retry distribution is:

  • First 20 attempts within the first hour
  • 47 attempts within the first day
  • All 60 attempts within 3 days

After all retry attempts are exhausted, the webhook status is set to failed. You can view delivery attempts and their results in the Paddle dashboard under Developer Tools → Notifications, or via the API using the GET /notifications/{notification_id}/logs endpoint.

Delivery guarantees

Paddle provides at-least-once delivery, meaning:

  • Events will be delivered at least once
  • Events may occasionally be delivered more than once
  • Events may arrive out of order

Your webhook handler must be designed to handle these scenarios gracefully.

Best practices for Paddle webhooks

Implement idempotent handlers

Since Paddle may deliver the same webhook multiple times, your handlers must be idempotent, so processing the same event multiple times should have the same effect as processing it once.

Handle out-of-order events

Paddle cannot guarantee the order of webhook delivery. A subscription.updated event might arrive before the corresponding subscription.created event.

Use the occurred_at timestamp to ensure you're processing events in the correct logical order.

Store events before processing

For critical workflows, persist the raw webhook payload before attempting to process it. This provides a fallback if processing fails.

Use the fetch-before-process pattern

For critical operations, treat the webhook as a signal and fetch the current state from the Paddle API rather than relying solely on the webhook payload:

async function handleTransactionCompleted(payload) {
  const { data } = payload;

  // Fetch the current transaction state from Paddle
  const transaction = await paddle.transactions.get(data.id);

  // Use the API response, which is guaranteed to be current
  await fulfillOrder(transaction);
}

This pattern is particularly useful for:

  • High-value transactions
  • Complex subscription changes
  • Scenarios where data consistency is critical

For more detail, check out out Fetch Before Process Pattern guide.

Implement IP allowlisting

Paddle sends webhooks from specific IP addresses. For additional security, configure your firewall or application to only accept webhook requests from Paddle's IP ranges.

Important: IP addresses may change over time. Always refer to the official Paddle documentation for the current list of IP addresses. Paddle provides separate lists for sandbox and production environments.

Subscribe only to events you need

Rather than subscribing to all events, select only the event types your application requires. This reduces unnecessary traffic and processing load. For most SaaS applications, the essential events are:

  • subscription.created
  • subscription.updated
  • subscription.canceled
  • transaction.completed
  • transaction.paid

Testing Paddle webhooks

Using the webhook simulator

Webhook simulator

Paddle provides a built-in webhook simulator (which uses Hookdeck's Console under the hood) in the dashboard (Developer Tools → Notifications → Simulate) that sends test webhooks with valid signatures. You can:

  • Test individual event types
  • Run predefined scenarios (e.g., full subscription lifecycle)
  • Verify your endpoint responds correctly

Local development

Since Paddle requires a publicly accessible HTTPS endpoint, local development requires a tunnelling solution, like Hookdeck CLI.

# Install the Hookdeck CLI
npm install -g hookdeck-cli

# Start a tunnel to your local server
hookdeck listen 3000 paddle-source

# Configure the provided URL in Paddle dashboard

The Hookdeck CLI provides additional benefits like request inspection, replay capabilities, and persistent URLs, making it particularly well-suited for webhook development.

Limitations and pain points

Strict 5-second timeout

Problem: Paddle requires webhook endpoints to respond within 5 seconds, or the delivery is marked as failed and queued for retry. This strict timeout can be challenging for applications with complex processing logic or external service dependencies.

Why It Happens: Paddle's infrastructure is optimized for high throughput and needs to quickly determine delivery success to manage its retry queue efficiently. A longer timeout would tie up resources and slow down delivery to other customers.

Workarounds:

  1. Queue-based processing: Accept the webhook immediately, store it in a message queue (like Redis, RabbitMQ, SQS), and process asynchronously.
  2. Database-first approach: Write the raw event to your database, respond, then process.

How Hookdeck Can Help: Hookdeck sits between Paddle and your application, accepting webhooks immediately and providing a configurable delivery window of up to 60 seconds (or more with retries). This gives your endpoint more time to respond while ensuring Paddle sees successful delivery. Hookdeck also handles queuing automatically, so you can focus on business logic rather than infrastructure.

Serverless cold start timeouts

Problem: When running webhook handlers on serverless platforms (AWS Lambda, Vercel Functions, Google Cloud Functions), cold starts can consume a significant portion of the 5-second timeout, causing intermittent failures.

Why It Happens: Serverless platforms deallocate compute resources after a period of inactivity. When a new request arrives, the platform must provision a new instance, load your code, and initialize dependencies—a process that can take several seconds for complex applications.

Workarounds:

  1. Provisioned concurrency: Configure your serverless platform to keep warm instances.
  2. Scheduled warming: Use scheduled events to periodically invoke your function.
  3. Edge functions: Use edge-deployed functions (Cloudflare Workers, Vercel Edge) that have near-zero cold start times.

How Hookdeck Can Help: Hookdeck's infrastructure is always warm and responds to Paddle immediately, then forwards events to your endpoint with configurable retry logic. Even if your first delivery attempt fails due to a cold start, Hookdeck automatically retries, giving your function time to warm up. You can also configure rate limiting to control how quickly events are delivered, preventing your serverless function from being overwhelmed.

No guaranteed event ordering

Problem: Paddle delivers webhooks in the order they're generated, but network conditions and retry logic mean events may arrive at your endpoint out of order. A subscription.updated event might arrive before subscription.created.

Why It Happens: Webhooks are delivered asynchronously over the internet. If one delivery attempt fails and triggers a retry, subsequent events may overtake it. Paddle prioritizes delivery reliability over strict ordering.

Workarounds:

  1. Timestamp-based ordering: Always check occurred_at before applying updates:
async function handleEvent(payload) {
  const entity = await db.findByPaddleId(payload.data.id);

  if (entity && new Date(payload.occurred_at) <= entity.lastUpdated) {
    return; // Ignore stale events
  }

  await db.upsert({
    paddleId: payload.data.id,
    lastUpdated: payload.occurred_at,
    ...payload.data
  });
}
  1. Event versioning: Track event sequence numbers when available.

  2. Eventual consistency: Design your system to tolerate temporary inconsistency:

async function handleSubscriptionUpdated(payload) {
  const { data } = payload;

  // Upsert pattern handles missing 'created' event
  await db.subscriptions.upsert(
    { paddleSubscriptionId: data.id },
    {
      $set: {
        status: data.status,
        updatedAt: payload.occurred_at
      },
      $setOnInsert: {
        createdAt: data.created_at
      }
    }
  );
}

How Hookdeck Can Help: Hookdeck maintains ordered delivery within a connection by default and provides event deduplication to prevent processing the same event multiple times. You can also use Hookdeck's transformation feature to add sequence numbers or implement custom ordering logic before events reach your endpoint.

Duplicate event delivery

Problem: Due to at-least-once delivery semantics and retry behavior, you may receive the same webhook event multiple times. Without proper handling, this can cause duplicate charges, multiple fulfilment attempts, or corrupted data.

Why It Happens: If your endpoint responds slowly or network issues cause the response to be lost, Paddle may retry delivery even though you successfully processed the event. This is an intentional trade-off to ensure events are never lost.

Workarounds:

  1. Idempotency keys: Use event_id or notification_id to detect duplicates:
const processedEvents = new Set(); // Use Redis in production

async function handleWebhook(payload) {
  if (processedEvents.has(payload.event_id)) {
    return { status: 'duplicate' };
  }

  processedEvents.add(payload.event_id);
  await processEvent(payload);
}
  1. Database constraints: Use unique constraints to prevent duplicate inserts:
CREATE TABLE webhook_events (
  event_id VARCHAR(50) PRIMARY KEY,
  event_type VARCHAR(50),
  processed_at TIMESTAMP DEFAULT NOW()
);
try {
  await db.webhookEvents.insert({ eventId: payload.event_id });
  await processEvent(payload);
} catch (error) {
  if (error.code === 'DUPLICATE_KEY') {
    return; // Already processed
  }
  throw error;
}

How Hookdeck Can Help: Hookdeck provides built-in deduplication that can be configured per connection. It identifies duplicate events based on configurable criteria and ensures each unique event is delivered only once to your endpoint, even if Paddle sends multiple notifications.

Limited debugging and visibility

Problem: When webhooks fail or behave unexpectedly, debugging can be difficult. Paddle's dashboard shows delivery attempts and responses, but provides limited insight into why your handler failed or what the payload contained at the time.

Why It Happens: Paddle's logging is designed for operational monitoring, not developer debugging. Detailed payload inspection and request/response capture require additional tooling.

Workarounds:

  1. Comprehensive logging: Log every incoming webhook with full context.

  2. Structured logging services: Use services like Datadog, LogDNA, or Papertrail for searchable, structured logs.

  3. Request capture middleware: Store raw requests for replay and debugging.

How Hookdeck Can Help: Hookdeck provides comprehensive observability out of the box. Every event is logged with full payload visibility, request/response details, and delivery timing. You can search and filter events, inspect failures, and replay any event with a single click. Hookdeck also supports alerting rules so you're notified when delivery issues occur.

No native webhook replay

Problem: When your handler has a bug or your endpoint is down, you may miss important events. Paddle doesn't provide a built-in way to replay historical webhooks so once retries are exhausted, events are marked as failed.

Why It Happens: Webhook replay at scale requires significant storage and infrastructure. Paddle focuses on reliable initial delivery and leaves replay functionality to external solutions.

Workarounds:

  1. Store all events: Persist every webhook to your database for manual replay.

  2. Use the Paddle API: For subscription and transaction data, you can fetch current state from the API instead of relying on webhook history:

// Resync a subscription from the API
async function resyncSubscription(subscriptionId) {
  const subscription = await paddle.subscriptions.get(subscriptionId);
  await db.subscriptions.upsert({
    paddleSubscriptionId: subscription.id,
    ...subscription
  });
}

How Hookdeck Can Help: Hookdeck stores all events for up to 30 days (depending on your plan) and provides one-click replay for any event. You can replay individual events, bulk replay all failed events, or replay events matching specific criteria. This eliminates the need to build and maintain your own replay infrastructure.

Complex signature verification

Problem: Implementing signature verification correctly requires careful handling of the raw request body, timestamp parsing, and HMAC computation. Many frameworks automatically parse JSON bodies, which breaks verification.

Why It Happens: Signature verification requires the exact bytes that were signed. JSON parsing, whitespace changes, or character encoding differences will produce a different signature.

Workarounds:

  1. Access raw body in Express:
// Must be configured before JSON parsing
app.use('/webhooks/paddle', express.raw({ type: 'application/json' }));

app.post('/webhooks/paddle', (req, res) => {
  const rawBody = req.body; // Buffer
  const signature = req.headers['paddle-signature'];

  if (!verifySignature(rawBody, signature)) {
    return res.status(401).send();
  }

  const payload = JSON.parse(rawBody);
  // Process...
});
  1. Use official SDKs: Paddle's SDKs handle signature verification correctly.

  2. Body caching middleware:

app.use((req, res, next) => {
  let data = '';
  req.on('data', chunk => data += chunk);
  req.on('end', () => {
    req.rawBody = data;
    try {
      req.body = JSON.parse(data);
    } catch (e) {
      req.body = {};
    }
    next();
  });
});

How Hookdeck Can Help: Hookdeck can verify Paddle webhook signatures on your behalf using its source verification feature. You configure your Paddle secret key in Hookdeck, and it handles all signature validation before forwarding events to your endpoint. This eliminates signature verification complexity from your application code entirely.

Migration between Paddle Classic and Paddle Billing

Problem: Paddle Billing is a complete rewrite with a new API, new webhook events, and new payload structures. Migrating from Paddle Classic requires building an entirely new integration.

Why It Happens: Paddle Billing was designed from the ground up to address limitations in Paddle Classic. While this provides a better long-term foundation, it means the two systems are not compatible.

Workarounds:

  1. Run integrations in parallel: During migration, maintain handlers for both systems.

  2. Use import events: When migrating subscriptions, listen for subscription.imported and customer.imported events to update your records.

  3. Gradual migration: Migrate new customers to Paddle Billing while existing customers remain on Classic.

How Hookdeck Can Help: Hookdeck's transformation feature can normalize payloads between Paddle Classic and Paddle Billing formats, allowing you to maintain a single handler while migrating. You can also use routing rules to direct events from different sources to appropriate handlers based on payload content.

Conclusion

Paddle webhooks provide a powerful mechanism for keeping your application synchronized with billing events in real-time. By understanding the event types, implementing proper signature verification, and following best practices for idempotency and error handling, you can build robust integrations that handle the complexities of subscription billing.

The key challenges (strict timeouts, out-of-order delivery, and limited replay capabilities) are common to many webhook providers and can be addressed through careful architecture and appropriate tooling. Whether you implement your own queuing infrastructure or leverage an event gateway like Hookdeck, the goal is the same: reliable, observable, and maintainable webhook processing that lets you focus on your core product.

Additional resources


Author picture

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.