bscription.trialing, subscription.activated, subscription.updated, subscription.canceled, subscription.past_due, adjustment.created, adjustment.updated` |
| Header | Required | Description |
|---|
Content-Type | Yes | Must be application/json |
paddle-signature | Conditional | HMAC-SHA256 signature. Required if PADDLE_WEBHOOK_SECRET is configured in environment variables. |
Request Body
The request body follows Paddle's standard webhook payload structure. The exact shape varies by event_type, but all payloads share a common root structure:
{
"event_type": "subscription.created",
"data": {
"id": "sub_01h5k8x9y2z3a4b5c6d7e8f9",
"customer_id": "ctm_01h5k8x9y2z3a4b5c6d7e8f9",
"status": "active",
"items": [
{
"price": {
"id": "pri_01h5k8x9y2z3a4b5c6d7e8f9",
"product_id": "pro_01h5k8x9y2z3a4b5c6d7e8f9"
}
}
],
"custom_data": {
"supabase_user_id": "550e8400-e29b-41d4-a716-446655440000",
"plan_id": "PRO",
"billing_cycle": "yearly",
"customer_email": "user@example.com"
},
"started_at": "2024-01-15T10:00:00Z",
"next_billed_at": "2025-01-15T10:00:00Z"
}
}
Key Payload Fields:
event_type: Determines which internal handler processes the request.
data.id: Unique Paddle subscription or transaction identifier.
data.customer_id: Paddle customer identifier.
data.items[].price.id / data.items[].price.product_id: Used to infer plan type and billing cycle when custom_data is incomplete.
data.custom_data: Checkout metadata. Highly recommended to include supabase_user_id, plan_id, and billing_cycle during Paddle checkout initialization to bypass fallback lookups.
Success Response (200 OK)
{
"received": true
}
The endpoint returns immediately after processing. All database mutations and plan syncs occur synchronously within the request lifecycle.
Error Responses
| Status Code | Response Body | Trigger Condition |
|---|
401 Unauthorized | { "error": "Invalid signature" } | PADDLE_WEBHOOK_SECRET is set, but paddle-signature header is missing or fails verification. |
500 Internal Server Error | { "error": "Webhook handler failed" } | Unhandled exception during payload parsing, database operation, or user resolution. |
405 Method Not Allowed | { "message": "Method not allowed" } | PUT or DELETE requests. |
Usage Example
Simulate a Paddle webhook delivery using curl:
curl -X POST https://your-domain.com/api/webhooks/paddle \
-H "Content-Type: application/json" \
-H "paddle-signature: t=1705312800,v1=a1b2c3d4e5f6..." \
-d '{
"event_type": "subscription.created",
"data": {
"id": "sub_01h5k8x9y2z3a4b5c6d7e8f9",
"customer_id": "ctm_01h5k8x9y2z3a4b5c6d7e8f9",
"status": "trialing",
"items": [
{
"price": {
"id": "pri_01h5k8x9y2z3a4b5c6d7e8f9",
"product_id": "pro_01h5k8x9y2z3a4b5c6d7e8f9"
}
}
],
"custom_data": {
"supabase_user_id": "550e8400-e29b-41d4-a716-446655440000",
"plan_id": "BUILDER",
"billing_cycle": "monthly"
},
"started_at": "2024-01-15T10:00:00Z",
"next_billed_at": "2024-02-15T10:00:00Z"
}
}'
Common Pitfalls
-
Signature Verification Misconfiguration
The endpoint checks for PADDLE_WEBHOOK_SECRET in environment variables. If the secret is set but the actual HMAC-SHA256 verification logic is not implemented (the source contains a placeholder that returns true when the secret exists), requests may pass validation without cryptographic proof. Ensure you implement proper signature verification using Paddle's official SDK or a compatible HMAC library before enabling the secret in production.
-
Missing custom_data During Checkout
The handler prioritizes custom_data.supabase_user_id for user mapping. If this field is omitted during Paddle checkout initialization, the endpoint falls back to email or customer ID lookups, which are slower and may fail if the email format changes or the customer ID does not match internal records. Always pass supabase_user_id, plan_id, and billing_cycle via Paddle's customData parameter when generating checkout links.
-
Idempotency & Duplicate Inserts
Paddle retries webhook deliveries on failure, which can result in duplicate payloads. The current implementation uses insert operations on the paddle_subscriptions table. Without a unique constraint on paddle_subscription_id, duplicate events will cause database errors. Add a unique index to paddle_subscriptions(paddle_subscription_id) or modify the handler to use upsert logic for production resilience.
/api/user/subscription β Referenced in the source code for displaying subscription status and refund states to end-users. Syncs with records created by this webhook handler.
/api/auth/[...nextauth] β Handles Supabase authentication callbacks. Works in tandem with this webhook to ensure supabase_user_id is available for checkout customData injection.
- Paddle Customer API (
@/lib/paddle-customer-api) β Internal utility used by this endpoint to fetch customer emails when payloads omit them. Useful for debugging missing email fallbacks.