Billing
Subscription management via Stripe. Handles plan upgrades, downgrades, and payment management.
Base path: /api/billing
Plan Tiers
| Plan | Price | Key Limits |
|---|---|---|
| Free | $0/mo | 1 repo, 3 scans/day, 0 cloud accounts |
| Pro | $29/mo | 5 repos, unlimited scans, 1 cloud account |
| Team | $79/mo | Unlimited repos, unlimited scans, unlimited cloud accounts |
| Enterprise | Custom | SSO/SCIM, custom RBAC, dedicated support |
Create Checkout Session
POST /api/billing/checkout-session
Authentication: Bearer token required | Role: Organization owner
Create a Stripe Checkout session to start a subscription or change plans. Returns a URL to redirect the user to Stripe's hosted checkout page.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
priceId | string | Yes | Stripe price ID for the target plan |
Response
{
"url": "https://checkout.stripe.com/c/pay/cs_live_..."
}Errors
| Status | Error | Description |
|---|---|---|
| 400 | Invalid price ID | The priceId does not match a known plan |
| 500 | Failed to create checkout session | Stripe API error |
Example
curl -X POST https://api.controlinfra.com/api/billing/checkout-session \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-H "x-org-id: ORG_ID" \
-d '{"priceId": "price_1abc..."}'Create Portal Session
POST /api/billing/portal-session
Authentication: Bearer token required | Role: Organization owner
Create a Stripe Customer Portal session. The portal allows users to manage their subscription, update payment methods, view invoices, and cancel.
Request Body
No body required. The organization's Stripe customer ID is resolved automatically.
Response
{
"url": "https://billing.stripe.com/p/session/..."
}Errors
| Status | Error | Description |
|---|---|---|
| 500 | Failed to create portal session | Stripe API error or no Stripe customer on file |
Example
curl -X POST https://api.controlinfra.com/api/billing/portal-session \
-H "Authorization: Bearer TOKEN" \
-H "x-org-id: ORG_ID"Verify Session
POST /api/billing/verify-session
Authentication: Bearer token required | Role: Organization owner
Verify a completed Stripe Checkout session after the user is redirected back. Syncs the subscription status and plan tier to the organization.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | Yes | Stripe Checkout session ID (from redirect URL) |
Response
{
"success": true,
"plan": "pro"
}Errors
| Status | Error | Description |
|---|---|---|
| 400 | Session ID is required | Missing sessionId in body |
| 400 | Invalid session | Session not found or belongs to a different org |
| 400 | Session not completed | Checkout was not completed or has no subscription |
| 500 | Failed to verify session | Stripe API error |
Example
curl -X POST https://api.controlinfra.com/api/billing/verify-session \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-H "x-org-id: ORG_ID" \
-d '{"sessionId": "cs_live_..."}'Webhook
POST /api/billing/webhook
Authentication: None (Stripe signature verification via stripe-signature header)
Handles Stripe webhook events. This endpoint is mounted before the global JSON body parser in the application and receives the raw request body for signature verification.
Note: This endpoint is not called by clients. It is configured in the Stripe Dashboard as the webhook URL.
Handled Events
| Event | Action |
|---|---|
checkout.session.completed | Syncs new subscription to the org |
customer.subscription.updated | Syncs plan changes (upgrade/downgrade) |
customer.subscription.deleted | Resets org to Free plan |
invoice.payment_failed | Sets subscription status to past_due |
Response
Always returns 200 to prevent Stripe retries:
{
"received": true
}Errors
| Status | Error | Description |
|---|---|---|
| 400 | Invalid signature | Stripe webhook signature verification failed |
Upgrade/Downgrade Behavior
On upgrade: The new plan takes effect immediately. Stripe prorates the charge for the remainder of the billing cycle.
On downgrade: The plan change takes effect at the end of the current billing period. Usage limits are not enforced until the new plan is active.
On cancellation: The subscription remains active until the end of the billing period, then the org reverts to the Free plan.
On payment failure: The subscription status is set to past_due. Stripe will retry the payment according to its retry schedule. If all retries fail, the subscription is eventually deleted and the org reverts to Free.