Billing
Manage subscriptions, preview plan changes, view payment history, and track usage. Plans and coupon validation endpoints are public — all subscription management endpoints require a valid session (JWT cookie or Bearer token).
List Plans
GET https://api.indreelforge.com/api/plans
Returns all active plans ordered by tier. No authentication required.
curl https://api.indreelforge.com/api/plans
Response
{
"plans": [
{
"id": 1,
"name": "Free",
"slug": "free",
"description": "Get started with IndReelForge",
"price_monthly": "0.00",
"price_annual": "0.00",
"annual_total": "0.00",
"upload_limit": 10,
"profile_limit": 2,
"seat_limit": 1,
"has_whitelabel": false,
"has_scheduling": false,
"has_analytics": false,
"has_priority_support": false,
"ffmpeg_minutes_monthly": 10,
"features": ["10 uploads / month", "2 connected profiles"],
"addons": [],
"sort_order": 0
},
{
"id": 2,
"name": "Basic",
"slug": "basic",
"description": "For individuals and creators",
"price_monthly": "19.00",
"price_annual": "15.83",
"annual_total": "190.00",
"upload_limit": 100,
"profile_limit": 5,
"seat_limit": 1,
"has_whitelabel": false,
"has_scheduling": true,
"has_analytics": true,
"has_priority_support": false,
"ffmpeg_minutes_monthly": 60,
"features": ["100 uploads / month", "5 connected profiles", "Scheduling", "Analytics"],
"addons": [
{ "slug": "profiles_5", "label": "+5 profiles", "price_monthly": "9.00" }
],
"sort_order": 1
}
]
}
upload_limit is the string "unlimited" for plans with no cap.
Get Plan by Slug
GET https://api.indreelforge.com/api/plans/:slug
Returns a single active plan. No authentication required.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string (path) | Yes | Plan identifier: free, basic, professional, advanced, or business |
curl https://api.indreelforge.com/api/plans/professional
Response
{
"plan": {
"id": 3,
"name": "Professional",
"slug": "professional",
"description": "For growing teams",
"price_monthly": "49.00",
"price_annual": "40.83",
"annual_total": "490.00",
"upload_limit": 500,
"profile_limit": 15,
"seat_limit": 3,
"has_whitelabel": false,
"has_scheduling": true,
"has_analytics": true,
"has_priority_support": true,
"ffmpeg_minutes_monthly": 300,
"features": ["500 uploads / month", "15 connected profiles", "3 team seats", "Priority support"],
"addons": []
}
}
Returns 404 if the slug does not match an active plan.
Get Subscription
GET https://api.indreelforge.com/api/billing/subscription
Returns the current subscription state for the authenticated user, including plan limits, billing status, and saved payment method details. Free users receive a synthesised subscription object rather than null, so the response shape is consistent across all plan tiers.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
skip_payment_method | 1 or true | No | Skip the Paddle payment-method lookup. Useful when polling for subscription activation and the card info is not needed. |
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
'https://api.indreelforge.com/api/billing/subscription'
Response — paid user
{
"subscription": {
"id": "sub_01abc123",
"plan_slug": "professional",
"plan_name": "Professional",
"billing_cycle": "monthly",
"status": "active",
"current_period_start": "2026-04-01T00:00:00.000Z",
"current_period_end": "2026-05-01T00:00:00.000Z",
"cancel_at_period_end": false,
"cancelled_at": null,
"pending_plan_change": null,
"pending_billing_cycle": null,
"scheduled_change_at": null,
"price": "49.00",
"annual_total": "490.00",
"upload_limit": 500,
"profile_limit": 15,
"seat_limit": 3,
"ffmpeg_minutes_monthly": 300,
"has_scheduling": true,
"has_analytics": true,
"has_whitelabel": false,
"is_managed": false
},
"payment_method": {
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2027
},
"is_free": false
}
Response — free user
{
"subscription": {
"id": null,
"plan_slug": "free",
"plan_name": "Free",
"billing_cycle": "monthly",
"status": "free",
"current_period_start": null,
"current_period_end": null,
"cancel_at_period_end": false,
"cancelled_at": null,
"pending_plan_change": null,
"pending_billing_cycle": null,
"scheduled_change_at": null,
"price": 0,
"annual_total": 0,
"upload_limit": 10,
"profile_limit": 2,
"seat_limit": 1,
"ffmpeg_minutes_monthly": 0,
"has_scheduling": false,
"has_analytics": false,
"has_whitelabel": false,
"is_managed": false
},
"payment_method": null,
"is_free": true
}
subscription.status values: free, active, trialing, past_due, pending_downgrade, cancelled, expired.
payment_method is null when no card is saved or when skip_payment_method=1 is passed.
is_managed: true indicates an admin-granted plan that has no underlying Paddle subscription — payment-related UI actions should be hidden.
Change Plan
POST https://api.indreelforge.com/api/billing/change-plan
Schedules a plan change or billing-cycle switch. Requires an active Paddle subscription; free users must go through the checkout flow. Requires 2FA to be verified on the current session.
Both upgrades and downgrades take effect at the end of the current billing period — the user keeps their current plan until then.
Passing planSlug: "free" cancels the subscription at the end of the current period (equivalent to calling POST /cancel).
| Body Parameter | Type | Required | Description |
|---|---|---|---|
planSlug | string | Yes | Target plan: free, basic, professional, advanced, or business |
billingCycle | string | No | monthly (default) or annual |
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"planSlug": "advanced", "billingCycle": "annual"}' \
-X POST https://api.indreelforge.com/api/billing/change-plan
Response
{
"success": true,
"effective": "next_billing_cycle",
"scheduled_at": "2026-05-01T00:00:00.000Z",
"message": "Your plan will upgrade to Advanced at the start of your next billing cycle (5/1/2026). You keep your current plan until then."
}
Error responses
| Status | code | Description |
|---|---|---|
400 | NO_SUBSCRIPTION | No active subscription exists; use the checkout flow |
400 | — | Already on this plan and billing cycle |
400 | NO_PADDLE_PRICE | Paddle price ID not configured for this plan/cycle |
402 | PAST_DUE | Last payment failed; update payment method first |
403 | PADDLE_FORBIDDEN | Plan change must be completed via Paddle customer portal; portal_url included in the response |
409 | BILLING_LOCK_TIMEOUT | A concurrent billing operation is in progress |
Preview Plan Change
POST https://api.indreelforge.com/api/billing/preview-change
Returns a cost preview for switching to a different plan or billing cycle. Uses Paddle's proration API when available, falling back to a local price comparison for users without an active subscription.
No 2FA required — this is a read-only operation.
| Body Parameter | Type | Required | Description |
|---|---|---|---|
planSlug | string | Yes | Target plan slug |
billingCycle | string | No | monthly (default) or annual |
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"planSlug": "business", "billingCycle": "monthly"}' \
-X POST https://api.indreelforge.com/api/billing/preview-change
Response
{
"current_plan": "professional",
"new_plan": "business",
"current_price": 49,
"new_price": 99,
"is_upgrade": true,
"is_downgrade": false,
"effective": "next_billing_cycle",
"prorated_immediate_charge": null,
"next_billed_amount": 99.00,
"next_billed_at": "2026-05-01T00:00:00.000Z",
"used_paddle_preview": true,
"message": "This upgrade takes effect at the start of your next billing cycle. You keep your current plan until then."
}
prorated_immediate_charge and next_billed_amount are null when Paddle preview is unavailable (free users or Paddle misconfigured).
used_paddle_preview: true means the amounts are authoritative from Paddle; false means they are local estimates.
Cancel Subscription
POST https://api.indreelforge.com/api/billing/cancel
Schedules the subscription to cancel at the end of the current billing period. The user keeps full access until then. Requires 2FA.
| Body Parameter | Type | Required | Description |
|---|---|---|---|
reason | string | No | Cancellation reason: too_expensive, missing_features, switching_competitor, not_using, technical_issues, temporary_pause, or other |
other_text | string | No | Free-text explanation (max 500 chars); only recorded when reason is other |
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"reason": "too_expensive"}' \
-X POST https://api.indreelforge.com/api/billing/cancel
Response
{
"success": true,
"message": "Your subscription will end on 5/1/2026. You will keep access until then.",
"ends_at": "2026-05-01T00:00:00.000Z"
}
Returns 400 if there is no active subscription to cancel, or 409 with BILLING_LOCK_TIMEOUT if a concurrent billing operation is in progress.
Reactivate Subscription
POST https://api.indreelforge.com/api/billing/reactivate
Reverses a scheduled cancellation. Only valid when cancel_at_period_end is true on the current subscription. Requires 2FA.
No request body is needed.
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
-X POST https://api.indreelforge.com/api/billing/reactivate
Response
{
"success": true,
"message": "Your subscription has been reactivated. It will continue to renew on the next billing date."
}
Returns 400 if no scheduled cancellation is found, or 409 with BILLING_LOCK_TIMEOUT if a concurrent billing operation is in progress.
Payment History
GET https://api.indreelforge.com/api/billing/payments
Returns a paginated list of payments for the authenticated user.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number, default 1 |
limit | integer | No | Results per page, default 20, max 100 |
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
'https://api.indreelforge.com/api/billing/payments?page=1&limit=20'
Response
{
"payments": [
{
"id": "pay_01abc",
"plan_slug": "professional",
"plan_name": "Professional",
"amount": "49.00",
"currency": "USD",
"billing_cycle": "monthly",
"provider": "paddle",
"status": "completed",
"provider_invoice_url": "https://invoice.paddle.com/inv/...",
"provider_invoice_pdf": null,
"created_at": "2026-04-01T00:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 6
}
}
Fetch Invoice
GET https://api.indreelforge.com/api/billing/payments/:id/invoice
Returns a URL for the hosted invoice or PDF for a specific payment. If a cached URL exists it is returned immediately; otherwise the URL is fetched from Paddle, cached in the database, and then returned.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string (path) | Yes | Payment record ID |
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
https://api.indreelforge.com/api/billing/payments/pay_01abc/invoice
Response
{
"url": "https://invoice.paddle.com/inv/live_abc123..."
}
Returns 404 if the payment is not found, belongs to another user, or Paddle has not yet generated an invoice for the transaction.
Upcoming Charge
GET https://api.indreelforge.com/api/billing/upcoming
Returns the next expected charge synthesised from the current subscription period end date. Currency is derived from the user's most recent completed payment to correctly reflect non-USD billing.
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
https://api.indreelforge.com/api/billing/upcoming
Response — active subscription
{
"upcoming": {
"amount_due": 49.00,
"currency": "USD",
"period_start": null,
"period_end": 1746057600,
"next_payment_attempt": 1746057600
}
}
period_end and next_payment_attempt are Unix timestamps. Returns { "upcoming": null } when the user has no active or past-due subscription.
Usage Metrics
GET https://api.indreelforge.com/api/billing/usage
Returns the current billing-period usage against plan limits. Profile limits include any active add-on slots purchased on top of the base plan.
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
https://api.indreelforge.com/api/billing/usage
Response
{
"uploads": {
"used": 42,
"limit": 500,
"resets_at": "2026-05-01T00:00:00.000Z"
},
"profiles": {
"used": 8,
"limit": 20,
"base_limit": 15,
"addon_slots": 5
},
"api_keys": {
"used": 2
},
"ffmpeg": {
"used": 37,
"limit": 300,
"resets_at": "2026-05-01T00:00:00.000Z"
},
"features": {
"has_scheduling": true,
"has_analytics": true,
"has_whitelabel": false
},
"billing_cycle": {
"start": "2026-04-01T00:00:00.000Z",
"end": "2026-05-01T00:00:00.000Z"
}
}
uploads.limit is null for plans with unlimited uploads. billing_cycle.start is null for free users. resets_at reflects the subscription current_period_end for paid users and the monthly reset timestamp for free users.
Validate Coupon
POST https://api.indreelforge.com/api/coupons/validate
Validates a coupon code before checkout. Checks expiry, usage caps, plan restrictions, per-user limits, and first-time-customer eligibility. The coupon code is always re-validated server-side at checkout; this endpoint exists for pre-checkout UX only. Requires authentication.
| Body Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Coupon code (case-insensitive) |
planSlug | string | No | Intended plan slug. When provided, plan-restricted coupons are validated against it |
curl \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"code": "LAUNCH50", "planSlug": "professional"}' \
-X POST https://api.indreelforge.com/api/coupons/validate
Response — valid coupon
{
"valid": true,
"id": "cpn_01abc",
"discount_type": "percent",
"discount_value": 50,
"influencer_name": null
}
discount_type is either percent or fixed. The coupon code is intentionally not echoed back in the response.
Response — invalid coupon
{
"valid": false,
"error": "Invalid, disabled, expired, or fully redeemed coupon",
"code": "INVALID"
}
code value | Reason |
|---|---|
INVALID | Code not found, disabled, expired, or max uses reached |
PLAN_MISMATCH | Coupon is restricted to specific plans and the provided planSlug is not among them |
PER_USER_LIMIT | The authenticated user has already redeemed this coupon the maximum number of times |
NOT_FIRST_TIME | Coupon is for first-time customers only and the user has a prior completed payment |