Skip to content

Billing

Subscription management via Stripe. Handles plan upgrades, downgrades, and payment management.

Base path: /api/billing

Plan Tiers

PlanPriceKey Limits
Free$0/mo1 repo, 3 scans/day, 0 cloud accounts
Pro$29/mo5 repos, unlimited scans, 1 cloud account
Team$79/moUnlimited repos, unlimited scans, unlimited cloud accounts
EnterpriseCustomSSO/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

FieldTypeRequiredDescription
priceIdstringYesStripe price ID for the target plan

Response

json
{
  "url": "https://checkout.stripe.com/c/pay/cs_live_..."
}

Errors

StatusErrorDescription
400Invalid price IDThe priceId does not match a known plan
500Failed to create checkout sessionStripe API error

Example

bash
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

json
{
  "url": "https://billing.stripe.com/p/session/..."
}

Errors

StatusErrorDescription
500Failed to create portal sessionStripe API error or no Stripe customer on file

Example

bash
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

FieldTypeRequiredDescription
sessionIdstringYesStripe Checkout session ID (from redirect URL)

Response

json
{
  "success": true,
  "plan": "pro"
}

Errors

StatusErrorDescription
400Session ID is requiredMissing sessionId in body
400Invalid sessionSession not found or belongs to a different org
400Session not completedCheckout was not completed or has no subscription
500Failed to verify sessionStripe API error

Example

bash
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

EventAction
checkout.session.completedSyncs new subscription to the org
customer.subscription.updatedSyncs plan changes (upgrade/downgrade)
customer.subscription.deletedResets org to Free plan
invoice.payment_failedSets subscription status to past_due

Response

Always returns 200 to prevent Stripe retries:

json
{
  "received": true
}

Errors

StatusErrorDescription
400Invalid signatureStripe 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.