> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shipfastai.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Billing API: Stripe checkout and subscriptions

> Reference for Stripe-powered billing endpoints: create a checkout session, open the customer portal, query subscription info, list plans, and handle lifecycle webhooks.

The Billing API integrates with Stripe to handle subscription checkout, customer portal access, subscription queries, and lifecycle events. All billing endpoints are mounted under `/api/billing/`. Protected endpoints require a valid Bearer token for the authenticated user.

<Note>
  You must configure `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` in your backend environment variables before these endpoints will work. See your backend `.env` for the required keys.
</Note>

***

## POST /api/billing/create-checkout-session

Create a Stripe Checkout session for a subscription. If the authenticated user does not yet have a Stripe customer record, one is created automatically using their name and email. Returns the Stripe-hosted checkout URL to redirect the user to.

**Headers:**

<ParamField header="Authorization" type="string" required>
  `Bearer <access_token>`
</ParamField>

**Query parameters:**

<ParamField query="price_id" type="string" required>
  The Stripe Price ID for the subscription plan (e.g., `price_1OqXxxxxxYYYYYYYY`). You can find price IDs in the Stripe Dashboard or via the Stripe CLI.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl --request POST \
    --url "http://localhost:8000/api/billing/create-checkout-session?price_id=price_1OqXxxxxxYYYYYYYY" \
    --header "Authorization: Bearer <access_token>"
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "http://localhost:8000/api/billing/create-checkout-session",
      headers={"Authorization": f"Bearer {access_token}"},
      params={"price_id": "price_1OqXxxxxxYYYYYYYY"},
  )
  checkout_url = response.json()["url"]
  # Redirect the user to checkout_url
  ```
</CodeGroup>

**Response:**

<ResponseField name="url" type="string" required>
  The Stripe-hosted checkout URL. Redirect the user's browser to this URL to complete payment.
</ResponseField>

```json theme={null}
{
  "url": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4..."
}
```

After a successful payment, Stripe redirects to `{FRONTEND_URL}/dashboard?success=true`. If the user cancels, Stripe redirects to `{FRONTEND_URL}/pricing?canceled=true`.

***

## POST /api/billing/create-portal-session

Create a Stripe Customer Portal session for the authenticated user. The portal lets the user manage their subscription, update payment methods, view invoices, and cancel. The user must have an existing Stripe customer record (created automatically at checkout).

**Headers:**

<ParamField header="Authorization" type="string" required>
  `Bearer <access_token>`
</ParamField>

No request body is required. The endpoint uses the `stripe_customer_id` stored on the authenticated user.

<CodeGroup>
  ```bash curl theme={null}
  curl --request POST \
    --url http://localhost:8000/api/billing/create-portal-session \
    --header "Authorization: Bearer <access_token>"
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "http://localhost:8000/api/billing/create-portal-session",
      headers={"Authorization": f"Bearer {access_token}"},
  )
  portal_url = response.json()["url"]
  # Redirect the user to portal_url
  ```
</CodeGroup>

**Response:**

<ResponseField name="url" type="string" required>
  The Stripe-hosted billing portal URL. Redirect the user's browser to this URL.
</ResponseField>

```json theme={null}
{
  "url": "https://billing.stripe.com/session/bps_test_a1b2c3d4..."
}
```

After the user leaves the portal, Stripe redirects them to `{FRONTEND_URL}/dashboard`.

Returns `400` with `{"detail": "No billing account found"}` if the user has never completed a checkout session and therefore has no Stripe customer ID.

***

## GET /api/billing/subscription

Return the current subscription status for the authenticated user.

**Headers:**

<ParamField header="Authorization" type="string" required>
  `Bearer <access_token>`
</ParamField>

```bash theme={null}
curl --request GET \
  --url http://localhost:8000/api/billing/subscription \
  --header "Authorization: Bearer <access_token>"
