Documentation

Paddle Webhooks

Webhooks allow Paddle to notify your app of events such as completed payments, subscription changes, or refunds. Testing these webhooks, however, can be tricky without the right setup.

In this guide, you’ll learn webhook best practices, how to test Paddle webhooks locally, and ensure they’re securely verified. By the end, you’ll have a webhook handling system that’s ready for production.

Challenges when testing Paddle webhooks

Webhooks enable your app to react to events in Paddle’s system. Paddle sends these events to a specified URL, and your app processes them.

However, handling webhooks presents challenges:

  1. Difficult testing, eg. localhost isn’t accessible from the internet
  2. You need to validate event signatures to ensure they’re from Paddle
  3. Simulating real-world events like subscription upgrades or cancellations can be cumbersome

To overcome these challenges, you need a secure webhook handler, a way to simulate events, and a way to test events locally.

With the right tools and techniques, these challenges are manageable.

Step 1: Create a webhook handler

First things first: you need a webhook handler to process incoming events. This varies depending on your tech stack, but the general idea is the same.

Below is an example of how you can set up a webhook handler in Django:

# views.py

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import hashlib
import hmac
import time

@csrf_exempt
def paddle_webhook(request):
    secret = "your-paddle-webhook-secret"  # Replace with your Paddle webhook secret
    payload = request.body.decode('utf-8')
    signature_header = request.headers.get('Paddle-Signature')

    # Extract timestamp and signature from the Paddle-Signature header
    try:
        ts, signature = [
            part.split("=")[1]
            for part in signature_header.split(";")
        ]
        ts = int(ts)
    except (ValueError, IndexError):
        return JsonResponse({'error': 'Invalid signature header'}, status=400)

    # Optionally, check the timestamp to prevent replay attacks
    current_time = int(time.time())
    if abs(current_time - ts) > 300:  # 5-minute tolerance
        return JsonResponse({'error': 'Timestamp too old'}, status=400)

    # Verify the signature
    signed_payload = f"{ts}:{payload}"
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected_signature, signature):
        return JsonResponse({'error': 'Invalid signature'}, status=400)

    # Process the event
    data = request.POST  # Paddle sends form-encoded data
    event_type = data.get('alert_name')

    if event_type == 'subscription_created':
        subscription_id = data.get('subscription_id')
        print(f"Subscription {subscription_id} created.")
    elif event_type == 'payment_succeeded':
        order_id = data.get('order_id')
        print(f"Payment for order {order_id} succeeded.")
    else:
        print(f"Unhandled event type: {event_type}")

    return JsonResponse({'status': 'success'})

In this implementation:

  • The secret is your Paddle webhook secret, available in the Paddle dashboard.
  • The Paddle-Signature header contains the timestamp and HMAC signature.
  • Timestamps are checked to prevent replay attacks, and signatures are validated using HMAC-SHA256.

You'll also need to add a URL pattern to your Django app's urls.py:

# urls.py    

from django.urls import path
from . import views

urlpatterns = [
    path('webhooks', views.paddle_webhook),
    # ...your other URLs
]

Step 2: Simulate Paddle events locally

Paddle doesn’t provide a way to simulate events in a local development environment. To test your webhook handler, you need to simulate these events manually.

For this, you can use the usewebhook-cli to simulate and forward Paddle webhooks to your local server.

Debug webhook requests

Installation

You can install it with this one-liner:

curl -sSL https://usewebhook.com/install.sh | bash

Then, use the usewebhook command to start inspecting and forwarding webhook events:

$ usewebhook

> Dashboard: https://usewebhook.com/?id=123
> Webhook URL: https://usewebhook.com/123

You’ll get two URLs:

  1. Dashboard URL: View and manage incoming requests.
  2. Webhook URL: Set this as your endpoint in Paddle.

The dashboard shows incoming requests in real-time, including the payload, headers, and request ID.

You can even share your unique dashboard URL with your team to collaborate on debugging webhook issues.

Forwarding webhooks

You can now forward incoming requests to your local server for testing:

usewebhook --forward-to http://localhost:8000/webhooks

Replaying webhooks

Or replay a specific webhook from history:

usewebhook --request-id <request-ID> --forward-to http://localhost:8000/webhooks

This saves time when debugging issues by eliminating the need to re-trigger events manually. You can also inspect the payload and headers of incoming requests from your browser.

Step 3: Moving to production

Once your webhook endpoint is thoroughly tested locally, it’s time to deploy it to production. Keep the following considerations in mind:

  1. Validate Webhook Signatures: Always validate the webhook signature using Paddle’s library or your own implementation. This ensures that events genuinely come from Paddle and haven’t been tampered with.
  2. Use HTTPS: Paddle requires webhook URLs to use HTTPS. This secures communication between Paddle and your app, protecting sensitive data in transit.
  3. Use a Live Domain for Webhook URL: Update your webhook configuration in the Paddle dashboard to use your live domain. Avoid hardcoding the URL in your app to make it easier to switch between environments.
  4. Use Environment-Specific API Keys: Separate your test and live environments in Paddle. Use different API keys and webhook signing secrets for each environment to prevent accidental data contamination.
  5. Set Up Logging and Monitoring: Set up logging and monitoring for webhook events. This helps you track the health of your integration and quickly identify issues.
  6. Handle Retries and Duplicates: Paddle retries webhook events if your server doesn’t respond with a 2xx status code. Ensure your webhook handler is idempotent to prevent duplicate processing of events.

Wrapping up

Testing Paddle webhooks doesn’t need to be complicated. By implementing a secure handler, validating signatures, and using tools like usewebhook-cli, you can efficiently test and debug your webhooks in a local environment.

With these practices in place, your webhook handling system will be robust and production-ready.