Skip to main content
RapidDev - Software Development Agency
API AutomationsStripeAPI Key

How to Automate Stripe Subscription Management using the API

Automate the full Stripe subscription lifecycle using POST /v1/subscriptions to create, PATCH /v1/subscriptions/{id} to upgrade or downgrade, and DELETE /v1/subscriptions/{id} to cancel. React to lifecycle events via webhooks: invoice.paid, invoice.payment_failed, and customer.subscription.deleted. Key gotcha: plan changes trigger proration charges by default. Rate limit: 100 req/sec in live mode; billing cycles can spike webhook volume.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read30-60 minutesStripeMay 2026RapidDev Engineering Team
TL;DR

Automate the full Stripe subscription lifecycle using POST /v1/subscriptions to create, PATCH /v1/subscriptions/{id} to upgrade or downgrade, and DELETE /v1/subscriptions/{id} to cancel. React to lifecycle events via webhooks: invoice.paid, invoice.payment_failed, and customer.subscription.deleted. Key gotcha: plan changes trigger proration charges by default. Rate limit: 100 req/sec in live mode; billing cycles can spike webhook volume.

API Quick Reference

Auth

API Key (Bearer token)

Rate limit

100 requests/second (live mode)

Format

JSON

SDK

Available

Understanding the Stripe API

Stripe Billing is the most feature-complete subscription engine available via API. It handles recurring invoicing, automatic payment retry (dunning), proration for mid-cycle plan changes, trial periods, usage-based billing, and subscription pausing — all configurable via REST API calls.

The subscription lifecycle flows through several Stripe objects: Customer → PaymentMethod → Subscription → Invoice → Charge. When a subscription renews, Stripe automatically creates an Invoice, attempts to charge the saved PaymentMethod, and fires webhook events for each state change. Your application reacts to those webhooks to update user entitlements, send confirmation emails, and trigger dunning sequences for failed payments.

The critical integration pattern for SaaS: always use webhooks rather than polling to update subscription status. Never rely on the API response from the create/update call alone — the payment may still fail asynchronously. Official documentation: https://stripe.com/docs/api/subscriptions

Base URLhttps://api.stripe.com

Setting Up Stripe API Authentication

Stripe Billing uses two keys for different purposes: the secret key (sk_*) for all server-side subscription management, and the publishable key (pk_*) for client-side Checkout Sessions and Elements. The Stripe SDK accepts the secret key directly at initialization. Test mode subscriptions use sk_test_* and generate no real charges.

  1. 1Log in to your Stripe Dashboard at dashboard.stripe.com
  2. 2Click 'Developers' then 'API keys' in the top navigation
  3. 3Copy your secret key (sk_live_* for production) and publishable key (pk_live_* for frontend)
  4. 4Set environment variables: STRIPE_SECRET_KEY=sk_live_... and STRIPE_PUBLISHABLE_KEY=pk_live_...
  5. 5Set up a webhook endpoint at Developers > Webhooks > Add endpoint, subscribing to: invoice.paid, invoice.payment_failed, customer.subscription.deleted, customer.subscription.updated
  6. 6Copy the webhook signing secret (whsec_*) and set STRIPE_WEBHOOK_SECRET environment variable
  7. 7In your Stripe Dashboard, create Products and Prices (the price ID like price_* is what you pass to the subscriptions API)
auth.py
1import os
2import stripe
3
4stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')
5
6# Verify connectivity and list available prices
7prices = stripe.Price.list(active=True, limit=5)
8for price in prices.data:
9 print(f'{price.id}: {price.currency.upper()} {price.unit_amount/100:.2f}/{price.recurring.interval}')

Security notes

  • Store sk_live_* in server environment variables — never in frontend code, mobile apps, or git repositories
  • The publishable key pk_live_* is safe for frontend use — it cannot make server-side charges alone
  • Store the webhook signing secret (whsec_*) separately from the API key
  • Use test mode (sk_test_* + pk_test_*) for development — test subscriptions never charge real cards
  • Verify webhook signatures on every incoming event before processing to prevent replay attacks
  • Rotate API keys at Developers > API keys if you suspect exposure

