Skip to Content
WebhooksEvents and scenarios

Webhook Events and Scenarios

Learn about different webhook events and their usage scenarios to properly handle subscription and payment workflows.

Event Types

1. payment.completed

Description: Completion of a one-time payment (lifetime)

When sent: When a one-time product or service payment is successful

Use cases:

  • Lifetime access purchases
  • One-time product purchases
  • Single service payments

2. subscription.created

Description: Creation of a new subscription

When sent: When a user creates a subscription for the first time

Use cases:

  • New subscription activation
  • Trial period start
  • Initial subscription setup

3. subscription.updated

Description: Update of an existing subscription

When sent: When subscription status, plan, or other parameters change

Use cases:

  • Plan upgrades/downgrades
  • Subscription renewals
  • Status changes
  • Reactivation

Paddle Note: Paddle may sometimes send multiple consecutive subscription.updated webhooks for the same subscription, even if business data (status, product, price) hasn’t changed. This is normal platform behavior and doesn’t require special handling.

4. subscription.cancelled

Description: Subscription cancellation

When sent: When a subscription is cancelled by user or system

Use cases:

  • User cancellation
  • Failed payment cancellation
  • Administrative cancellation

5. refund.created

Description: Creation of a refund

When sent: When a payment refund is processed

Use cases:

  • Customer refund requests
  • Administrative refunds
  • Dispute resolutions

Subscription Object Fields

The data.subscription object in subscription.* events contains the following fields:

FieldTypeDescription
idstringSubscription identifier
statusstringOne of active, trialing, past_due, canceled, incomplete
current_period_startstring (ISO)Start of the current billing period
current_period_endstring (ISO)Next billing date. Not rewound on immediate cancellation — use ended_at for the actual end of access
trial_period_startstring (ISO)Start of trial, if any
trial_period_endstring (ISO)End of trial, if any
cancel_at_period_endbooleantrue when cancellation is scheduled for the end of the current period
cancel_atstring (ISO)When the subscription will be cancelled (set together with cancel_at_period_end: true)
canceled_atstring (ISO)When the user requested cancellation
ended_atstring (ISO)When the subscription actually ended. Set on immediate cancellation and after cancel_at is reached

To determine whether a subscription is currently active, check status and ended_at. If ended_at is set and not in the future, access should be revoked even if current_period_end is later. If cancel_at_period_end is true, the subscription is active but will not renew.

Event Scenarios

Scenario 1: Lifetime Access Purchase

StepEventDescription
1User makes paymentUser completes payment for lifetime access
2payment.completedPayment successfully processed

Payment Successful

{ "type": "payment.completed", "data": { "payment": { "id": "pi_lifetime123", "status": "succeeded" }, "price": { "interval": "lifetime" } } }

Event: payment.completed - payment successfully completed

In case of refund:

Refund Processed

{ "type": "refund.created", "data": { "refund": { "id": "re_refund123", "reason": "requested_by_customer", "amount": 9999 } } }

Event: refund.created - refund processed

Scenario 2: Subscription Without Trial

StepEventDescription
1User subscribesUser completes payment for subscription
2subscription.createdSubscription created and activated

Subscription Created

{ "type": "subscription.created", "data": { "subscription": { "id": "sub_active123", "status": "active", "current_period_start": "2024-01-15T12:30:45.000Z", "current_period_end": "2024-02-15T12:30:45.000Z" } } }

Event: subscription.created - subscription created and activated

On subscription renewal:

Subscription Renewed

{ "type": "subscription.updated", "data": { "subscription": { "id": "sub_active123", "status": "active", "current_period_start": "2024-02-15T12:30:45.000Z", "current_period_end": "2024-03-15T12:30:45.000Z" } } }

Event: subscription.updated - subscription automatically renewed

Updated subscription.current_period_start and subscription.current_period_end will be sent for the current subscription

On subscription cancellation:

Variant A: Cancellation at period end

StepEventDescription
1User cancelsUser requests cancellation
2subscription.updatedSubscription scheduled for cancellation, remains active
3End of periodPeriod reaches cancel_at
4subscription.cancelledSubscription actually cancelled

First, when the user requests cancellation:

{ "type": "subscription.updated", "data": { "subscription": { "id": "sub_active123", "status": "active", "cancel_at_period_end": true, "cancel_at": "2025-02-15T12:30:45.000Z", "canceled_at": "2024-02-15T12:30:45.000Z", "current_period_end": "2025-02-15T12:30:45.000Z" } } }

Then, when the period ends:

{ "type": "subscription.cancelled", "data": { "subscription": { "id": "sub_active123", "status": "canceled", "cancel_at_period_end": true, "canceled_at": "2024-02-15T12:30:45.000Z", "ended_at": "2025-02-15T12:30:45.000Z" } } }

Subscription remains in active status with cancel_at_period_end: true until cancel_at is reached, then status changes to canceled and ended_at is set.

