The authentication system includes automatic Stripe subscription migration to seamlessly transition users from legacy applications while preserving their existing subscriptions, billing cycles, and pricing.
Automatic migration attempts happen during the following authentication flows:
POST /api/auth/loginPOST /api/auth/registerPOST /api/auth/google/loginPOST /api/auth/google/registerPOST /api/auth/google/callback-pkce (mode: login)POST /api/auth/google/callback-pkce (mode: register)POST /api/auth/apple/callback (mode: login)POST /api/auth/apple/callback (mode: register)| Scenario | Behavior | User Experience |
|---|---|---|
| User has local subscription | No migration attempted; existing record used | Immediate access with existing subscription |
| User has Stripe subscription | Background migration imports subscription | Can log in immediately; subscription appears after migration |
| User has no subscription | Migration finds nothing; no error | Can log in and subscribe normally |
| Migration error occurs | Error logged; authentication succeeds | Can log in; migration retried on next login |
cancel_at_period_end=true are migrated with their cancellation status preservedMigration is designed to never block user authentication:
The migration process follows these steps:
1. Check if user has local subscription (migration_status field) 2. If not, search Stripe for customer by email 3. If customer found, retrieve all subscriptions 4. Filter for active subscriptions (active, trialing, past_due) 5. Select most recent subscription if multiple exist 6. Extract price ID and sync price/plan to database 7. Create local subscription record with all metadata 8. Mark migration as complete (migration_status='migrated') 9. Log success/failure to audit table 10. Continue with authentication flow
Subscription records include migration tracking fields:
migration_status - null (not migrated), 'migrated', or 'migration_failed'migrated_at - Timestamp when migration completedlegacy_subscription_id - Original Stripe subscription IDWhen implementing client applications:
/api/subscriptions
List all subscriptions with filtering and pagination (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Query Parameters:
sort - JSON array with field name and direction
["fieldName","ASC|DESC"]
id, userId, username, email, subscriptionPlanId, status, currentPeriodStart, currentPeriodEnd, trialStart, trialEnd, cancelAtPeriodEnd, canceledAt, createdAt, updatedAt, planName, interval, amount, currency, trialPeriodDays, isActive
sort=["createdAt","DESC"] - Sort by creation date in descending order
page - Page number (1-based)
page=1 - Get the first page
perPage - Number of items per page
perPage=10 - Show 10 items per page
filter - JSON object with field name/value pairs for filtering
{"fieldName1":"value1","fieldName2":value2}
filter={"status":"active","planName":"Pro"} - Filter active subscriptions with plan name containing "Pro"
q - Global search across username, email, subscription status, plan name, plan interval, and currencyusername - Filter by username (partial match, case-insensitive)email - Filter by email address (partial match, case-insensitive)status - Filter by subscription status (active, trialing, past_due, canceled, incomplete, etc.)planName - Filter by subscription plan name (partial match, case-insensitive)interval - Filter by billing interval (month, year)amount - Filter by plan amount in cents (exact match)currency - Filter by currency code (usd, eur, etc.)trialPeriodDays - Filter by trial period days (exact match)isActive - Filter by plan active status (true/false)createdAt - Filter by subscription creation date (exact date or date range)updatedAt - Filter by last update date (exact date or date range)currentPeriodStart - Filter by current period start date (exact date or date range)currentPeriodEnd - Filter by current period end date (exact date or date range)trialStart - Filter by trial start date (exact date or date range)trialEnd - Filter by trial end date (exact date or date range)canceledAt - Filter by cancellation date (exact date or date range)q in the filter will search across all text columns:
filter={"q":"pro","status":"active"} - Find active subscriptions with
"pro" in any text field
syncWithStripe - Whether to sync with Stripe before fetching data (default: true)
Example Requests:
GET /api/subscriptions?page=1&perPage=10&sort=["createdAt","DESC"]&filter={"status":"active"}
GET /api/subscriptions?page=2&perPage=20&filter={"q":"pro","status":"active"}
GET /api/subscriptions?filter={"planName":"Premium","interval":"month"}
GET /api/subscriptions?filter={"currency":"usd","amount":999}
GET /api/subscriptions?filter={"email":"gmail.com","isActive":true}
Response (200 OK):
{
"data": [
{
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 1,
"stripePriceId": "price_...",
"name": "Pro Plan",
"interval": "month",
"amount": 999,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": "2023-06-01T00:00:00Z",
"trialEnd": "2023-06-15T00:00:00Z",
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": null,
"promotion": {
"id": 5,
"name": "Summer Sale 2023",
"coupon": {
"id": 3,
"type": "DISC",
"name": "Discount Percentage",
"percentOff": 20,
"trialDays": null
}
},
"discount": {
"percentOff": 20,
"amountOff": 200,
"discountedAmount": 799
}
}
// Additional subscription objects...
],
"total": 42
}
Notes:
planName, interval, amount,
etc.syncWithStripe parameter allows you to ensure that all subscription data is fresh
from Stripe before applying filters and pagination/api/subscriptions/me
Get the current user's subscription (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Response (200 OK):
{
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 1,
"stripePriceId": "price_...",
"name": "Pro Plan",
"interval": "month",
"amount": 999,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": "2023-06-01T00:00:00Z",
"trialEnd": "2023-06-15T00:00:00Z",
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": null,
"promotion": {
"id": 5,
"name": "Summer Sale 2023",
"coupon": {
"id": 3,
"type": "DISC",
"name": "Discount Percentage",
"percentOff": 20,
"trialDays": null
}
},
"discount": {
"percentOff": 20,
"amountOff": 200,
"discountedAmount": 799
}
}
Notes:
plan.amount field shows the base plan price in cents (e.g., 999 = $9.99)discount field shows:
percentOff - The discount percentage appliedamountOff - The discount amount in centsdiscountedAmount - The final price after discount in cents (e.g., 799 = $7.99)promotion field will be null and discount will be null/api/subscriptions/plans
List all available subscription plans (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Query Parameters:
sort - JSON array with field name and direction
["fieldName","ASC|DESC"]
id, stripePriceId, name, interval, amount, currency, trialPeriodDays, isActive, createdAt, updatedAt
sort=["amount","ASC"] - Sort by price amount in ascending order
sort=["name","ASC"] - Sort by plan name in ascending order
page - Page number (1-based)
page=1 - Get the first page
perPage - Number of items per page
perPage=10 - Show 10 items per page
filter - JSON object with field name/value pairs for filtering
{"fieldName1":"value1","fieldName2":value2}
filter={"isActive":true} - Show only active subscription plans
filter={"interval":"month"} - Filter by billing interval
filter={"currency":"usd"} - Filter by currency
q - Global search across plan name, interval, and currencyname - Filter by plan name (partial match, case-insensitive)interval - Filter by billing interval (month, year, etc.)amount - Filter by plan amount in cents (exact match)currency - Filter by currency code (usd, eur, etc.)trialPeriodDays - Filter by trial period days (exact match)isActive - Filter by plan active status (true/false)q in the filter will search across all text columns:
filter={"q":"pro"} - Find plans with "pro" in their name
syncWithStripe - Whether to sync with Stripe before fetching data (default: true)
Example Requests:
GET /api/subscriptions/plans?page=1&perPage=10&sort=["amount","ASC"]&filter={"isActive":true}&syncWithStripe=true
GET /api/subscriptions/plans?page=2&perPage=20&filter={"q":"pro","interval":"month"}
GET /api/subscriptions/plans?filter={"currency":"usd","amount":999}
GET /api/subscriptions/plans?filter={"trialPeriodDays":14,"isActive":true}
Response (200 OK):
{
"data": [
{
"id": 1,
"stripePriceId": "price_...",
"name": "Basic Plan",
"interval": "month",
"amount": 499,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
{
"id": 2,
"stripePriceId": "price_...",
"name": "Pro Plan",
"interval": "month",
"amount": 999,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
}
// Additional plan objects...
],
"total": 5
}
Notes:
syncWithStripe parameter allows you to ensure that all subscription data is fresh
from Stripe before applying filters and pagination/api/subscriptions/verify-payment
Verify payment status for a subscription using payment intent ID (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Query Parameters:
paymentIntentId - The Stripe payment intent ID to verifyResponse (200 OK):
{
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 1,
"stripePriceId": "price_...",
"name": "Pro Plan",
"interval": "month",
"amount": 999,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": null,
"trialEnd": null,
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": "2023-06-15T00:00:00Z",
"promotion": null
}
Error Responses:
{
"message": "Payment intent ID is required"
}
{
"message": "Subscription not found or could not be updated"
}
/api/subscriptions/verify-setup
Verify setup intent status for a subscription (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Query Parameters:
setupIntentId - The Stripe setup intent ID to verifyResponse (200 OK):
{
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 1,
"stripePriceId": "price_...",
"name": "Pro Plan",
"interval": "month",
"amount": 999,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": null,
"trialEnd": null,
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": "2023-06-15T00:00:00Z",
"promotion": null
}
Error Responses:
{
"message": "Setup intent ID is required"
}
{
"message": "Subscription not found or could not be updated"
}
/api/subscriptions/change-plan
Change the user's subscription to a different plan (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Request Body:
{
"planId": 2,
"prorationBehavior": "create_prorations" // Optional: "create_prorations", "none", or "always_invoice"
}
Response (200 OK):
{
"message": "Subscription plan changed successfully",
"subscription": {
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 2,
"stripePriceId": "price_...",
"name": "Premium Plan",
"interval": "month",
"amount": 1999,
"currency": "usd",
"trialPeriodDays": 0,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": null,
"trialEnd": null,
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": "2023-06-15T00:00:00Z",
"promotion": null
}
}
Error Responses:
{
"message": "Plan ID is required"
}
{
"message": "You don't have an active subscription"
}
{
"message": "You are already subscribed to this plan"
}
{
"message": "The selected plan is not available"
}
/api/subscriptions/change-plan-with-payment-method
Change the user's subscription to a different plan while updating payment method (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Request Body:
{
"planId": 2,
"paymentMethodId": "pm_1234567890",
"prorationBehavior": "create_prorations" // Optional: "create_prorations", "none", or "always_invoice"
}
Response (200 OK):
{
"requiresAction": true,
"setupIntentClientSecret": "seti_1RJYc0PFpKHu6YD8quMd7VH8_secret_...",
"setupIntentId": "seti_1RJYc0PFpKHu6YD8quMd7VH8",
"planId": 2,
"prorationBehavior": "create_prorations"
}
Error Responses:
{
"message": "Plan ID is required"
}
{
"message": "Payment method ID is required"
}
{
"message": "You don't have an active subscription"
}
{
"message": "There was a problem setting up the payment method"
}
/api/subscriptions/confirm-change-plan
Confirm plan change after setup intent is completed (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Request Body:
{
"setupIntentId": "seti_1RJYc0PFpKHu6YD8quMd7VH8",
"planId": 2,
"prorationBehavior": "create_prorations" // Optional: "create_prorations", "none", or "always_invoice"
}
Response (200 OK):
{
"message": "Subscription plan and payment method updated successfully",
"subscription": {
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 2,
"stripePriceId": "price_...",
"name": "Premium Plan",
"interval": "month",
"amount": 1999,
"currency": "usd",
"trialPeriodDays": 0,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": null,
"trialEnd": null,
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": "2023-06-15T00:00:00Z",
"promotion": null
}
}
Error Responses:
{
"message": "Setup intent ID is required"
}
{
"message": "Plan ID is required"
}
{
"message": "Setup intent is not complete. Current status: requires_payment_method"
}
{
"message": "Failed to attach payment method"
}
/api/subscriptions/create-free-trial
Create a free trial subscription without requiring payment method (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Request Body:
{
"priceId": "price_H5ggYwtDq4fbrJ",
"trialPeriodDays": 14 // Optional: Defaults to 14 days if not specified
}
Response (200 OK):
{
"message": "Free trial subscription created successfully",
"subscription": {
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 1,
"stripePriceId": "price_H5ggYwtDq4fbrJ",
"name": "Pro Plan",
"interval": "month",
"amount": 999,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "trialing",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": "2023-06-01T00:00:00Z",
"trialEnd": "2023-06-15T00:00:00Z",
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": null,
"promotion": null
},
"trialEnd": "2023-06-15T00:00:00Z"
}
Error Responses:
{
"message": "User already has an active subscription"
}
{
"message": "Price ID is required"
}
{
"message": "User not found"
}
{
"message": "Subscription plan not found"
}
/api/subscriptions/create-with-payment-method
Create a subscription with payment method ID (deferred payment flow) (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Request Body:
{
"priceId": "price_H5ggYwtDq4fbrJ",
"paymentMethodId": "pm_1234567890",
"trialPeriodDays": 14, // Optional: Number of trial days
"promoCode": "SUMMER20", // Optional: Promotion code to apply (provide either promoCode or promotionId, not both)
"promotionId": 5 // Optional: Promotion ID (database ID) to apply (provide either promoCode or promotionId, not both)
}
Response (200 OK):
{
"subscriptionId": "sub_1RJYc0PFpKHu6YD8quMd7VH8",
"type": "payment",
"clientSecret": "cs_test_...",
"promotionApplied": true
}
Alternative Response (Setup Flow):
{
"subscriptionId": "sub_1RJYc0PFpKHu6YD8quMd7VH8",
"type": "setup",
"clientSecret": "seti_1RJYc0PFpKHu6YD8quMd7VH8_secret_...",
"promotionApplied": true
}
Error Responses:
{
"message": "Price ID is required"
}
{
"message": "Payment method ID is required"
}
{
"message": "Please provide either promoCode or promotionId, not both"
}
{
"message": "Invalid promotion code"
}
{
"message": "Promotion with ID 5 not found"
}
{
"message": "Promotion code \"SUMMER20\" not found in Stripe"
}
{
"message": "This promotion cannot be applied to subscriptions"
}
{
"message": "User already has an active subscription"
}
{
"message": "Subscription plan not found"
}
Notes:
promoCode (string) or promotionId (number), but not bothpromoCode is the promotion code string (e.g., "SUMMER20")promotionId is the database ID from the promotions tablepromotionApplied field in the response indicates whether a promotion was successfully applied/api/subscriptions/migrate-and-sync
Trigger bulk migration and synchronization of Stripe subscriptions for SUBS users (requires authentication and admin privileges). This endpoint searches Stripe for subscriptions associated with user email addresses and imports them into the local database, preserving all billing cycles, pricing, and trial information. It can also re-sync existing subscriptions to ensure data consistency between Stripe and the local database. This is particularly useful for:
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr... x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3
Request Body:
{
"batchSize": 50, // Optional: Number of users to process (default: 50)
"dryRun": false, // Optional: If true, returns users to migrate without processing (default: false)
"resync": false, // Optional: If true, re-syncs ALL SUBS users including those with subscriptions (default: false)
"forceResync": false, // Optional: If true, includes users synced within the last hour (default: false)
"activeUsersOnly": true // Optional: If true, only migrates users who have logged in at least once (default: true)
}
Response (200 OK) - Dry Run:
{
"message": "Dry run completed - no migrations performed",
"usersToMigrate": 42,
"users": [
{
"id": 123,
"email": "user@example.com",
"hasSubscription": false
},
{
"id": 456,
"email": "existing@example.com",
"hasSubscription": true
}
// Additional users...
],
"note": "Excluding users synced within the last hour",
"activeUsersOnly": true
}
Response (200 OK) - Actual Migration:
{
"message": "Bulk migration completed",
"results": {
"total": 50,
"successful": 30, // New migrations
"failed": 5,
"skipped": 10,
"resynced": 5 // Existing subscriptions that were re-synced
},
"errors": [
{
"userId": 456,
"email": "failed@example.com",
"error": "Failed to sync price: Price not found in Stripe"
}
// Additional errors if any...
]
}
Response (200 OK) - Re-sync Mode:
{
"message": "Bulk re-sync completed",
"results": {
"total": 50,
"successful": 15, // New migrations found
"failed": 2,
"skipped": 8, // No Stripe subscriptions found
"resynced": 25 // Existing subscriptions updated with latest Stripe data
}
}
Error Responses:
{
"message": "Access denied. Admin privileges required."
}
Notes:
last_login_at IS NOT NULLfalse to include all SUBS users regardless of login statussynced_at timestamp on the subscriptionforceResync=true to override this behavior and re-process all usersresync=false, activeUsersOnly=trueresync=false, activeUsersOnly=falseresync=true, activeUsersOnly=true, batchSize=50 multiple timesresync=true, activeUsersOnly=falseresync=true, forceResync=truedryRun=true to see what would be processed/api/subscriptions/resume
Resume a subscription that was previously scheduled for cancellation (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Response (200 OK):
{
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 1,
"stripePriceId": "price_...",
"name": "Pro Plan",
"interval": "month",
"amount": 999,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": null,
"trialEnd": null,
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": "2023-06-15T00:00:00Z",
"promotion": null
}
Error Responses:
{
"message": "Subscription not found or could not be resumed"
}
{
"message": "Failed to resume subscription",
"error": "Cannot resume subscription because it is already fully canceled"
}
/api/subscriptions/sync-plans
Sync subscription plans with Stripe (requires authentication and admin privileges). This endpoint synchronizes subscription plans from Stripe into the local database, ensuring pricing and plan information is up-to-date.
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr... x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3
Request Body:
{
"dryRun": false // Optional: If true, returns plans to sync without processing (default: false)
}
Response (200 OK) - Dry Run:
{
"message": "Dry run completed - no plans were synced",
"plansToSync": 8,
"details": {
"existingPlans": [
{
"id": 1,
"stripePriceId": "price_...",
"name": "Basic Plan",
"amount": 499,
"currency": "usd",
"interval": "month",
"isActive": true
}
// Additional existing plans...
],
"stripePrices": [
{
"id": "price_...",
"nickname": "Basic Monthly",
"amount": 499,
"currency": "usd",
"interval": "month",
"active": true
}
// Additional Stripe prices...
],
"analysis": {
"totalExistingPlans": 5,
"totalStripePrices": 8,
"plansToUpdate": 2,
"plansToAdd": 3,
"plansToDeactivate": 0
}
}
}
Response (200 OK) - Actual Sync:
{
"message": "Successfully synchronized 8 subscription plans with Stripe",
"syncedPlans": 8,
"results": {
"synced": 5,
"added": 3,
"deactivated": 0
}
}
Error Responses:
{
"message": "Access denied. Admin privileges required."
}
Notes:
dryRun=true to preview which plans will be synced without making any changes/api/subscriptions/update-payment-method
Update the payment method for a user's subscription (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Request Body:
{
"paymentMethodId": "pm_1234567890"
}
Response (200 OK):
{
"success": true,
"message": "Payment method updated successfully",
"subscription": {
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 1,
"stripePriceId": "price_...",
"name": "Pro Plan",
"interval": "month",
"amount": 999,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": null,
"trialEnd": null,
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": "2023-06-15T00:00:00Z",
"promotion": null
}
}
Error Responses:
{
"success": false,
"message": "Payment method ID is required"
}
{
"success": false,
"message": "Failed to update payment method",
"error": {
"type": "card_error",
"message": "Your card was declined.",
"code": "card_declined",
"decline_code": "generic_decline"
}
}
Notes:
/api/subscriptions/me
Update the current user's subscription (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr... x-csrf-token: a8d7f9c6e5b4a3c2d1e0f9a8d7f6c5b4a3
Request Body:
{
"planId": 2,
"paymentMethodId": "pm_1234567890" // Optional: New payment method
}
Response (200 OK):
{
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 2,
"stripePriceId": "price_...",
"name": "Premium Plan",
"interval": "month",
"amount": 1999,
"currency": "usd",
"trialPeriodDays": 0,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": null,
"trialEnd": null,
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": "2023-06-15T00:00:00Z",
"promotion": null
}
/api/subscriptions/me
Cancel the current user's subscription (requires authentication)
Headers:
Authorization: Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Query Parameters:
cancelAtPeriodEnd - Whether to cancel at period end (default: true). Set to false for immediate cancellation.Response (200 OK):
{
"id": 1,
"userId": 1,
"email": "user@example.com",
"username": "user123",
"plan": {
"id": 1,
"stripePriceId": "price_...",
"name": "Pro Plan",
"interval": "month",
"amount": 999,
"currency": "usd",
"trialPeriodDays": 14,
"isActive": true,
"createdAt": "2023-01-15T08:30:00Z",
"updatedAt": null
},
"status": "active",
"currentPeriodStart": "2023-06-01T00:00:00Z",
"currentPeriodEnd": "2023-07-01T00:00:00Z",
"trialStart": null,
"trialEnd": null,
"cancelAtPeriodEnd": true,
"canceledAt": "2023-06-15T00:00:00Z",
"createdAt": "2023-06-01T00:00:00Z",
"updatedAt": "2023-06-15T00:00:00Z",
"promotion": null
}