Key endpoints

POST/v1/subscriptions

Creates a new subscription for an existing Stripe customer. Requires a customer ID and a price ID. The subscription immediately attempts to collect payment unless you set payment_behavior or use a trial period.

ParameterTypeRequiredDescription
customerstringrequiredThe Stripe customer ID (cus_*) for the subscriber
itemsarrayrequiredArray of subscription items, each with a price ID. The price defines the amount and billing interval.
trial_endnumberoptionalUnix timestamp for when the trial ends and billing begins. Use 'now' to end an active trial.
payment_behaviorstringoptionalallow_incomplete: subscription active even if payment fails. default_incomplete: subscription incomplete until payment succeeds.
proration_behaviorstringoptionalFor plan changes: always_invoice (charge proration immediately), create_prorations (prorate on next invoice), or none.

Request

json
1{"customer": "cus_PqKLt2eZvKYlo2C", "items": [{"price": "price_1OqKLt2eZvKYlo2Cghi"}], "payment_settings": {"save_default_payment_method": "on_subscription"}, "expand": ["latest_invoice.payment_intent"]}

Response

json
1{"id": "sub_1OqKMj2eZvKYlo2C", "object": "subscription", "customer": "cus_PqKLt2eZvKYlo2C", "status": "active", "current_period_start": 1706745820, "current_period_end": 1709424220, "items": {"data": [{"id": "si_1OqKMj", "price": {"id": "price_1OqKLt", "unit_amount": 2900, "currency": "usd", "recurring": {"interval": "month"}}}]}, "latest_invoice": {"payment_intent": {"status": "succeeded"}}}
POST/v1/subscriptions/{id}

Updates an existing subscription — change the plan, add items, modify trial, pause, or update payment settings. Used for upgrades, downgrades, and mid-cycle changes.

ParameterTypeRequiredDescription
itemsarrayoptionalNew subscription items. For plan changes, include the subscription item ID (si_*) you're replacing.
proration_behaviorstringoptionalControls how proration is charged on plan changes. always_invoice charges immediately; none skips proration.
cancel_at_period_endbooleanoptionalIf true, subscription cancels at the end of the current billing period instead of immediately.

Request

json
1{"items": [{"id": "si_1OqKMj", "price": "price_1NewPlanId"}], "proration_behavior": "always_invoice"}

Response

json
1{"id": "sub_1OqKMj2eZvKYlo2C", "status": "active", "items": {"data": [{"id": "si_1OqKMj", "price": {"id": "price_1NewPlanId", "unit_amount": 9900}}]}}
DELETE/v1/subscriptions/{id}

Cancels a subscription. Immediate cancellation stops billing now; set cancel_at_period_end=true on the update endpoint instead for end-of-period cancellation.

ParameterTypeRequiredDescription
cancellation_details.feedbackstringoptionalReason for cancellation: customer_service, low_quality, missing_features, other, switched_service, too_complex, too_expensive, unused

Request

json
1{"cancellation_details": {"comment": "Canceled via self-service portal", "feedback": "too_expensive"}}

Response

json
1{"id": "sub_1OqKMj2eZvKYlo2C", "status": "canceled", "canceled_at": 1706745900, "current_period_end": 1709424220}
GET/v1/subscriptions

Lists subscriptions with optional filters. Use status parameter to filter active, trialing, past_due, canceled, or unpaid subscriptions. Use customer to get all subscriptions for a specific customer.

ParameterTypeRequiredDescription
customerstringoptionalFilter by customer ID to get all their subscriptions
statusstringoptionalFilter by status: active, trialing, past_due, canceled, incomplete, incomplete_expired, unpaid
pricestringoptionalFilter subscriptions on a specific price ID

Response

json
1{"object": "list", "data": [{"id": "sub_1OqKMj", "status": "active", "customer": "cus_PqKLt2", "current_period_end": 1709424220}], "has_more": false}

Step-by-step automation

1

