Subscription Auto Renewal
Description
This document outlines the automated process for subscription auto-renewal. Unlike new registrations, this flow is initiated entirely by Stripe when a subscription's billing period is due for renewal. The system relies on a series of webhooks from Stripe to manage the entire lifecycle of the renewal, from payment processing to handling failures.
The primary webhooks involved are invoice.paid for successful renewals and invoice.payment_failed for failures. The system listens for these events to update the user's subscription status, extend their access period, and manage retries in case of payment issues, ensuring a seamless continuation of service for the user without any manual intervention.
Prerequisites:
- An active user subscription is approaching its
deadline_atdate. - The subscription has
auto_renewenabled. - Stripe is configured to automatically attempt renewal payments.
Process Flow Diagram
---
config:
theme: base
layout: dagre
flowchart:
curve: linear
htmlLabels: true
themeVariables:
edgeLabelBackground: "transparent"
---
flowchart TD
%% == NODES DEFINITION ==
StripeScheduler((Stripe Scheduler))
%% Layer components
subgraph ApiControllerLayer["API Controller Layer"]
WebhookController[WebhookController]
end
subgraph ApiServiceLayer["API Service Layer"]
SubscriptionService(SubscriptionService)
InvoiceHandler(SubscriptionInvoiceHandler)
end
subgraph DatabaseLayer["Database Layer"]
SubscriptionDB[(subscriptions)]
HistoryDB[(subscription_histories)]
WebhookEventsDB[(stripe_webhook_events)]
end
subgraph ExternalServices["External Services"]
StripeAPI((Stripe API))
end
%% == STEPS DEFINITION ==
StepW1["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W1</span><p style='margin-top: 8px'>Attempt Payment & Send Webhook<br/>(invoice.paid / invoice.payment_failed)</p></div>"]
StepW2["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W2</span><p style='margin-top: 8px'>Verify & Log Webhook</p></div>"]
StepW3["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W3</span><p style='margin-top: 8px'>Process Event with Invoice Handler</p></div>"]
StepW4["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W4</span><p style='margin-top: 8px'>Update Subscription & Create History</p></div>"]
%% == CONNECTIONS ==
StripeScheduler --> StripeAPI
StripeAPI --- StepW1 --> WebhookController
WebhookController --- StepW2 --> WebhookEventsDB
WebhookController --- StepW3 --> SubscriptionService
SubscriptionService --> InvoiceHandler
InvoiceHandler --- StepW4
StepW4 --> SubscriptionDB
StepW4 --> HistoryDB
%% == STYLING ==
style ApiControllerLayer fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
style ApiServiceLayer fill:#f0f8e6,stroke:#339933,stroke-width:2px
style DatabaseLayer fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
style ExternalServices fill:#fcd9d9,stroke:#cc3333,stroke-width:2px
style StepW1 fill:transparent,stroke:transparent,stroke-width:1px
style StepW2 fill:transparent,stroke:transparent,stroke-width:1px
style StepW3 fill:transparent,stroke:transparent,stroke-width:1px
style StepW4 fill:transparent,stroke:transparent,stroke-width:1px
Use Cases
Case 1: Successful Auto Renewal
Description
When a subscription's recurring payment is processed successfully, Stripe sends an invoice.paid webhook. The system listens for this event to create a new history record for the renewed period and extends the subscription's deadline_at.
Sequence Diagram
sequenceDiagram
participant StripeAPI as Stripe API
participant Webhook as WebhookController
participant SubSvc as SubscriptionService
participant InvoiceHandler as SubscriptionInvoiceHandler
participant DB as Database
Note over StripeAPI,DB: Successful Auto Renewal Flow
StripeAPI->>Webhook: POST /webhook (invoice.paid, billing_reason: subscription_cycle)
rect rgb(255, 255, 200)
Note right of Webhook: Verify & Log Event
Webhook->>DB: Check for existing event (idempotency)
Webhook->>DB: Create stripe_webhook_events record
end
Webhook->>SubSvc: handleInvoicePaid(invoice_data)
SubSvc->>InvoiceHandler: handleInvoicePaid(invoice_data)
rect rgb(200, 255, 255)
Note right of InvoiceHandler: Process Renewal
InvoiceHandler->>DB: Find subscription by provider_id
alt Subscription is active
InvoiceHandler->>DB: Begin Transaction
InvoiceHandler->>DB: Update subscriptions (deadline_at = new_period_end)
InvoiceHandler->>DB: Create new subscription_histories record (type: renewal, payment_status: paid, amount, etc.)
InvoiceHandler->>DB: Commit Transaction
InvoiceHandler-->>SubSvc: Success
else Subscription not active
Note over InvoiceHandler: Log error, no action taken.
InvoiceHandler-->>SubSvc: Failure
end
end
SubSvc-->>Webhook: Success
Webhook->>DB: Update stripe_webhook_events (status: completed)
Webhook-->>StripeAPI: 200 OK
Steps
Step 1: Receive invoice.paid Webhook
- Stripe automatically attempts payment for the new billing cycle. Upon success, it sends an
invoice.paidwebhook withbilling_reason: 'subscription_cycle'.
Step 2: Verify and Process Event
- The
WebhookControllerverifies the event and delegates it to theSubscriptionService, which then calls theSubscriptionInvoiceHandler.
Step 3: Update Subscription and Create History
- The
SubscriptionInvoiceHandleridentifies the correspondingsubscriptionin the local database. - Within a database transaction, it performs two key actions:
- It updates the main
subscription.deadline_atto the new period's end date provided in the webhook data. - It creates a new record in
subscription_historiesto represent the renewal period. This new record has itssubscription_histories.typeset torenewalandsubscription_histories.payment_statusset topaid.
- It updates the main
Case 2: Failed Auto Renewal
Description
If a recurring payment fails, Stripe sends an invoice.payment_failed webhook. The system logs this failure. After several failed attempts managed by Stripe, Stripe will automatically change the subscription's status (e.g., to past_due or canceled) and send a corresponding customer.subscription.updated or customer.subscription.deleted webhook, at which point the system updates the local subscription status.
Sequence Diagram
sequenceDiagram
participant StripeAPI as Stripe API
participant Webhook as WebhookController
participant SubSvc as SubscriptionService
participant InvoiceHandler as SubscriptionInvoiceHandler
participant DB as Database
Note over StripeAPI,DB: Failed Auto Renewal Flow
StripeAPI->>Webhook: POST /webhook (invoice.payment_failed)
rect rgb(255, 220, 180)
Note right of Webhook: Log Initial Failure
Webhook->>SubSvc: handleInvoicePaymentFailed(invoice_data)
SubSvc->>InvoiceHandler: handleInvoicePaymentFailed(invoice_data)
InvoiceHandler->>DB: Find subscription by provider_id
InvoiceHandler->>DB: Create new subscription_histories record (type: renewal, payment_status: failed, payment_attempt: 1)
end
Note over StripeAPI,StripeAPI: Stripe Smart Retries attempt payment again...
StripeAPI->>Webhook: POST /webhook (invoice.payment_failed)
rect rgb(255, 220, 180)
Note right of Webhook: Log Retry Failure
Webhook->>SubSvc: handleInvoicePaymentFailed(invoice_data)
SubSvc->>InvoiceHandler: handleInvoicePaymentFailed(invoice_data)
InvoiceHandler->>DB: Find existing failed history record
InvoiceHandler->>DB: Update subscription_histories (payment_attempt + 1)
end
Note over StripeAPI,StripeAPI: After all retries fail...
StripeAPI->>Webhook: POST /webhook (customer.subscription.updated, status: canceled)
rect rgb(255, 200, 200)
Note right of Webhook: Cancel Subscription
Webhook->>SubSvc: handleSubscriptionUpdated(subscription_data)
SubSvc->>DB: Update subscriptions (status: canceled, canceled_at)
end
Steps
Step 1: Receive invoice.payment_failed Webhook
- If a recurring payment fails, Stripe sends this webhook.
Step 2: Log Payment Failure
- The
SubscriptionInvoiceHandlerfinds the relevant subscription. - If this is the first failure for the billing period, it creates a new
subscription_historiesrecord withsubscription_histories.typeset torenewal,subscription_histories.payment_statusset tofailed, andpayment_attemptas 1. - On subsequent
invoice.payment_failedevents for the same cycle, the handler finds the existingfailedhistory record and increments itspayment_attemptcounter.
Step 3: Stripe Manages Retries and Final Status
- Stripe's Smart Retries feature automatically attempts to charge the customer again.
- After all retry attempts are exhausted, Stripe updates the subscription's status (e.g., to
past_due, orcanceled) and sends acustomer.subscription.updatedorcustomer.subscription.deletedwebhook.
Step 4: Update Final Subscription Status
- The application listens for these final status-change webhooks. The
SubscriptionLifeCycleHandlerprocesses them to update the mainsubscription.statustocanceled(orpast_due) and sets thecanceled_attimestamp, officially ending the user's access for that period.
Related Database Structure
erDiagram
users {
bigint id PK
string name "User's full name"
string email "User's email address (unique)"
string payment_provider_customer_id "Customer ID from Stripe (nullable)"
timestamp created_at
timestamp updated_at
}
subscriptions {
bigint id PK
string slug "Unique identifier for linking with Stripe metadata"
bigint user_id FK
bigint group_id FK
bigint package_id FK
bigint package_plan_id FK
string status "unpaid, active, past_due, canceled"
string payment_provider_subscription_id "Stripe subscription ID"
boolean auto_renew "Flag for auto-renewal"
timestamp deadline_at "Current billing period end"
timestamp canceled_at "Timestamp of cancellation"
timestamp created_at
timestamp updated_at
}
subscription_histories {
bigint id PK
bigint subscription_id FK
string status "active, inactive"
string payment_status "pending, paid, failed"
string type "new_contract, renewal, change"
string invoice_id "Stripe invoice ID"
string payment_intent_id "Stripe payment intent ID"
timestamp started_at "Period start timestamp"
timestamp expires_at "Period end timestamp"
timestamp paid_at "Payment success timestamp"
integer payment_attempt "Number of failed payment attempts for this cycle"
timestamp created_at
timestamp updated_at
}
stripe_webhook_events {
bigint id PK
string stripe_event_id "Stripe event ID to prevent duplicates"
string event_type
enum status "pending, processing, completed, failed"
text error "Error message if processing fails"
timestamp created_at
timestamp updated_at
}
users ||--o{ subscriptions : has
subscriptions ||--o{ subscription_histories : has
Related API Endpoints
| Method | Endpoint | Controller | Description |
|---|---|---|---|
| POST | /api/v1/admin/stripe/webhook | WebhookController@handleWebhook | Listens for and processes all incoming events from Stripe related to subscriptions. |
Error Handling
-
Log:
- All webhook processing failures are logged to the application log.
- Webhook processing errors are stored in the
errorcolumn of thestripe_webhook_eventstable.
-
Error Detail:
| Status Code | Error Message | Description |
|---|---|---|
| 400 | Invalid webhook signature. | The request did not originate from Stripe. |
| 404 | Subscription not found for webhook. | The subscription ID from the webhook does not match any record in the database. |
| 500 | Database error: {message} | A database operation failed during webhook processing. |
Additional Notes
- Automation: The entire renewal flow is automated and requires no user interaction from the point of initial subscription.
- Single Source of Truth: Stripe is the source of truth for the subscription's billing status. The local database is updated to reflect the state changes communicated via webhooks.
- Idempotency: The system uses the
stripe_webhook_eventstable to track processed event IDs, ensuring that each webhook is only processed once, even if Stripe sends it multiple times. - Retry Logic: Payment retry logic is handled by Stripe's "Smart Retries". The application's role is to log these attempts and react to the final subscription status change (e.g., cancellation).