Scenario 3: Subscription With Trial Period

StepEventDescription
1User starts trialUser begins trial period
2subscription.createdSubscription created with status: trialing
3Trial period endsTrial period expires
4subscription.updatedSubscription updated with status: active

Trial Started

{ "type": "subscription.created", "data": { "subscription": { "id": "sub_trial123", "status": "trialing", "trial_period_start": "2024-01-15T12:30:45.000Z", "trial_period_end": "2024-01-22T12:30:45.000Z" } } }

Event: subscription.created - subscription created with status “trialing”

Fields subscription.trial_period_start and subscription.trial_period_end will also be provided

Trial Ended

{ "type": "subscription.updated", "data": { "subscription": { "id": "sub_trial123", "status": "active", "current_period_start": "2024-01-22T12:30:45.000Z", "current_period_end": "2024-02-22T12:30:45.000Z" } } }

Event: subscription.updated - trial period ended, subscription became active

Updated subscription.current_period_start and subscription.current_period_end will be sent for the current subscription

If user cancels during trial:

Trial Cancelled

{ "type": "subscription.cancelled", "data": { "subscription": { "id": "sub_trial123", "status": "canceled", "canceled_at": "2024-01-18T12:30:45.000Z", "ended_at": "2024-01-18T12:30:45.000Z" } } }

Event: subscription.cancelled - subscription cancelled during trial period

Scenario 4: Plan Change (Upgrade/Downgrade)

StepEventDescription
1User changes planUser upgrades or downgrades subscription plan
2subscription.updatedSubscription updated with new plan

Plan Changed

{ "type": "subscription.updated", "data": { "subscription": { "id": "sub_active123", "status": "active" }, "price": { "id": "price_new_plan", "unit_amount": 4999, "interval": "month" } } }

Event: subscription.updated - subscription plan changed

Possible changes leading to subscription.updated:

  • Plan change (upgrade/downgrade)
  • Subscription renewal
  • Reactivation of cancelled subscription

Scenario 5: Subscription Refund

StepEventDescription
1Refund requestedActive subscription refund requested
2refund.createdRefund processed
3subscription.cancelledSubscription cancelled due to refund

Refund Created

{ "type": "refund.created", "data": { "refund": { "id": "re_sub_refund456", "reason": "requested_by_customer", "amount": 2999 } } }

Event: refund.created - refund processed

Subscription Cancelled

{ "type": "subscription.cancelled", "data": { "subscription": { "id": "sub_active123", "status": "canceled", "canceled_at": "2024-01-20T12:30:45.000Z", "ended_at": "2024-01-20T12:30:45.000Z" } } }

Event: subscription.cancelled - subscription cancelled due to refund

Event Handling Examples

Complete Event Handler

const handleWebhookEvent = async (event) => { const { type, data } = event; switch (type) { case 'payment.completed': await handlePaymentCompleted(data); break; case 'subscription.created': await handleSubscriptionCreated(data); break; case 'subscription.updated': await handleSubscriptionUpdated(data); break; case 'subscription.cancelled': await handleSubscriptionCancelled(data); break; case 'refund.created': await handleRefundCreated(data); break; default: console.log(`Unhandled event type: ${type}`); } }; // Handler implementations const handlePaymentCompleted = async (data) => { const { payment, price, user, customer } = data; if (price.interval === 'lifetime') { // Grant lifetime access await grantLifetimeAccess(user.id); console.log(`Lifetime access granted to user ${user.id}`); } else { // Handle one-time payment await processOneTimePayment(payment.id, user.id); } // Send confirmation email await sendPaymentConfirmation(customer.email, price.unit_amount); }; const handleSubscriptionCreated = async (data) => { const { subscription, user, customer } = data; if (subscription.status === 'trialing') { // Start trial period await startTrialPeriod(user.id, subscription.trial_period_end); console.log(`Trial started for user ${user.id}`); } else if (subscription.status === 'active') { // Activate subscription immediately await activateSubscription(user.id, subscription.id); console.log(`Subscription activated for user ${user.id}`); } // Send welcome email await sendWelcomeEmail(customer.email, subscription.status); };

Best Practices

1. Handle Event Order

Event Order: Events may not always arrive in chronological order. Always check timestamps and handle out-of-order events.

const handleEventWithOrder = async (event) => { const lastProcessedTime = await getLastProcessedTime(event.data.user.id); if (new Date(event.created_at) < lastProcessedTime) { console.log('Out of order event, handling carefully...'); // Handle out-of-order event logic } await processEvent(event); await updateLastProcessedTime(event.data.user.id, event.created_at); };

2. Idempotency

const processedEvents = new Set(); const handleIdempotentEvent = async (event) => { if (processedEvents.has(event.id)) { console.log(`Event ${event.id} already processed`); return; } await processEvent(event); processedEvents.add(event.id); };

Next Steps