Create a Customer and Attach a Payment Method

Why: Subscriptions require an existing Stripe Customer with a saved payment method — creating the customer first keeps your billing data clean and linked to your internal user ID.

Create a Stripe Customer with your internal user ID in metadata, then attach a PaymentMethod and set it as default. In practice, you collect the payment method via Stripe Elements or Checkout on the frontend — the PaymentMethod ID is returned to your server after the customer enters their card. Never handle raw card numbers server-side.

request.sh
1# 1. Create customer
2curl https://api.stripe.com/v1/customers \
3 -u "$STRIPE_SECRET_KEY:" \
4 -d email="user@example.com" \
5 -d "metadata[user_id]"="usr_123abc"
6
7# 2. Attach payment method (pm_* from Stripe.js/Elements on frontend)
8curl https://api.stripe.com/v1/payment_methods/pm_1OqKLt2eZvKYlo2Cabc/attach \
9 -u "$STRIPE_SECRET_KEY:" \
10 -d customer="cus_PqKLt2eZvKYlo2C"
11
12# 3. Set as default
13curl https://api.stripe.com/v1/customers/cus_PqKLt2eZvKYlo2C \
14 -u "$STRIPE_SECRET_KEY:" \
15 -d "invoice_settings[default_payment_method]"="pm_1OqKLt2eZvKYlo2Cabc"

Pro tip: Store the Stripe customer ID on your user record immediately after creation. A user should have exactly one Stripe customer — check for existing customer IDs before creating new ones to avoid duplicates.

Expected result: A Stripe Customer ID (cus_*) with a default payment method attached, ready for subscription creation.

2

Create the Subscription

Why: Creating the subscription triggers the first invoice and payment attempt — the expand parameter on latest_invoice.payment_intent lets you confirm payment succeeded in the same call.

Call POST /v1/subscriptions with the customer ID and price ID from your Stripe Dashboard. Expand latest_invoice.payment_intent to see payment status in the response. If status is 'active', billing succeeded. If status is 'incomplete', the payment requires additional action (3D Secure). Use payment_behavior='default_incomplete' if you want to confirm payment before activating.

request.sh
1curl https://api.stripe.com/v1/subscriptions \
2 -u "$STRIPE_SECRET_KEY:" \
3 -d customer="cus_PqKLt2eZvKYlo2C" \
4 -d "items[0][price]"="price_1OqKLt2eZvKYlo2Cghi" \
5 -d "payment_settings[save_default_payment_method]"="on_subscription" \
6 -d "expand[]"="latest_invoice.payment_intent"

Pro tip: Use payment_behavior='default_incomplete' when creating subscriptions — the subscription stays inactive until payment succeeds, preventing you from granting access before you've confirmed billing.

Expected result: A subscription object with status 'active' (payment succeeded) or 'trialing' (trial started). If status is 'incomplete', return the client_secret to the frontend for 3D Secure confirmation.

3

Handle Plan Upgrades and Downgrades

Why: Proration is applied by default when changing plans mid-cycle — understanding this prevents surprise charges to your customers.

To change a subscription's plan, PATCH /v1/subscriptions/{id} with the new price and the subscription item ID (si_*) you're replacing. Set proration_behavior: 'always_invoice' to charge the prorated difference immediately, 'create_prorations' to include it on the next invoice, or 'none' to skip proration entirely. For upgrades, always_invoice is the clearest customer experience.

request.sh
1# Get the subscription item ID first
2SUB_ITEM_ID=$(curl -s https://api.stripe.com/v1/subscriptions/sub_1OqKMj2eZvKYlo2C \
3 -u "$STRIPE_SECRET_KEY:" | python3 -c "import sys,json; print(json.load(sys.stdin)['items']['data'][0]['id'])")
4
5# Upgrade to new price, charge proration immediately
6curl -X POST https://api.stripe.com/v1/subscriptions/sub_1OqKMj2eZvKYlo2C \
7 -u "$STRIPE_SECRET_KEY:" \
8 -d "items[0][id]"="$SUB_ITEM_ID" \
9 -d "items[0][price]"="price_1NewPlanId" \
10 -d proration_behavior="always_invoice"

