> ## 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.

# Checkout and download API

> Reference for the storefront checkout flow, product download with license-key-encrypted ZIP delivery, and download history.

The storefront API handles one-time product purchases through Stripe Checkout and delivers the purchased product as a password-protected ZIP file. These endpoints power the main Shipfastai storefront and are separate from the subscription billing endpoints used inside each product tier.

<Note>
  Checkout and download endpoints use Supabase authentication (cookie-based). Make sure you are logged in through the Shipfastai frontend before calling these endpoints.
</Note>

***

## GET /api/checkout

Start a one-time purchase flow for a product tier. This endpoint redirects the browser to a Stripe-hosted checkout page. If the user is not authenticated, they are redirected to the login page first. If the user already owns the requested tier, they are redirected to the dashboard.

**Query parameters:**

<ParamField query="tier" type="string" required>
  The product tier to purchase. Must be one of `basic`, `pro`, or `enterprise`.
</ParamField>

```bash theme={null}
curl --request GET \
  --url "https://your-app.com/api/checkout?tier=pro" \
  --cookie "session=..."
```

**Behavior:**

| Condition                     | Result                                                   |
| ----------------------------- | -------------------------------------------------------- |
| Valid tier, authenticated     | Redirects to Stripe Checkout page                        |
| Valid tier, not authenticated | Redirects to `/login?redirect=/api/checkout?tier={tier}` |
| User already owns the tier    | Redirects to `/dashboard`                                |
| Invalid or missing tier       | Returns `400` with `{"error": "Invalid tier specified"}` |

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

### Available tiers

| Tier         | Price | Includes                                                                             |
| ------------ | ----- | ------------------------------------------------------------------------------------ |
| `basic`      | \$199 | FastAPI + Next.js boilerplate, JWT auth, OAuth, Stripe subscriptions, Docker, CI/CD  |
| `pro`        | \$299 | Everything in Basic plus RAG pipeline, streaming LLM chat, admin dashboard, API keys |
| `enterprise` | \$499 | Everything in Pro plus fine-tuning scripts, multi-tenancy, usage-based billing       |

<Note>
  Promotion codes are supported. Stripe Checkout displays a promotion code field automatically.
</Note>

***

## GET /api/download

Download the purchased product as an AES-256 encrypted ZIP file. The ZIP password is the license key issued at purchase time. The license key is also returned in the `X-License-Key` response header.

**Query parameters:**

<ParamField query="purchase_id" type="string">
  The UUID of a specific purchase to download. If omitted, the most recent completed purchase is used.
</ParamField>

```bash theme={null}
curl --request GET \
  --url "https://your-app.com/api/download?purchase_id=abc123" \
  --cookie "session=..." \
  --output product.zip
```

**Response headers:**

| Header                | Description                                                     |
| --------------------- | --------------------------------------------------------------- |
| `Content-Type`        | `application/zip`                                               |
| `Content-Disposition` | `attachment; filename="Shipfastai-{tier}-{license_prefix}.zip"` |
| `X-License-Key`       | The full license key used as the ZIP password                   |

### Rate limiting

Each user is limited to one download per purchase every **60 seconds**. Requests within the cooldown window return `429`:

```json theme={null}
{
  "error": "Please wait 45 seconds before downloading again."
}
```

### Download limits

Each purchase has a maximum number of downloads (default: **5** for new purchases). Once the limit is reached, the purchase status changes to `expired` and further downloads are blocked.

```json theme={null}
{
  "error": "Download limit reached. Your license has expired. Please purchase a new license to continue downloading.",
  "expired": true
}
```

### Error responses

| Status | Condition                                           |
| ------ | --------------------------------------------------- |
| `401`  | Not authenticated                                   |
| `403`  | No active purchase found, or download limit reached |
| `404`  | Product files not found for the given tier          |
| `429`  | Rate limit — download requested too soon            |
| `500`  | ZIP creation failed or unexpected error             |

***

## GET /api/download/history

Retrieve the download history for the authenticated user. Returns the most recent 50 download log entries, ordered by download time (newest first).

**Query parameters:**

<ParamField query="purchase_id" type="string">
  Filter results to a specific purchase. If omitted, returns download logs across all purchases.
</ParamField>

```bash theme={null}
curl --request GET \
  --url "https://your-app.com/api/download/history?purchase_id=abc123" \
  --cookie "session=..."
```

**Response** — array of download log entries:

<ResponseField name="id" type="string" required>
  UUID of the download log entry.
</ResponseField>

<ResponseField name="purchase_id" type="string" required>
  UUID of the associated purchase.
</ResponseField>

<ResponseField name="user_id" type="string" required>
  UUID of the user who performed the download.
</ResponseField>

<ResponseField name="version" type="string">
  The product version that was downloaded (e.g., `"1.0.0"`).
</ResponseField>

<ResponseField name="ip_address" type="string">
  IP address of the client at the time of download.
</ResponseField>

<ResponseField name="user_agent" type="string">
  User-Agent header of the client at the time of download.
</ResponseField>

<ResponseField name="country" type="string">
  Country code derived from the client IP address, if available.
</ResponseField>

<ResponseField name="downloaded_at" type="string" required>
  ISO 8601 timestamp of when the download occurred.
</ResponseField>

```json theme={null}
[
  {
    "id": "d4e5f6a1-b2c3-4d5e-f6a1-b2c3d4e5f6a1",
    "purchase_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "version": "1.0.0",
    "ip_address": "203.0.113.42",
    "user_agent": "Mozilla/5.0 ...",
    "country": "US",
    "downloaded_at": "2026-04-09T14:30:00Z"
  }
]
```

***

## POST /api/stripe/webhook

Stripe webhook endpoint for the storefront. Handles purchase completion, payment confirmation, and refund events. This endpoint is called directly by Stripe, not by your application.

<Warning>
  Do not call this endpoint from your application. Register it in the Stripe Dashboard as your webhook URL: `https://your-app.com/api/stripe/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 against `STRIPE_WEBHOOK_SECRET`.
</ParamField>

### Handled events

| Event type                   | Effect                                                                                                                                                            |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checkout.session.completed` | Creates a purchase record with a generated license key, 5 max downloads, and 12-month update entitlement (`entitled_until`). Sends a purchase confirmation email. |
| `payment_intent.succeeded`   | Same as `checkout.session.completed`. Falls back to looking up the checkout session if metadata is not on the payment intent directly.                            |
| `charge.refunded`            | Sets the purchase status to `refunded` for the matching payment.                                                                                                  |

**Response:**

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

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

### Local testing with the Stripe CLI

Forward events from Stripe to your local development server:

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

Set the webhook signing secret printed by the CLI as `STRIPE_WEBHOOK_SECRET` in your `.env.local`.
