Billing API
Stripe-powered subscription billing and usage-based overage.
Billing API
QANATIX uses Stripe for subscription billing. Free users never interact with Stripe. Paid plans (Pro/Scale) include a base subscription plus metered overage for searches and ingestions.
Billing endpoints are only available in cloud mode (DEPLOYMENT_MODE=cloud). Self-hosted deployments return 404 for all billing endpoints.
Plans
| Plan | Price | Searches | Search overage | Ingestions | Ingestion overage | Entities |
|---|---|---|---|---|---|---|
| Free | $0 | 1,000/mo | Blocked | 500/mo | Blocked | 1,000 |
| Pro | $199/mo | 50,000/mo | $1.50/1K | 10,000/mo | $6.00/1K | 100,000 |
| Scale | $399/mo | 500,000/mo | $1.00/1K | 100,000/mo | $4.00/1K | Unlimited |
| Enterprise | Custom | Unlimited | Custom | Unlimited | Custom | Unlimited |
Free plan has hard caps — requests return HTTP 402 when limits are reached. Paid plans allow overages, billed at end of month via Stripe.
How billing works
- Sign up — user gets a free plan with no Stripe customer
- Upgrade — user clicks "Upgrade to Pro" in the dashboard, redirected to Stripe Checkout
- Payment — Stripe processes payment, webhook activates the plan
- Usage tracking — every search/ingestion increments a Redis counter (existing infrastructure)
- Meter events — every 5 minutes, counters are flushed to Stripe Billing Meters
- Invoicing — Stripe generates invoices with base fee + metered overage (tiered pricing handles included quotas)
- Self-service — user manages billing via Stripe Customer Portal (invoices, payment method, cancel)
Checkout
POST /portal/billing/checkout
Create a Stripe Checkout session for plan upgrade. Redirects the user to Stripe's hosted payment page.
Requires: JWT authentication (portal)
curl -X POST https://api.qanatix.com/api/v1/portal/billing/checkout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"plan": "pro",
"success_url": "https://qanatix.com/dashboard/settings?upgraded=1",
"cancel_url": "https://qanatix.com/dashboard/settings"
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
plan | string | Yes | Target plan: "pro" or "scale" |
success_url | string | Yes | Redirect URL after successful payment |
cancel_url | string | Yes | Redirect URL if user cancels |
Response (200)
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_..."
}Redirect the user to checkout_url. After payment, Stripe redirects to success_url.
Customer Portal
POST /portal/billing/portal
Create a Stripe Customer Portal session for self-service billing management.
Requires: JWT authentication + existing Stripe customer (must be on a paid plan)
curl -X POST https://api.qanatix.com/api/v1/portal/billing/portal \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"return_url": "https://qanatix.com/dashboard/settings"
}'Response (200)
{
"portal_url": "https://billing.stripe.com/p/session/..."
}Open portal_url in a new tab. The portal lets users:
- View invoice history
- Update payment method
- Cancel subscription
- Change plan
Billing Status
GET /portal/billing/status
Get current billing status for the authenticated user's tenant.
curl https://api.qanatix.com/api/v1/portal/billing/status \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"plan": "pro",
"has_subscription": true,
"billing_period_end": "2026-04-08T00:00:00Z",
"is_cloud": true
}| Field | Description |
|---|---|
plan | Current plan: free, pro, scale, enterprise |
has_subscription | Whether a Stripe subscription exists |
billing_period_end | End of current billing period (null for free) |
is_cloud | Whether this is a cloud deployment (billing available) |
Webhook
POST /billing/stripe/webhook
Stripe webhook receiver. Signature-verified, no JWT/API key auth required.
This endpoint is called by Stripe to notify us of subscription lifecycle events. It is not meant to be called by users.
Events handled
| Event | Action |
|---|---|
checkout.session.completed | Activate plan, save subscription ID |
customer.subscription.updated | Sync plan and billing period |
customer.subscription.deleted | Revert to free plan |
invoice.paid | Audit log |
invoice.payment_failed | Warning log (Stripe handles retry) |
Usage metering
Usage is reported to Stripe Billing Meters automatically:
- Two meters:
qanatix_search(searches) andqanatix_ingestion(ingestions) - Frequency: every 5 minutes via SAQ cron job
- Scope: only paid tenants with a Stripe customer ID
- Tiered pricing: Stripe handles "first N included free" via tiered price configuration
- Source of truth: Postgres
usage_recordstable (Stripe meter events are fire-and-forget)
Self-hosted mode
When DEPLOYMENT_MODE=self_hosted:
- All
/billing/*endpoints return404 - All
/portal/billing/*endpoints return404 - No Stripe API calls are ever made
- Usage tracking to Redis + Postgres continues normally
- Operators see usage via
GET /portal/usageorGET /usage
Error responses
| Status | Meaning |
|---|---|
400 | Invalid request (bad plan, missing customer, invalid signature) |
404 | Billing not available (self-hosted mode) |
422 | Validation error (invalid plan name) |