# Billing

### The account’s subscription, from Koo’s mirror of Stripe

`GET /accounts/{accountId}/subscription`

**Required role:** owner

Owner-only: it exposes invoices and the card’s brand/last4. Returns 404 while the account is on Free. The `invoices` list is empty until the billing webhook mirrors them.

**Parameters**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `SubscriptionDto`
- `404` · `ErrorEnvelopeDto` — no subscription — the account is on Free

Attributes of `SubscriptionDto`:

- `id` · `string` · read-only
- `accountId` · `string` · read-only
- `status` · `"active" | "trialing" | "past_due" | "canceled" | "paused" | "unpaid" | "incomplete" | "incomplete_expired"` · read-only
- `cadence` · `"monthly" | "annual"` · required
- `currentPeriodEnd` · `string <date-time>` · read-only
- `cancelAtPeriodEnd` · `boolean` · read-only
- `priceVersion` · `string` · read-only
- `legacyUntil` · `string <date-time>` · read-only
- `couponCode` · `string` · read-only
- `coupon` · `object` · read-only
- `items` · `object[]` · read-only
- `invoices` · `object[]` · read-only
- `paymentMethod` · `object` · read-only

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/subscription \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```json
{
  "id": "acct_01example0000000000000000x",
  "accountId": "acct_01example0000000000000000x",
  "status": "active",
  "cadence": "monthly",
  "currentPeriodEnd": "2026-01-01T00:00:00.000Z",
  "cancelAtPeriodEnd": true,
  "priceVersion": "example",
  "legacyUntil": "2026-01-01T00:00:00.000Z",
  "couponCode": "example",
  "coupon": {
    "code": "example",
    "label": "example",
    "percentOff": 1,
    "amountOffCents": 0
  },
  "items": [
    {
      "kind": "plan",
      "ref": "example",
      "quantity": 0,
      "unitPriceUsd": 0
    }
  ],
  "invoices": [
    {
      "id": "acct_01example0000000000000000x",
      "periodStart": "2026-01-01T00:00:00.000Z",
      "periodEnd": "2026-01-01T00:00:00.000Z",
      "totalUsd": 0,
      "kind": "subscription",
      "status": "example",
      "pdfUrl": "https://example.com"
    }
  ],
  "paymentMethod": {
    "brand": "example",
    "last4": "example",
    "expMonth": 1,
    "expYear": 1
  }
}
```

### Subscribe to a paid plan — returns a Stripe Checkout URL

`POST /accounts/{accountId}/subscription`

**Required role:** owner

Prices the SUBSCRIPTION as it stands today (plan fee + extra seats) and opens a hosted Stripe Checkout session. Compute — the resources your services run — is not part of Checkout: it bills monthly in arrears for what you actually ran, minus the always-free allowance and your plan credit. **The tier does not change here.** It flips only when Stripe confirms the subscription through the billing webhook, so a client must never show the new plan on this response. Idempotent: a second call reuses the account’s Stripe customer, and a call while a subscription already exists returns 409. `monthly` and `annual` cadences are supported.

**Parameters**

- `accountId` _(path)_ · `string` · required

**Request body**

- `tierId` · `string` · required — Target tier id — must be a self-serve tier (never `enterprise`).
- `cadence` · `"monthly" | "annual"` · optional — Billing interval. `annual` bills a year of plan + seats up front at the tier’s 10× price (the commitment discount). Compute always bills monthly in arrears for what you ran, either way.

**Returns**

- `200` · `CheckoutSessionDto`
- `403` · `ErrorEnvelopeDto` — not the owner · contact-sales tier · billing graduated to Control Plane
- `409` · `ErrorEnvelopeDto` — a subscription already exists
- `502` · `ErrorEnvelopeDto` — Stripe is unavailable

Attributes of `CheckoutSessionDto`:

- `checkoutUrl` · `string <uri>` · read-only — Redirect the browser here to complete payment.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/subscription \
  -X POST \
  -H "Authorization: Bearer kc_your_api_token" \
  -H "Content-Type: application/json" \
  -d '{
      "tierId": "tier_01example0000000000000000x"
    }'