```

**Response:**

<ResponseField name="status" type="string" required>
  Current subscription status (e.g., `free`, `active`, `cancelled`, `past_due`).
</ResponseField>

<ResponseField name="tier" type="string" required>
  Current subscription tier (e.g., `free`, `pro`).
</ResponseField>

<ResponseField name="stripe_customer_id" type="string">
  The Stripe customer ID associated with the user, or `null` if no Stripe record exists.
</ResponseField>

```json theme={null}
{
  "status": "active",
  "tier": "pro",
  "stripe_customer_id": "cus_a1b2c3d4e5f6"
}
```

***

## GET /api/billing/plans

Return the list of available subscription plans with their features. This endpoint does not require authentication.

```bash theme={null}
curl --request GET \
  --url http://localhost:8000/api/billing/plans
```

**Response:**

<ResponseField name="plans" type="object[]" required>
  Array of available subscription plans.

  <Expandable title="plan properties">
    <ResponseField name="name" type="string" required>Display name of the plan.</ResponseField>
    <ResponseField name="tier" type="string" required>Tier identifier (e.g., `free`, `pro`).</ResponseField>
    <ResponseField name="price" type="number">Fixed price for free-tier plans (`0`).</ResponseField>
    <ResponseField name="price_monthly" type="string">Stripe Price ID for monthly billing. Present on paid tiers.</ResponseField>
    <ResponseField name="price_yearly" type="string">Stripe Price ID for yearly billing. Present on paid tiers.</ResponseField>
    <ResponseField name="features" type="string[]" required>List of features included in the plan.</ResponseField>
  </Expandable>
</ResponseField>

```json theme={null}
{
  "plans": [
    {
      "name": "Free",
      "tier": "free",
      "price": 0,
      "features": [
        "Basic authentication",
        "User management",
        "Community support"
      ]
    },
    {
      "name": "Pro",
      "tier": "pro",
      "price_monthly": "price_1OqXxxxxxMONTHLY",
      "price_yearly": "price_1OqXxxxxxYEARLY",
      "features": [
        "Everything in Free",
        "AI Chat (OpenAI, Anthropic, Gemini)",
        "RAG Pipeline",
        "API Key access",
        "Priority support"
      ]
    }
  ]
}
```

***

## POST /api/billing/webhook

Stripe webhook endpoint. This endpoint is called directly by Stripe — not by your application. Stripe sends signed events here to notify your backend of subscription changes.

<Warning>
  Do not call this endpoint from your application. Register it in the Stripe Dashboard (or via the Stripe CLI) as your webhook URL: `https://<your-backend-url>/api/billing/webhook`.
</Warning>

**Headers required by Stripe:**

<ParamField header="stripe-signature" type="string" required>
  The `Stripe-Signature` header added automatically by Stripe. The backend verifies this signature against your `STRIPE_WEBHOOK_SECRET` to confirm the event is authentic.
</ParamField>

### Handled events

| Event type                      | Effect                                                                                                                                             |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checkout.session.completed`    | Sets `subscription_status` to `active` and determines `subscription_tier` from the checkout line items using the configured price-to-tier mapping. |
| `customer.subscription.updated` | Updates `subscription_status` on the matching user to the new Stripe subscription status.                                                          |
| `customer.subscription.deleted` | Sets `subscription_status` to `cancelled` and `subscription_tier` to `free` for the customer.                                                      |
| `invoice.payment_failed`        | Sets `subscription_status` to `past_due` for the customer.                                                                                         |

**Response** (on successful receipt):

```json theme={null}
{
  "received": true
}
```

Returns `400` for an invalid payload or a failed signature verification.

### Local testing with the Stripe CLI

To test webhooks locally, forward events from Stripe to your running dev server:

```bash theme={null}
stripe listen --forward-to http://localhost:8000/api/billing/webhook
```

The CLI prints a webhook signing secret that you should set as `STRIPE_WEBHOOK_SECRET` in your local `.env` while testing.
