# Billing & Subscription — Finradar API > Version: 3.61.0 | Generated: 2026-06-20 | Content Hash: d88e0637 > Fetch this file at: https://uat.finradarapi.com/llms/billing-and-subscription.txt ## Authentication All endpoints require an API key. Pass it via query parameter `?apiKey=YOUR_KEY` or header `X-API-Key: YOUR_KEY`. WebSocket endpoints accept the key in the `token` auth payload or query parameter. --- ## Billing & Subscription Manage subscriptions, view plans, purchase API quota, and upgrade plans via Stripe. ### GET /api/v1/billing/subscription Get the authenticated user's subscription info and request-based API usage. EXEMPT (`cost: 0`). Returns plan name, request-based usage vs limit, subscription status, renewal date. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `status` (string): ApiResponse envelope status — `success` on 200, `error` on 4xx/5xx. - `request_id` (string (nullable)): Per-request correlation ID. - `timestamp` (string): ISO-8601 UTC timestamp. - `data` (object): Subscription state payload. - `data.reach_limit_api` (integer): Number of REQUESTS (NOT tokens) consumed by the user this billing cycle. Increments by 1 on every billed call regardless of cost tier — distinct from the token ledger which weights by cost tier (1/5/10/25). On accounts with no `user_plan` row (legacy), defaults to 0. - `data.total_limit_api` (integer): Total REQUESTS allowed this billing cycle (e.g. 100 for free, higher for paid plans). Increased by [POST /api/v1/billing/quota](/docs/account/billing-and-subscription/post-billing-quota) when the user buys additional quota with their USD `credit_balance` wallet at 100 requests per $1. NOT in tokens — this is the legacy per-request meter. - `data.plan_name` (string): Plan tier — `free` (default for new signups), `weekly`, `monthly`, `pro`, `yearly`. Drives UI tier badges. Set on Stripe `checkout.session.completed` via `_handle_checkout_completed` after a successful upgrade. - `data.status` (string): Stripe subscription status — `active` (paying), `past_due` (failed payment, in 7-day grace), `canceled`, `trialing`, `unpaid`. For free-tier accounts always `active`. Use to dispatch UI banners ('Update payment method' on `past_due`). - `data.current_period_end` (string (nullable)): ISO-8601 UTC timestamp the current billing period ends (= next renewal). Null for free-tier accounts (no renewal cycle). For paid accounts coincides with the Stripe subscription's `current_period_end`. **Since:** v1.0.0 **Utility:** Authenticated subscription-state probe. EXEMPT (`cost: 0`). Returns the legacy request-based quota model (`reach_limit_api` / `total_limit_api`, in REQUESTS not tokens) plus plan name and Stripe subscription status. NOTE the unit distinction: this is the LEGACY per-request quota model used pre-Phase-56; the canonical token-based model is at [GET /api/v1/account/balance](/docs/account/token-pricing/get-account-balance) (in TOKENS, with cost-tier weighting). Both meters run side-by-side: a billed call decrements both `reach_limit_api` (+1 request) and the token ledger (-N tokens depending on cost tier). For plan upgrades use [POST /api/v1/payment/create-checkout-session](/docs/account/billing-and-subscription/post-payment-create-checkout-session); to buy additional request quota with the USD wallet use [POST /api/v1/billing/quota](/docs/account/billing-and-subscription/post-billing-quota). **Use case:** Dashboard 'Subscription' card: render plan name + a request-quota progress bar (`reach_limit_api / total_limit_api`) + renewal-date chip. For the token-quota progress bar (the canonical Phase 56+ surface), use /api/v1/account/balance instead. **Sample response:** ```json { "status": "success", "request_id": "req_3OqK2jK9L8pQ4xZ3", "timestamp": "2026-05-02T15:51:00.000Z", "data": { "reach_limit_api": 12, "total_limit_api": 100, "plan_name": "free", "status": "active", "current_period_end": null } } ``` ### GET /api/v1/plans/ List all active subscription plans with pricing, features, and Stripe price IDs. PUBLIC — NO authentication required. EXEMPT (`cost: 0`). **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `status` (string): ApiResponse envelope status — `success` on 200. - `request_id` (string (nullable)): Per-request correlation ID. - `timestamp` (string): ISO-8601 UTC timestamp. - `data` (array): Array of active plans, sorted by `price ASC` (cheapest first). Empty array only if all plans are inactive (operational issue — plans are seeded at deployment). - `data[].id` (integer): Plan database ID. Pass to [POST /api/v1/payment/create-checkout-session](/docs/account/billing-and-subscription/post-payment-create-checkout-session) via `?plan_id=N` to initiate the Stripe-hosted upgrade flow. - `data[].name` (string): Plan display name (e.g. `Free`, `Paid`, `Pro`). Casing follows brand guidelines — use verbatim in pricing-card UIs rather than lowercasing client-side. - `data[].stripe_price_id` (string (nullable)): Stripe Price object ID in `price_XXXXX` format. Null for the Free plan (no Stripe price — checkout for Free is rejected at create-checkout-session). Used server-side to construct the Stripe Checkout `line_items[].price`. - `data[].price` (integer): Plan price in CENTS (integer). E.g. `2900` = $29.00/month. 0 for the Free plan. Divide by 100 for USD display: `(price / 100).toFixed(2)`. - `data[].currency` (string): ISO-4217 currency code. Defaults to `USD` server-side. Use to render currency symbol ($ / € / £). - `data[].interval` (string): Billing interval — `month` (default), `week`, `year`. Drives '/month' vs '/year' suffix on pricing cards. - `data[].features` (array): JSON array of feature-bullet strings for the pricing-card UI (`features: ["2,000 tokens/month", "Community support", ...]`). Empty array on plans that haven't been content-populated yet (operational gap). - `data[].is_active` (boolean): Always true in the response (the query filter is `is_active=True`). Inactive plans are hidden from the public catalog by default. Inactive plans still service existing subscribers — they're just not offered to new signups. **Since:** v1.0.0 **Utility:** Public catalogue of active subscription plans — drives the marketing pricing page and the in-app upgrade modal. PUBLIC (no auth required) and EXEMPT (`cost: 0`). Filters to `is_active=true` and orders by `price ASC`. Each row carries `stripe_price_id` (the Stripe Price object ID) which the frontend passes to [POST /api/v1/payment/create-checkout-session](/docs/account/billing-and-subscription/post-payment-create-checkout-session) via `?plan_id=N` to initiate a Stripe-hosted upgrade flow. Prices are in CENTS (integer) — divide by 100 for USD display. Currency defaults to `USD`. The `features` JSON column is plan-specific (e.g. an array of feature bullets for the pricing card UI). **Use case:** Marketing pricing page renders this once on page load → maps each row to a pricing card → 'Upgrade' button on each card POSTs to /api/v1/payment/create-checkout-session with the plan's `id`. **Sample response:** ```json { "status": "success", "request_id": "req_3OqK2jK9L8pQ4xZ4", "timestamp": "2026-05-02T15:51:00.000Z", "data": [ { "id": 1, "name": "Free", "stripe_price_id": null, "price": 0, "currency": "USD", "interval": "month", "features": [ "2,000 tokens / month", "Community support", "Basic API access" ], "is_active": true }, { "id": 2, "name": "Paid", "stripe_price_id": "price_1QPzWaKG43KrBnru0AbCdEfG", "price": 2900, "currency": "USD", "interval": "month", "features": [ "200,000 tokens / month", "Priority email support", "Full API access", "Sniper alerts", "Webhooks" ], "is_active": true } ] } ``` ### POST /api/v1/billing/quota Purchase additional REQUEST quota by debiting the USD `credit_balance` wallet at 100 requests per $1. EXEMPT (`cost: 0`). Logs a SPEND `Transaction`. Returns 400 with `INSUFFICIENT_BALANCE` when the wallet does not cover the requested amount. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `status` (string): ApiResponse envelope status — `success` on 200, `error` on 4xx/5xx. - `request_id` (string (nullable)): Per-request correlation ID. - `timestamp` (string): ISO-8601 UTC timestamp. - `data` (object): Post-purchase result payload. - `data.message` (string): Human-readable confirmation (e.g. `Successfully purchased 1000 additional API requests.`). Use for the UI success toast. - `data.new_total_limit` (integer): Post-purchase value of `user_plans.total_limit_api` (in REQUESTS, not tokens). Mirrors the legacy per-request quota; the new bonus quota is added on top of the plan's base allocation. Use to update the 'Total requests' progress-bar denominator client-side without a separate /api/v1/billing/subscription call. - `data.new_balance` (number): Post-purchase USD wallet balance (`users.credit_balance`). NOT in cents — server returns `float(credit_balance)`. Use to update the 'Wallet balance' chip client-side. **Since:** v1.0.0 **Utility:** Customer-initiated request-quota top-up — debits the USD `credit_balance` wallet (in DOLLARS), adds REQUESTS (NOT tokens) to `user_plans.total_limit_api` at the fixed exchange rate of 100 requests per $1. EXEMPT (`cost: 0`). Atomic single-transaction: balance debit + quota credit + SPEND `Transaction` row are committed together. Returns 400 with `INSUFFICIENT_BALANCE` if `credit_balance < amount_dollars`. NB: this affects the LEGACY per-request meter (see [GET /api/v1/billing/subscription](/docs/account/billing-and-subscription/get-billing-subscription)) — NOT the canonical Phase 56 token meter (which has its own monthly-cycle refill and does NOT support manual top-up). To top up the USD wallet first, use [POST /api/v1/payments/create-intent](/docs/account/payments-module/post-payments-create-intent). **Use case:** Dashboard 'Buy more requests' modal: user enters $10 → POST here with `amount_dollars: 10` → backend debits wallet by $10, adds 1,000 requests to `total_limit_api`, returns success → UI updates the request-quota progress bar. **Parameters:** - `amount_dollars` (body, required): USD amount to spend (NOT cents). Positive number; integer or float (e.g. 10, 20, 12.50). Validated server-side: rejected with 400 `BAD_REQUEST` on missing/non-numeric/zero/negative; rejected with 400 `INSUFFICIENT_BALANCE` if `users.credit_balance < amount_dollars`. Each $1 buys 100 REQUESTS. **Sample response:** ```json { "status": "success", "request_id": "req_3OqK2jK9L8pQ4xZ5", "timestamp": "2026-05-02T15:51:00.000Z", "data": { "message": "Successfully purchased 1000 additional API requests.", "new_total_limit": 1100, "new_balance": 2.5 } } ``` ### POST /api/v1/payment/create-checkout-session Create a Stripe-hosted Checkout session for a subscription upgrade. EXEMPT (`cost: 0`). Returns the Stripe URL — frontend MUST redirect via `window.location.href`. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `status` (string): ApiResponse envelope status — `success` on 200, `error` on 4xx/5xx. - `request_id` (string (nullable)): Per-request correlation ID. - `timestamp` (string): ISO-8601 UTC timestamp. - `data` (object): Checkout-session payload. - `data.url` (string): Stripe Checkout-hosted page URL in `https://checkout.stripe.com/c/pay/cs_XXXXXXX#YYYYY` format. REDIRECT the browser via `window.location.href = response.data.url` — do NOT iframe-embed (Stripe blocks framing of checkout pages). On success, Stripe redirects back to `${FRONTEND_URL}/account/subscription?session_id={CHECKOUT_SESSION_ID}`; on cancel, to `${FRONTEND_URL}/account/subscription?cancelled=true`. The Checkout session embeds metadata (`user_uuid`, `plan_id`, `plan_name`) so the `checkout.session.completed` webhook handler can activate the right subscription on the right user. **Since:** v1.0.0 **Utility:** Server-side wrapper for `stripe.checkout.Session.create` (mode=subscription) — returns a Stripe-hosted checkout URL where the user enters card details and confirms the upgrade. EXEMPT (`cost: 0`). The frontend MUST redirect (not iframe-embed). On successful payment, Stripe webhooks `checkout.session.completed` → `_handle_checkout_completed` activates the subscription on `user_plans` (sets `plan`, `stripe_subscription_id`, `current_period_end`). On cancellation / failure the user is redirected back to `/account/subscription?cancelled=true`. Free plan (no `stripe_price_id`) cannot be checked out — returns 400 `BAD_REQUEST` if `plan.stripe_price_id` is null. **Use case:** Pricing-page 'Upgrade to Paid' button: get plan id from /api/v1/plans/ → POST here with `?plan_id=2` → redirect browser to the returned URL → Stripe-hosted checkout → on success Stripe redirects back to `/account/subscription?session_id=...` → SPA polls /api/v1/billing/subscription for the activated state. **Parameters:** - `plan_id` (query, required): Plan database ID (NOT name) — the integer `id` from [GET /api/v1/plans/](/docs/account/billing-and-subscription/get-plans). Returns 400 `BAD_REQUEST` if missing or non-numeric, 404 `NOT_FOUND` if the plan does not exist or is inactive, 400 `BAD_REQUEST` if the plan has no `stripe_price_id` (Free plan). **Sample response:** ```json { "status": "success", "request_id": "req_3OqK2jK9L8pQ4xZ6", "timestamp": "2026-05-02T15:51:00.000Z", "data": { "url": "https://checkout.stripe.com/c/pay/cs_test_b1Yq8nL7aP2kT6sB1mDfHj9vC3xRtKzE5wQ8jN4uM2pX7yI6oA9hT0gU1vF#fidkdWxOYHwnPyd1blpxYHZxWjA0SF9MUFc2dVZxYG1QSXNqcGhJQDR9aGhNNXxPSk09Q1xLTHN1MnxwfFBIYDU8YzVtXX1HSUtAaD1KaHM3RkB" } } ``` ### GET /api/v1/billing/history Get billing transaction history for the authenticated user. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `transactions` (array): Array of wallet transaction rows, sorted by `created_at DESC` (most recent first). Empty array on accounts with no billing history (free tier accounts that have never upgraded). Includes both Stripe-tied charges and admin-grant credits. - `transactions[].id` (string): Internal wallet-transaction ID in `wtx_XXXXXX` format (e.g. `wtx_abc123`). Stable across re-runs. Use as deduplication key when caching transaction history client-side. - `transactions[].created_at` (string): ISO-8601 UTC timestamp the transaction was recorded. For Stripe-tied transactions this is the Stripe webhook arrival time (typically within seconds of the actual charge). Use for chronological display. - `transactions[].type` (string): Transaction type — one of `subscription_refill` (monthly Stripe charge → 200K-token grant), `one_time_grant` (free-tier signup grant or promotional credit), `manual_adjustment` (admin-applied credit/debit), `refund` (Stripe refund). Use to dispatch UI badges and reconciliation logic. - `transactions[].amount_usd` (number (nullable)): USD amount charged or refunded via Stripe (positive for charges, negative for refunds). Null for non-Stripe events like `one_time_grant` and `manual_adjustment` (these are tokens-only, no money movement). - `transactions[].tokens_credited` (integer (nullable)): Tokens added to (or subtracted from) the balance as a result of this transaction. Null when the transaction was admin-recorded but did not move tokens (rare). For paid-plan refills typically 200000; for free-plan signup grants 2000. - `transactions[].stripe_invoice_id` (string (nullable)): Linked Stripe invoice ID in `in_XXXXX` format. Present for `subscription_refill` and `refund` transactions; null for `one_time_grant` and `manual_adjustment`. Pass to Stripe Customer Portal for receipt download. - `transactions[].description` (string): Human-readable description for UI display (e.g. `"Monthly paid plan refill"`, `"Free-tier signup grant"`, `"Admin credit — support ticket #4521"`). Casing follows brand guidelines. **Since:** v1.2.1 **Utility:** Wallet transaction history for the authenticated user — every credit (subscription refill, one-time grant, manual adjustment, refund) related to billing events, ordered by `created_at DESC` (most recent first). NOT to be confused with the per-call token-debit ledger (that's `/api/v1/account/transactions`); this endpoint is specifically for billing-related credit events. Returns Stripe-shaped records (`amount_usd`, `stripe_invoice_id`) when the transaction was tied to a charge. The right endpoint for the customer-dashboard 'Billing History' table and for support workflows ('was the customer's $29 charge applied?'). EXEMPT (`cost: 0`) — account self-serve. **Use case:** Displaying a transaction history table in the user's wallet page. **Parameters:** - `limit` (query, optional, default: 20): Maximum transactions to return per page (capped at 100 server-side). Most callers want 20-50 for typical 'last N transactions' display; for full-history exports use cursor pagination via `offset`. - `offset` (query, optional, default: 0): Zero-based pagination offset (NUMBER of transactions to skip from the top). Increment by `limit` to walk pages: `offset=0` page 1, `offset=20` page 2 (when `limit=20`). **Sample response:** ```json { "transactions": [ { "id": "wtx_abc123", "created_at": "2026-04-26T08:39:00.000Z", "type": "subscription_refill", "amount_usd": 29, "tokens_credited": 200000, "stripe_invoice_id": "in_1Pz...", "description": "Monthly paid plan refill" } ] } ``` ### GET /api/v1/billing/invoices Fetch Stripe invoices for the authenticated user (PDF download links + amounts + status). EXEMPT (`cost: 0`). Returns an empty array for users without a `stripe_customer_id` (free-tier users who never upgraded). **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `status` (string): ApiResponse envelope status — `success` on 200. - `request_id` (string (nullable)): Per-request correlation ID. - `timestamp` (string): ISO-8601 UTC timestamp. - `data` (array): Array of Stripe invoice rows, sorted by Stripe (newest first by default). Empty array on free-tier users without `stripe_customer_id` AND on Stripe API errors (server logs the error and returns empty rather than 5xx — graceful degradation). Cap at the requested `limit` (default 10, max 100 enforced by Stripe). - `data[].id` (string): Stripe invoice ID in `in_XXXXXX` format. Stable for the lifetime of the invoice. Use as deduplication key when caching client-side. - `data[].amount_due` (integer): Amount still owed on the invoice in CENTS (Stripe-native unit). 0 for paid invoices; positive for `open` / `past_due` invoices. Divide by 100 for USD display. - `data[].amount_paid` (integer): Amount already paid on the invoice in CENTS. For monthly subscriptions on the Paid plan: 2900 (= $29.00). Sum across all paid invoices = lifetime customer revenue. - `data[].status` (string): Stripe invoice status — `paid` (most common), `open` (awaiting payment), `void` (admin-cancelled), `uncollectible` (Stripe gave up after retries), `draft` (rare, mostly admin-created). Drives UI badges. - `data[].invoice_pdf` (string (nullable)): Short-lived Stripe-hosted PDF URL for the invoice receipt. Passes through Stripe's auth — no server-side auth needed; just `Download`. Null for invoices in `draft` or `void` status. - `data[].created` (integer): Unix epoch timestamp (seconds) the invoice was created in Stripe. Multiply by 1000 for JS Date constructor: `new Date(invoice.created * 1000)`. Use for chronological display. - `data[].currency` (string): ISO-4217 currency code (lowercase per Stripe convention — e.g. `usd`, `eur`). Use to render currency symbol. **Since:** v1.0.0 **Utility:** Live Stripe invoice list for the authenticated user — calls `stripe.Invoice.list(customer=...)` server-side and returns the relevant fields. EXEMPT (`cost: 0`). Returns empty array (NOT 400) for users without `stripe_customer_id` so the dashboard 'Invoices' table can render the empty state without special-casing free-tier users. Amounts are in CENTS (Stripe's native unit) — divide by 100 for USD display. The `invoice_pdf` URL is a short-lived Stripe-hosted PDF download — passes through Stripe's auth on click, no need to authenticate the request server-side. **Use case:** Dashboard 'Wallet → Invoices' table: GET this on page load → render rows of `created` (date) / `amount_paid` / `status` / 'Download PDF' link. For an inline transaction history (charges + grants + refunds) use [GET /api/v1/billing/history](/docs/account/billing-and-subscription/get-billing-history); for the Stripe Customer Portal where the user can download invoices and update billing info use [POST /api/v1/account/billing-portal](/docs/account/token-pricing/post-account-billing-portal). **Parameters:** - `limit` (query, optional, default: 10): Max invoices to return per page. Default 10. Stripe enforces an upper bound of 100 server-side — values above are silently truncated by Stripe. For a user's full invoice history, paginate via Stripe's `starting_after` cursor (NOT exposed by this endpoint — use POST /api/v1/account/billing-portal for full history). **Sample response:** ```json { "status": "success", "request_id": "req_3OqK2jK9L8pQ4xZ7", "timestamp": "2026-05-02T15:51:00.000Z", "data": [ { "id": "in_1QPzWaKG43KrBnru0xYzAbCd", "amount_due": 0, "amount_paid": 2900, "status": "paid", "invoice_pdf": "https://pay.stripe.com/invoice/acct_1QPzWaKG43KrBnru/test_YWNjdF8xUVB6V2FLRzQzS3JCbnJ1LF9PcUsyaks5TDhwUTR4WjE/pdf?s=ap", "created": 1745625540, "currency": "usd" }, { "id": "in_1QPzVXKG43KrBnruZyXwCdEf", "amount_due": 0, "amount_paid": 2900, "status": "paid", "invoice_pdf": "https://pay.stripe.com/invoice/acct_1QPzWaKG43KrBnru/test_YWNjdF8xUVB6V2FLRzQzS3JCbnJ1LF9PcUsyaks5TDhwUTR4WjI/pdf?s=ap", "created": 1742947140, "currency": "usd" } ] } ```