Pro tip: Preview proration before charging by calling POST /v1/subscriptions/{id}/upcoming_invoice (not a real invoice — just a preview). Show customers the prorated amount before they confirm the upgrade.

Expected result: The subscription updated with the new price. If proration='always_invoice', a new invoice is created and immediately charged for the prorated difference.

4

Handle Dunning with invoice.payment_failed Webhook

Why: Stripe retries failed payments automatically, but your application needs to know when a payment fails to restrict access, send customer notifications, and track dunning state.

Listen for invoice.payment_failed webhook events. When received, retrieve the subscription to check its status — 'past_due' means Stripe is still retrying, 'unpaid' means all retries exhausted. Use Stripe's Smart Retries (enabled by default in Billing settings) which retries 4 times over ~3 weeks. Add your own email/SMS notification on each failure event.

request.sh
1# Register webhook for payment failure events
2curl https://api.stripe.com/v1/webhook_endpoints \
3 -u "$STRIPE_SECRET_KEY:" \
4 -d url="https://yourapp.com/webhooks/stripe" \
5 -d "enabled_events[]"="invoice.payment_failed" \
6 -d "enabled_events[]"="invoice.paid" \
7 -d "enabled_events[]"="customer.subscription.deleted"

Pro tip: Stripe's Smart Retries intelligently schedules retry attempts based on machine learning signals about when the card is likely to succeed. Enable it in your Stripe Dashboard under Billing > Settings > Smart Retries.

Expected result: Your webhook handler receives and processes subscription lifecycle events, granting or revoking access based on payment status.

Complete working code

A complete subscription management module handling creation, plan changes, cancellation, and webhook processing. This shows the full integration pattern for a SaaS application — create customers, manage subscriptions, and react to billing events.

automate_stripe_subscriptions.py
1import stripe
2import os
3import logging
4from flask import Flask, request, jsonify
5
6logging.basicConfig(level=logging.INFO)
7log = logging.getLogger(__name__)
8
9stripe.api_key = os.environ['STRIPE_SECRET_KEY']
10WEBHOOK_SECRET = os.environ['STRIPE_WEBHOOK_SECRET']
11
12app = Flask(__name__)
13
14# --- Subscription Management Functions ---
15
16def get_or_create_customer(email, user_id):
17 existing = stripe.Customer.search(query=f'metadata["user_id"]:"{user_id}"')
18 if existing.data:
19 return existing.data[0].id
20 customer = stripe.Customer.create(email=email, metadata={'user_id': user_id})
21 log.info(f'Created customer {customer.id} for user {user_id}')
22 return customer.id
23
24def create_subscription(customer_id, price_id, trial_days=0):
25 params = {
26 'customer': customer_id,
27 'items': [{'price': price_id}],
28 'payment_settings': {'save_default_payment_method': 'on_subscription'},
29 'expand': ['latest_invoice.payment_intent']
30 }
31 if trial_days > 0:
32 import time
33 params['trial_end'] = int(time.time()) + trial_days * 86400
34
35 sub = stripe.Subscription.create(**params)
36 log.info(f'Subscription {sub.id} created, status: {sub.status}')
37 return sub
38
39def cancel_subscription(subscription_id, at_period_end=True):
40 if at_period_end:
41 sub = stripe.Subscription.modify(subscription_id, cancel_at_period_end=True)
42 log.info(f'Subscription {subscription_id} will cancel at period end')
43 else:
44 sub = stripe.Subscription.delete(subscription_id)
45 log.info(f'Subscription {subscription_id} canceled immediately')
46 return sub
47
48# --- Webhook Handler ---
49
50@app.route('/webhooks/stripe', methods=['POST'])
51def webhook():
52 payload = request.get_data() # RAW body must not be parsed first
53 sig = request.headers.get('Stripe-Signature')
54
55 try:
56 event = stripe.Webhook.construct_event(payload, sig, WEBHOOK_SECRET)
57 except stripe.error.SignatureVerificationError:
58 log.warning('Invalid webhook signature')
59 return jsonify({'error': 'Invalid signature'}), 400
60
61 obj = event.data.object
62 etype = event.type
63
64 if etype == 'invoice.paid':
65 log.info(f'Invoice paid: sub={obj.subscription}, customer={obj.customer}')
66 # grant_user_access(obj.customer)
67
68 elif etype == 'invoice.payment_failed':
69 log.warning(f'Payment failed attempt {obj.attempt_count}: sub={obj.subscription}')
70 # send_dunning_email(obj.customer_email, obj.attempt_count)
71 # if obj.attempt_count >= 3: restrict_access(obj.customer)
72
73 elif etype == 'customer.subscription.updated':
74 log.info(f'Subscription updated: {obj.id}, status={obj.status}')
75
76 elif etype == 'customer.subscription.deleted':
77 log.info(f'Subscription canceled: {obj.id}')
78 # revoke_user_access(obj.customer)
79
80 return jsonify({'status': 'ok'})
81
82if __name__ == '__main__':
83 app.run(port=3000)

