# Billing

The team's plan, usage, invoices and add-ons, and the two hosted pages where a customer pays.

## Who can call it

Two reads are open to a scope; everything else on this page needs a `full_access` key. A scope can read billing; nothing but a `full_access` key can change it.

| Credential | The two reads | Everything else |
| --- | --- | --- |
| `full_access` | 200 | 200 |
| `sending_access` | `401 restricted_api_key` | `401 restricted_api_key` |
| OAuth token with `billing:read` | 200 | `403 invalid_permission` |
| OAuth token with every scope | 200 | `403 invalid_permission` |

The reads are `GET /billing` and `GET /billing/invoices`. No OAuth scope reaches the rest: a token holding every scope in the [catalogue](https://www.rasket.com/docs/authentication) is still `403 invalid_permission` on checkout, the portal, a plan change, pay-as-you-go, both spend-cap routes, both add-on routes and both limit-request routes.

## Hosted pages

- `POST /billing/checkout` and `POST /billing/portal` answer a **URL**. Send the customer's browser there: the card is entered on that hosted page and never passes through this API.
- `return_url` must be an absolute `https` URL. The browser comes back to it with `checkout=success` or `checkout=cancelled` appended; without one it comes back to the dashboard.
- A checkout URL stops working at its `expires_at`. Ask for a new one.

> A few changes are refused for a key even when it is `full_access`: anything that would turn off the team's single sign-on — removing the `sso` add-on, or a plan change that drops it — needs a signed-in admin, because it decides who may sign in.

## Endpoints

### `GET /billing`

The plan, this period's usage, payment state, add-ons and any pending change.

Retrieve billing:

```sh
curl -X GET "https://api.rasket.com/billing" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/billing", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/billing",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "billing",
  "plan": {
    "code": "pro_50k",
    "name": "Pro 50k",
    "monthly_price_cents": 2000,
    "included_emails": 50000,
    "overage_per_1000_cents": 60
  },
  "effective_plan_code": "pro_50k",
  "subscription": {
    "status": "active",
    "current_period_start": "2026-09-01T00:00:00.000Z",
    "current_period_end": "2026-10-01T00:00:00.000Z",
    "invoicing": "automatic",
    "latest_invoice_status": "paid",
    "trial_ends_at": null
  },
  "usage": {
    "period_start": "2026-09-01",
    "period_end": "2026-10-01",
    "emails": 12840,
    "included_emails": 50000,
    "emails_today": 412,
    "day_start": "2026-09-14",
    "daily_limit": 10000,
    "monthly_limit": 50000,
    "marketing_emails": 0,
    "marketing_included_emails": 10000,
    "marketing_contacts_included": 1000,
    "marketing_addon_code": null
  },
  "pay_as_you_go": {
    "enabled": false
  },
  "payment_method": {
    "present": true,
    "brand": "visa",
    "last4": "4242"
  },
  "addons": [],
  "pending_change": null,
  "payment_state": null,
  "checkout_available": false,
  "paid_plan": true,
  "storage_addon_purchase": "add_item",
  "inbox_seats": {
    "code": "inbox_seat",
    "quantity": 0,
    "included": 3
  },
  "dedicated_ip": {
    "recommended_min_daily": 5000,
    "avg_daily_sends_30d": 412
  }
}
```

- `payment_method` says whether a card is on file and shows its brand and last four digits. The card itself never leaves the payment provider.
- `usage.emails_today` counts today's sends against `usage.daily_limit`, the most this team can send today. The count starts again at 00:00 UTC. `daily_limit` is `null` when there is no daily limit.
- `usage.monthly_limit` is the most this team can send this month before a send is refused. It can differ from `usage.included_emails`, the plan's allowance, when a limit was set for the team or the sending tier's cap is lower than the plan. `null` when nothing caps the month.
- `checkout_available` is `true` when the team has no subscription yet, so `POST /billing/checkout` is the way to start one. `paid_plan` is the other question: a team can hold a subscription for inbox storage alone while still on the free plan.
- `usage.marketing_*` is the marketing meter, its own pool: campaign recipients never count against `usage.emails`, and `POST /emails` never counts against `marketing_emails`.
- `inbox_seats` counts Inbox mailboxes past the ones the plan includes; `dedicated_ip` puts the volume a dedicated IP is recommended from beside this team's own 30-day daily average.
- `storage_addon_purchase` says how to buy inbox storage right now — `add_item` through `POST /billing/addons`, `checkout` through `POST /billing/checkout` with `addon_code`, `held` when the team already has it, or `unavailable` when it cannot be bought at the moment. Both purchase calls answer `503 service_unavailable` while it is `unavailable`, before anything is charged or recorded.

### `GET /billing/invoices`

The team's invoices, newest first, with links to the hosted invoice and its PDF.

List invoices:

```sh
curl -X GET "https://api.rasket.com/billing/invoices" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/billing/invoices", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/billing/invoices",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "in_1Q2w3E4r5T6y7U8i",
      "number": "ACME-0007",
      "status": "paid",
      "total_cents": 2000,
      "currency": "usd",
      "created_at": "2026-09-01T00:00:00.000Z",
      "hosted_invoice_url": "https://invoices.example.com/in_1Q2w3E4r5T6y7U8i",
      "invoice_pdf": "https://invoices.example.com/in_1Q2w3E4r5T6y7U8i.pdf"
    }
  ]
}
```

- Read from the payment provider and cached for sixty seconds per team.

### `POST /billing/checkout`

A hosted checkout page for a plan, or for inbox storage on its own. Send the customer's browser to its `url`.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `plan_code` | string | The plan to start the subscription on, such as `pro_50k` or `scale`. Send this or `addon_code`, never both. |
| `addon_code` | string | Buy an add-on on its own, leaving the team on the plan it is already on: `inbox_storage_100gb` or `inbox_seat`. Send this or `plan_code`, never both. |
| `return_url` | string | Where the browser comes back to, with `checkout=success` or `checkout=cancelled` appended. Absolute `https` only. Defaults to the dashboard page `return_to` names. |
| `return_to` | string | Which dashboard page to come back to when there is no `return_url`: `plan` (the default), `inbox` or `inbox_settings`. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `quantity` | integer | Units to buy, 1–100, with `addon_code` only. Defaults to 1; only `inbox_seat` is sold by the unit. |

Start a subscription:

```sh
curl -X POST "https://api.rasket.com/billing/checkout" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "plan_code": "pro_50k",
  "return_url": "https://app.example.com/settings/billing"
}'
```

```ts
const response = await fetch("https://api.rasket.com/billing/checkout", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    plan_code: "pro_50k",
    return_url: "https://app.example.com/settings/billing"
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/billing/checkout",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "plan_code": "pro_50k",
    "return_url": "https://app.example.com/settings/billing"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "checkout_session",
  "url": "https://checkout.example.com/c/pay/cs_live_a1B2c3D4",
  "expires_at": "2026-09-13T10:00:00.000Z"
}
```

- The customer enters their card on the hosted page. Card details never pass through this API.
- A `return_url` that is not absolute `https` is `422 validation_error`.
- Send exactly one of `plan_code` and `addon_code`. Neither is `422 missing_required_field`; both is `422 invalid_parameter`.
- `addon_code: "inbox_storage_100gb"` buys 100 GB of inbox storage without a plan. The subscription it creates carries only that add-on, and `POST /billing/plan` can add a plan to it later.
- A team that already has a subscription is `422 validation_error`: it changes its plan with `POST /billing/plan` and buys add-ons with `POST /billing/addons`.
- Called with an API key, no email address is sent to the payment provider: the customer types one on the hosted page.

### `POST /billing/portal`

A hosted page for the payment method, address, tax ID, invoices and cancellation.

Open the customer portal:

```sh
curl -X POST "https://api.rasket.com/billing/portal" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/billing/portal", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/billing/portal",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "portal_session",
  "url": "https://billing.example.com/p/session/bps_a1B2c3D4"
}
```

- Takes no body. Send the customer's browser to `url`.

### `POST /billing/plan`

Move an existing subscription to another paid plan.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `plan_code` (required) | string | The plan to move to, such as `pro_50k`, `pro_100k` or `scale`. |

Change the plan:

```sh
curl -X POST "https://api.rasket.com/billing/plan" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "plan_code": "scale"
}'
```

```ts
const response = await fetch("https://api.rasket.com/billing/plan", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    plan_code: "scale"
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/billing/plan",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "plan_code": "scale"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "billing",
  "plan": {
    "code": "pro_50k",
    "name": "Pro 50k",
    "monthly_price_cents": 2000,
    "included_emails": 50000,
    "overage_per_1000_cents": 60
  },
  "effective_plan_code": "pro_50k",
  "subscription": {
    "status": "active",
    "current_period_start": "2026-09-01T00:00:00.000Z",
    "current_period_end": "2026-10-01T00:00:00.000Z",
    "invoicing": "automatic",
    "latest_invoice_status": "paid",
    "trial_ends_at": null
  },
  "usage": {
    "period_start": "2026-09-01",
    "period_end": "2026-10-01",
    "emails": 12840,
    "included_emails": 50000,
    "emails_today": 412,
    "day_start": "2026-09-14",
    "daily_limit": 10000,
    "monthly_limit": 50000,
    "marketing_emails": 0,
    "marketing_included_emails": 10000,
    "marketing_contacts_included": 1000,
    "marketing_addon_code": null
  },
  "pay_as_you_go": {
    "enabled": false
  },
  "payment_method": {
    "present": true,
    "brand": "visa",
    "last4": "4242"
  },
  "addons": [],
  "pending_change": null,
  "payment_state": null,
  "checkout_available": false,
  "paid_plan": true,
  "storage_addon_purchase": "add_item",
  "inbox_seats": {
    "code": "inbox_seat",
    "quantity": 0,
    "included": 3
  },
  "dedicated_ip": {
    "recommended_min_daily": 5000,
    "avg_daily_sends_30d": 412
  }
}
```

- An upgrade takes effect at once, with prorations. A downgrade is scheduled for the end of the period and shows in `pending_change`.
- Free to paid is a checkout, and paid to free is a cancellation in the portal, so both are refused here.
- A change that would turn off the team's single sign-on is refused for an API key: a signed-in admin makes it.

### `POST /billing/payg`

Keep sending beyond the plan's included emails, billed per thousand, or stop at them.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `enabled` (required) | boolean | `true` to continue beyond the quota, `false` to stop at it. |

Set pay-as-you-go:

```sh
curl -X POST "https://api.rasket.com/billing/payg" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": true
}'
```

```ts
const response = await fetch("https://api.rasket.com/billing/payg", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    enabled: true
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/billing/payg",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "enabled": True
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "billing_payg",
  "enabled": true,
  "overage_ceiling_emails": null
}
```

- It is on by default once the project is on a paid plan with a payment method on file. Setting it here makes it your choice, and it is not changed for you again.
- Switching it on needs a plan with an overage rate and a payment method on file; otherwise `422 validation_error` names what is missing.
- It raises only the monthly quota — never the daily cap or a restricted team's limits. There is no overage ceiling; the project's spend cap still applies.

### `GET /billing/spend-cap`

The most this project may send this month, the rungs it warns at, and what it has sent.

Read the spend cap:

```sh
curl -X GET "https://api.rasket.com/billing/spend-cap" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/billing/spend-cap", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/billing/spend-cap",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "billing_spend_cap",
  "emails": 500000,
  "is_default": true,
  "alert_pcts": [80, 100],
  "used_emails": 412300,
  "sending_paused_at": null,
  "sending_paused_reason": null
}
```

- `used_emails` counts billable recipients on both meters, so a campaign and a receipt draw on the same cap.
- A project that has set no cap of its own is held to ten times its monthly allowance, which is what `is_default` reports.

### `POST /billing/spend-cap`

Set how much this project may send in a month, and when to be warned.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `emails` (required) | integer | The most this project may send in a month, in billable recipients. `0` pauses it on its next message. |
| `alert_pcts` | integer[] | Whole percentages of the cap to be warned at, one to eight of them. Omit to leave the current rungs alone. |

Set the spend cap:

```sh
curl -X POST "https://api.rasket.com/billing/spend-cap" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "emails": 500000,
  "alert_pcts": [80, 100]
}'
```

```ts
const response = await fetch("https://api.rasket.com/billing/spend-cap", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    emails: 500000,
    alert_pcts: [80, 100]
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/billing/spend-cap",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "emails": 500000,
    "alert_pcts": [80, 100]
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "billing_spend_cap",
  "emails": 500000,
  "is_default": false,
  "alert_pcts": [80, 100],
  "used_emails": 412300,
  "sending_paused_at": null,
  "sending_paused_reason": null
}
```

- Reaching the cap pauses this project's sending and leaves every other project on the account untouched.
- Saving takes effect at once: a cap above the month's usage lifts a pause the cap caused, and a cap at or below it pauses the project there and then.
- While it is paused, a send answers `429 monthly_quota_exceeded`. Raising the cap starts it again, and so does the next month.

### `POST /limits/requests`

Ask to send more. Small, safe increases are granted in the same request.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `requested_daily` | integer | The daily limit you would like, in emails a day. The only ask that can be granted automatically. |
| `requested_monthly` | integer | The monthly limit you would like. Always read by a person: a monthly allowance is part of the plan. |
| `expected_volume` | integer | Emails a month you expect to send at peak. |
| `use_case` (required) | string | What this project sends and why it needs more room. At most 1,000 characters. |

Ask for a higher sending limit:

```sh
curl -X POST "https://api.rasket.com/limits/requests" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "requested_daily": 20000,
  "expected_volume": 400000,
  "use_case": "Receipts and sign-in links for a store that is about to run a sale."
}'
```

```ts
const response = await fetch("https://api.rasket.com/limits/requests", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    requested_daily: 20000,
    expected_volume: 400000,
    use_case: "Receipts and sign-in links for a store that is about to run a sale."
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/limits/requests",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "requested_daily": 20000,
    "expected_volume": 400000,
    "use_case": "Receipts and sign-in links for a store that is about to run a sale."
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "limit_request",
  "id": "0199f1d5-6a3e-7c21-9a2f-3b9e2c4d5e6f",
  "status": "approved",
  "requested_daily": 20000,
  "requested_monthly": null,
  "expected_volume": 400000,
  "use_case": "Receipts and sign-in links for a store that is about to run a sale.",
  "granted_daily": 20000,
  "granted_monthly": null,
  "decision_reason": null,
  "decided_at": "2026-09-14T10:31:00.000Z",
  "created_at": "2026-09-14T10:31:00.000Z"
}
```

- An ask for a daily limit inside the band — at most twice what is in force, never past the ceiling of the project's sending tier, after its first week, and only while its delivery numbers are good — is `approved` in the same response and the new limit is already in force.
- Anything else is `pending`: somebody here reads it, and `decision_reason` says why it was not automatic. A project whose sending is being held down is `declined`, with the reason.
- Asking again while one is still `pending` answers with the ask already open rather than opening a second.

### `GET /limits/requests`

The asks this project has made, newest first, and what became of each.

List limit requests:

```sh
curl -X GET "https://api.rasket.com/limits/requests" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/limits/requests", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/limits/requests",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "data": [
    {
      "object": "limit_request",
      "id": "0199f1d5-6a3e-7c21-9a2f-3b9e2c4d5e6f",
      "status": "approved",
      "requested_daily": 20000,
      "requested_monthly": null,
      "expected_volume": 400000,
      "use_case": "Receipts and sign-in links for a store that is about to run a sale.",
      "granted_daily": 20000,
      "granted_monthly": null,
      "decision_reason": null,
      "decided_at": "2026-09-14T10:31:00.000Z",
      "created_at": "2026-09-14T10:31:00.000Z"
    }
  ],
  "has_more": false
}
```

### `POST /billing/addons`

Buy an add-on, or set how many units of it the team holds.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `addon_code` (required) | string | `extra_domains_100`, `dedicated_ip`, `sso`, `inbox_storage_100gb`, `inbox_seat`, or one of the marketing plans: `marketing_5k`, `marketing_10k`, `marketing_15k`, `marketing_25k`, `marketing_50k`, `marketing_100k`, `marketing_150k`. |
| `quantity` | integer | Units held after the call, 1–100. A set, not an increment. Defaults to 1. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `region` | string | `dedicated_ip` only: the region the pool should live in. Defaults to `us-east-1`. |
| `expected_daily_volume` | integer | Deprecated and ignored: a dedicated IP has no volume requirement. |

Set an add-on:

```sh
curl -X POST "https://api.rasket.com/billing/addons" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "addon_code": "extra_domains_100",
  "quantity": 1
}'
```

```ts
const response = await fetch("https://api.rasket.com/billing/addons", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    addon_code: "extra_domains_100",
    quantity: 1
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/billing/addons",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "addon_code": "extra_domains_100",
    "quantity": 1
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "team_addon",
  "addon_code": "extra_domains_100",
  "name": "100 extra domains",
  "monthly_price_cents": 2000,
  "quantity": 1,
  "status": "active",
  "requested_at": "2026-09-12T10:00:00.000Z",
  "provisioned_at": "2026-09-12T10:00:00.000Z"
}
```

- Every add-on is an item on the team's subscription, so a team without one is `422 validation_error` carrying `details.next: "checkout"` — `POST /billing/checkout` starts a subscription, and for `inbox_storage_100gb` it can buy the add-on on its own.
- Everything except `inbox_storage_100gb` also needs a paid plan, so a team that bought inbox storage on its own is still refused the rest of the catalogue.
- `dedicated_ip` is billed from now and normally reaches `active` within the request; it stays `provisioning` only while no address is free. Warm-up then runs on its own, and `GET /billing` shows it.
- For `inbox_seat`, `quantity` must equal the team's open mailboxes minus the ones the plan includes: seats follow the mailboxes.
- A marketing plan replaces any other marketing plan the team holds, in the same call, and takes `quantity` 1.
- `sso` needs the Scale plan or above, and is included on Enterprise.

### `DELETE /billing/addons/{addon_code}`

Take an add-on off the subscription, with a proration.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `addon_code` (required) | string | Any add-on code `POST /billing/addons` accepts. |

Remove an add-on:

```sh
curl -X DELETE "https://api.rasket.com/billing/addons/extra_domains_100" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/billing/addons/extra_domains_100", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/billing/addons/extra_domains_100",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "team_addon",
  "addon_code": "extra_domains_100",
  "name": "100 extra domains",
  "monthly_price_cents": 2000,
  "quantity": 1,
  "status": "canceled",
  "requested_at": "2026-09-12T10:00:00.000Z",
  "provisioned_at": "2026-09-12T10:00:00.000Z"
}
```

- Removing `extra_domains_100` deletes no domain: the lower limit only refuses the next one you add. Removing a marketing plan likewise keeps the contacts you hold and refuses the next one past the plan's own limit.
- Removing `sso` turns single sign-on off, so an API key is refused it with `403`: a signed-in admin removes it.