```

**Example response** (`200`)

```json
{
  "checkoutUrl": "https://example.com"
}
```

### Cancel the subscription, with the refund the cadence has earned

`DELETE /accounts/{accountId}/subscription`

**Required role:** owner

Owner-only. **Monthly**: the subscription lapses at the end of the period already paid for, with no refund — `accessUntil` is that date. **Annual**: the subscription ends immediately and the unused months are refunded in cash, clawing back the commitment discount (you pay the regular monthly rate for the time you used). A very young annual subscription is cancelled but its refund is held for staff review. The tier reverts to Free when Stripe confirms the cancellation through the billing webhook.

**Parameters**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `CancelSubscriptionResultDto`
- `404` · `ErrorEnvelopeDto` — no active subscription
- `409` · `ErrorEnvelopeDto` — already set to cancel
- `502` · `ErrorEnvelopeDto` — Stripe is unavailable

Attributes of `CancelSubscriptionResultDto`:

- `status` · `"canceled"` · read-only
- `accessUntil` · `string <date-time> | "now"` · read-only
- `refund` · `object` · read-only — Present only when cash was actually refunded.
- `refundUnderReview` · `boolean` · read-only

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/subscription \
  -X DELETE \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```json
{
  "status": "canceled",
  "accessUntil": "2026-01-01T00:00:00.000Z",
  "refund": {
    "amountUsd": 0,
    "currency": "USD",
    "estimatedArrivalDays": 0
  },
  "refundUnderReview": true
}
```

### The account’s compute bill so far this period

`GET /accounts/{accountId}/compute-usage`

**Required role:** owner

Owner-only. Rates the usage ledger LIVE — gross compute accrued this period, the in-kind free allowance applied per service type, the plan’s monthly compute credit (a projection mid-period; Stripe applies it at invoice finalization), and the resulting net charged in arrears at month end. Works for EVERY account including Free (it never 404s); a Free account, or a paid one whose compute item is not yet attached, reports against the current UTC calendar month as a display window. `netUsd` of $0 is a normal answer.

**Parameters**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `ComputeUsageDto`

Attributes of `ComputeUsageDto`:

- `periodStart` · `string <date-time>` · read-only
- `periodEnd` · `string <date-time>` · read-only
- `asOf` · `string <date-time>` · read-only
- `accruedUsd` · `number` · read-only
- `allowance` · `object` · read-only
- `credit` · `object` · read-only — applied/remaining are as-of-asOf projections; the credit is applied at invoice finalization.
- `netUsd` · `number` · read-only
- `projectedNetUsd` · `number` · read-only
- `services` · `object[]` · read-only

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/compute-usage \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```json
{
  "periodStart": "2026-01-01T00:00:00.000Z",
  "periodEnd": "2026-01-01T00:00:00.000Z",
  "asOf": "2026-01-01T00:00:00.000Z",
  "accruedUsd": 0,
  "allowance": {
    "eligible": true,
    "appliedUsd": 0,
    "byType": {
      "key": {
        "appliedUsd": 0,
        "coveredServiceId": "coveredservice_01example0000000000000000x"
      }
    }
  },
  "credit": {
    "includedUsd": 0,
    "appliedUsd": 0,
    "remainingUsd": 0
  },
  "netUsd": 0,
  "projectedNetUsd": 0,
  "services": [
    {
      "serviceId": "svc_01example0000000000000000x",
      "serviceName": "example",
      "projectName": "example",
      "type": "web",
      "accruedUsd": 0,
      "allowanceAppliedUsd": 0
    }
  ]
}
```

### Redeem a promo code against the account’s subscription

`POST /accounts/{accountId}/coupon`

**Required role:** owner

Owner-only. Applies a staff-issued promo code to the live subscription; the discount comes off the next invoice and survives service changes and reprices. Requires a paid subscription — a Free account has no invoice to discount (422). An unknown or expired code answers 404/422 without revealing which.

**Parameters**

- `accountId` _(path)_ · `string` · required

**Request body**

- `code` · `string` · required

**Returns**

- `204` — the coupon was applied
- `404` · `ErrorEnvelopeDto` — no such code
- `422` · `ErrorEnvelopeDto` — expired code · no paid subscription
- `502` · `ErrorEnvelopeDto` — Stripe is unavailable

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/coupon \
  -X POST \
  -H "Authorization: Bearer kc_your_api_token" \
  -H "Content-Type: application/json" \
  -d '{
      "code": "example"
    }'
```

### Remove the applied promo code

`DELETE /accounts/{accountId}/coupon`

**Required role:** owner

Owner-only. The next invoice bills the full amount. Idempotent: removing when none is applied is a no-op.

**Parameters**

- `accountId` _(path)_ · `string` · required

**Returns**

- `204` — no coupon is applied
- `404` · `ErrorEnvelopeDto` — no subscription
- `502` · `ErrorEnvelopeDto` — Stripe is unavailable

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/coupon \
  -X DELETE \
  -H "Authorization: Bearer kc_your_api_token"
```

### A Stripe Customer Portal session — manage card, invoices, cancellation

`GET /accounts/{accountId}/billing-portal`

**Required role:** owner

**Parameters**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `BillingPortalDto`
- `409` · `ErrorEnvelopeDto` — the account has no Stripe customer yet
- `502` · `ErrorEnvelopeDto` — Stripe is unavailable

Attributes of `BillingPortalDto`:

- `url` · `string <uri>` · read-only — Open this to manage payment methods, invoices and cancellation.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/billing-portal \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```json
{
  "url": "https://example.com"
}
```