Error handling

400This customer has no attached payment source or default payment method.
Cause

Attempting to create a subscription without a saved payment method on the customer. Stripe can't bill a customer without a payment method.

Fix

Attach a PaymentMethod to the customer and set it as default_payment_method in customer.invoice_settings before creating the subscription. Use Stripe Checkout or Elements to collect card details.

Retry strategy

No retry — add a payment method first

400No such price: 'price_...'
Cause

The price ID doesn't exist in the Stripe account, is in the wrong environment (test vs live), or was deleted.

Fix

Verify the price ID in your Stripe Dashboard under Products. Confirm you're using sk_live_* with live-mode price IDs and sk_test_* with test-mode price IDs.

Retry strategy

No retry — fix the price ID

402Your card was declined.
Cause

The customer's card was declined during subscription creation or renewal. Common reasons: insufficient funds, expired card, fraud detection.

Fix

Return the payment intent client_secret to the frontend and use stripe.confirmCardPayment() to let the customer retry or update their card. For renewals, the invoice.payment_failed webhook fires and Stripe retries automatically.

Retry strategy

For initial creation: collect new payment method. For renewals: Stripe's Smart Retries handles this automatically.

400Webhook signature verification failed
Cause

Webhook body was parsed (e.g., via body-parser JSON middleware) before being passed to stripe.webhooks.constructEvent(). Parsing modifies the body and breaks the HMAC verification.

Fix

In Express, use express.raw({type: 'application/json'}) for the webhook route. In Flask, use request.get_data() not request.json. The raw bytes must be passed to constructEvent unchanged.

Retry strategy

No retry — fix the middleware configuration

429Too many requests
Cause

Exceeded 100 req/sec rate limit. Can occur when processing many subscription changes simultaneously or handling a billing cycle spike with thousands of webhook events.

Fix

Process subscription changes with a queue (Redis, SQS) rather than all at once. For webhook handling, return 200 immediately and process asynchronously to avoid timeout-based retries that compound the load.

Retry strategy

Exponential backoff: 1s, 2s, 4s, 8s. Honor Retry-After header if present.

Rate Limits for Stripe API

ScopeLimitWindow
Live mode100 requestsper second
Test mode25 requestsper second
Billing cycle webhook burst1 event per subscriptionper renewal — can be 1,000+ simultaneously
retry-handler.ts
1import time
2import stripe
3
4def stripe_with_retry(fn, max_retries=5):
5 for attempt in range(max_retries):
6 try:
7 return fn()
8 except stripe.error.RateLimitError:
9 wait = min(2 ** attempt, 32)
10 print(f'Rate limit hit, waiting {wait}s')
11 time.sleep(wait)
12 except stripe.error.APIConnectionError as e:
13 if attempt == max_retries - 1: raise
14 time.sleep(2 ** attempt)
15 raise Exception('Max retries exceeded')
  • Process subscription changes asynchronously via a job queue (Celery, BullMQ) to avoid blocking requests and to handle billing cycle webhook spikes
  • Return 200 from webhook endpoints immediately, then process the event asynchronously — Stripe marks your endpoint as failed if no 2xx within 30 seconds
  • Use Stripe's idempotency keys when creating subscriptions: add Idempotency-Key: {unique-id} header to prevent duplicate subscriptions from retry logic
  • Cache subscription status in your database and update via webhooks — avoid calling GET /v1/subscriptions on every page load
  • Subscribe only to the webhook events you handle — unused events still count toward webhook delivery retries

