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
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
Event Scenarios
Scenario 1: Lifetime Access Purchase
Step | Event | Description |
---|---|---|
1 | User makes payment | User completes payment for lifetime access |
2 | payment.completed | Payment 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
Step | Event | Description |
---|---|---|
1 | User subscribes | User completes payment for subscription |
2 | subscription.created | Subscription 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:
End of Period
Variant A: Cancellation at period end
Step | Event | Description |
---|---|---|
1 | User cancels | User requests cancellation |
2 | End of period | Subscription remains active until period end |
3 | subscription.cancelled | Subscription cancelled at period end |
{
"type": "subscription.cancelled",
"data": {
"subscription": {
"id": "sub_active123",
"status": "cancelled",
"cancel_at_period_end": true,
"cancelled_at": "2024-02-15T12:30:45.000Z"
}
}
}
Event: subscription.cancelled - subscription cancelled at period end
Subscription remains in active
status until the end of the period, then the subscription status changes to cancelled
.
Scenario 3: Subscription With Trial Period
Step | Event | Description |
---|---|---|
1 | User starts trial | User begins trial period |
2 | subscription.created | Subscription created with status: trialing |
3 | Trial period ends | Trial period expires |
4 | subscription.updated | Subscription 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": "cancelled",
"cancelled_at": "2024-01-18T12:30:45.000Z"
}
}
}
Event: subscription.cancelled - subscription cancelled during trial period
Scenario 4: Plan Change (Upgrade/Downgrade)
Step | Event | Description |
---|---|---|
1 | User changes plan | User upgrades or downgrades subscription plan |
2 | subscription.updated | Subscription 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
Step | Event | Description |
---|---|---|
1 | Refund requested | Active subscription refund requested |
2 | refund.created | Refund processed |
3 | subscription.cancelled | Subscription 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": "cancelled",
"cancelled_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);
};