Security checklist

  • Store STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in environment variables — never in source code
  • Verify webhook signatures with stripe.webhooks.constructEvent() using raw request body on every webhook request
  • Use payment_behavior='default_incomplete' on subscription creation to prevent access grants before payment confirmation
  • Validate subscription status from webhook events, not from the initial API response — payments can fail asynchronously
  • Implement idempotency keys on subscription creation to prevent duplicate billing from network retries
  • Never grant access based solely on the subscription creation API response — always wait for invoice.paid webhook confirmation
  • Use Restricted API keys with only Subscriptions and Customers write access for subscription management automation
  • Log all subscription state changes with timestamps for audit trails and customer support

Automation use cases

SaaS Onboarding Flow

intermediate

Collect payment via Stripe Elements, create customer and subscription, then grant application access only after receiving the invoice.paid webhook confirmation.

Dunning Email Sequence

intermediate

Send progressive dunning emails on invoice.payment_failed events — day 1: gentle reminder, day 7: card update link, day 14: account suspension warning.

Usage-Based Billing

advanced

Report usage quantities via stripe.SubscriptionItem.create_usage_record() for metered pricing, then let Stripe calculate and invoice automatically at period end.

Annual Plan Upgrade Incentive

intermediate

Detect monthly subscribers approaching 12 months and offer an annual plan switch with proration preview, then process the change via the API if accepted.

No-code alternatives

Don't want to write code? These platforms can automate the same workflows visually.

Zapier

Free tier (100 tasks/month), paid from $19.99/month

Zapier's Stripe integration can create subscriptions and react to lifecycle events, but complex dunning sequences and plan change logic require multiple multi-step Zaps.

Pros
  • + No code required
  • + Connects Stripe to CRM and email tools easily
  • + Quick setup for simple flows
Cons
  • - Limited control over proration behavior
  • - No support for complex dunning logic
  • - Costs per task at scale

Make

Free tier (1,000 ops/month), paid from $9/month

Make's Stripe module handles subscription creation and webhook responses with more control than Zapier, including conditional branching for dunning sequences.

Pros
  • + Visual dunning workflow builder
  • + Better conditional logic than Zapier
  • + Webhook trigger support
Cons
  • - Subscription management still limited vs native API
  • - Monthly operation limits
  • - No proration preview support

n8n

Free (self-hosted), Cloud from €20/month

n8n's HTTP Request node can call all Stripe subscription endpoints, with webhook trigger nodes for lifecycle events — gives full API access with a visual builder.

Pros
  • + Full Stripe API access including proration behavior
  • + Self-hostable
  • + Webhook trigger nodes built-in
Cons
  • - Requires technical configuration
  • - No native Stripe module
  • - Infrastructure management on self-hosted

Best practices

  • Grant application access based on invoice.paid webhook events, not the subscription creation API response — payment can fail asynchronously
  • Use payment_behavior='default_incomplete' for subscription creation so the subscription stays inactive until first payment succeeds
  • Preview proration amounts with POST /v1/subscriptions/{id}/upcoming_invoice before applying plan changes to show customers what they'll be charged
  • Always specify cancel_at_period_end=true for customer-initiated cancellations — immediate cancellation with no refund is surprising and generates disputes
  • Implement idempotency keys (Idempotency-Key header) for subscription creation to prevent duplicate charges from network timeouts
  • Use Stripe's Smart Retries (Billing > Settings) which automatically retries failed payments at optimal times based on card network signals
  • Store Stripe customer IDs and subscription IDs in your database immediately on creation — use Stripe's customer search as a fallback only

Ask AI to help

Copy one of these prompts to get a personalized, working implementation.

ChatGPT / Claude Prompt

I'm building Stripe subscription management in Python with Flask. I use stripe.Subscription.create() with expand=['latest_invoice.payment_intent'] and handle webhooks with stripe.Webhook.construct_event(). Help me: (1) implement the full webhook handler for invoice.paid, invoice.payment_failed, and customer.subscription.deleted events, (2) add idempotency keys to subscription creation, (3) preview proration before plan changes using the upcoming invoice endpoint, and (4) handle 3D Secure (requires_action payment intent status) by returning the client_secret to my frontend.

Lovable / V0 Prompt

Build a Stripe subscription management UI in React with these features: (1) a 'Subscribe' button that creates a Stripe Checkout session via a backend API endpoint, (2) a subscription status card showing current plan, next billing date, and amount, (3) a 'Change Plan' modal with plan options and a proration preview fetched from the backend, (4) a 'Cancel Subscription' button with confirmation dialog and cancel_at_period_end option, (5) a billing history table showing past invoices with download links. The backend endpoints are: POST /api/subscribe, GET /api/subscription, PATCH /api/subscription/change-plan, DELETE /api/subscription.

Frequently asked questions

What's the difference between cancel_at_period_end and DELETE /v1/subscriptions?

Setting cancel_at_period_end=true via PATCH keeps the subscription active until the end of the current billing period, then cancels — the customer retains access and you don't owe a refund. DELETE /v1/subscriptions/{id} cancels immediately with no prorated refund by default. For customer-initiated cancellations, always use cancel_at_period_end=true. For fraud or ToS violations, use immediate cancellation.

What happens with proration when a customer upgrades mid-cycle?

By default (proration_behavior='create_prorations'), Stripe calculates the unused portion of the current plan as a credit and the remaining days on the new plan as a charge. These appear as proration line items on the next invoice. Use proration_behavior='always_invoice' to charge the difference immediately (better UX), or 'none' to skip proration entirely (simpler but customer pays full price on both plans for the overlap period).

How many times does Stripe retry a failed subscription payment?

Stripe Billing's Smart Retries attempts payment up to 4 times over approximately 3 weeks by default. The exact timing is determined by machine learning based on when the card is most likely to succeed. You can customize the retry schedule in Stripe Dashboard > Billing > Settings > Smart Retries. After all retries fail, the subscription moves to 'unpaid' status and the customer.subscription.deleted event fires if you've configured it to cancel on unpaid.

Why does my webhook keep failing signature verification?

The most common cause is parsing the request body before passing it to stripe.webhooks.constructEvent(). In Express.js, using express.json() middleware on the webhook route parses the body, which changes it and breaks the HMAC signature. Use express.raw({type: 'application/json'}) specifically for the webhook route. In Flask, use request.get_data() not request.json. The raw bytes from the request must be passed to constructEvent unchanged.

What happens when I hit the rate limit during a billing cycle?

Stripe returns a 429 Too Many Requests error. This can happen if you're processing thousands of subscription updates simultaneously. The fix: use a job queue (Celery, BullMQ) to process subscription changes asynchronously. For webhooks, return 200 immediately and process asynchronously — if your endpoint times out (30 seconds), Stripe retries the webhook, which compounds the load.

Is Stripe Billing free to use?

Stripe Billing has a fee of 0.5% of recurring revenue for standard plans or 0.8% for custom billing features (revenue recognition, prorations, multi-currency). This is on top of Stripe's standard payment processing fee (2.9% + $0.30 per transaction). There's no monthly subscription fee — you only pay a percentage of what you bill through Stripe.

Can RapidDev help build a complete subscription billing system?

Yes. RapidDev has built 600+ apps including full SaaS billing systems with subscription management, dunning sequences, usage-based billing, and revenue dashboards. If you need a custom Stripe Billing integration, get a free consultation at rapidevelopers.com.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.