# Billing API

> REST endpoints for billing — request and response reference with examples.

<!-- Source: https://docs.actuallycare.com/api/reference/billing -->

Subscription plans and checkout. The public-plans and public-checkout endpoints require no authentication; the rest operate on the authenticated account. Pricing on [www.actuallycare.com/pricing](https://www.actuallycare.com/pricing) comes from these endpoints.

Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors).

## Get public plan pricing

```http
GET /v1/billing/public-plans
```

Returns the current subscription plans with pricing in cents. This endpoint requires no authentication and is the source of truth for pricing shown on the marketing site. Responses are cacheable (5 minutes in browsers, 1 hour at the CDN edge).

### Example request

```bash title="cURL"
curl "https://api.actuallycare.com/v1/billing/public-plans"
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/billing/public-plans");
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.actuallycare.com/v1/billing/public-plans",
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": {
    "plans": [
      {
        "key": "agent",
        "name": "ActuallyCare+ Agent",
        "priceMonthly": 10000,
        "priceYearly": 120000,
        "trialDays": 30,
        "seats": 1,
        "features": [
          "example features"
        ]
      }
    ],
    "currency": "usd",
    "version": "example version"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Available plans |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## Start checkout without an account

```http
POST /v1/billing/public-checkout
```

Creates a Stripe Checkout session for a new customer who doesn't have an account yet. Only the agent plan is available through this endpoint; team and brokerage subscriptions require an account and the authenticated checkout endpoint. Returns the Stripe-hosted checkout URL to redirect the visitor to.

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Yes | Email address for the new customer and Stripe Checkout session · Format: email |
| `planKey` | enum | No | Subscription plan tier (only agent is available without an account) · One of: `agent` · Default: `"agent"` |
| `interval` | enum | No | Billing interval (defaults to monthly) · One of: `monthly`, `yearly` · Default: `"monthly"` |

### Example request

```bash title="cURL"
curl -X POST "https://api.actuallycare.com/v1/billing/public-checkout" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "agent@example.com"
  }'
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/billing/public-checkout", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "email": "agent@example.com"
  }),
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.actuallycare.com/v1/billing/public-checkout",
    json={
        "email": "agent@example.com"
    },
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": {
    "url": "example url",
    "sessionId": "example sessionId"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Checkout session created |
| `400` | Invalid request data |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## Get plans with checkout identifiers

```http
GET /v1/billing/plans
```

Returns the subscription plans for the in-app billing page. Same plans as the public endpoint, plus the identifiers needed to start an authenticated checkout. Prices here are in dollars.

### Example request

```bash title="cURL"
curl "https://api.actuallycare.com/v1/billing/plans" \
  -H "Authorization: Bearer YOUR_JWT"
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/billing/plans", {
  headers: {
    "Authorization": "Bearer YOUR_JWT",
  },
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.actuallycare.com/v1/billing/plans",
    headers={"Authorization": "Bearer YOUR_JWT"},
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": [
    {
      "key": "agent",
      "name": "ActuallyCare+ Agent",
      "priceId": "price_1QxT4kGH2vSNn8LKcAgentMo",
      "priceIds": {
        "monthly": "price_1QxT4kGH2vSNn8LKcAgentMo",
        "yearly": "price_1QxT5bGH2vSNn8LKcAgentYr"
      },
      "price": 100,
      "priceMonthly": 100,
      "priceYearly": 1000,
      "seats": 1,
      "escrowsPerMonth": -1,
      "includes": "base",
      "features": [
        "1 agent seat",
        "Unlimited data",
        "Base AI included (~20 requests/session)"
      ]
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Available plans |
| `401` | Authentication required |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## Get current subscription status

```http
GET /v1/billing/subscription
```

Returns the authenticated user's subscription state at each scope (personal, team, brokerage) plus which scope currently provides coverage.

### Example request

```bash title="cURL"
curl "https://api.actuallycare.com/v1/billing/subscription" \
  -H "Authorization: Bearer YOUR_JWT"
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/billing/subscription", {
  headers: {
    "Authorization": "Bearer YOUR_JWT",
  },
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.actuallycare.com/v1/billing/subscription",
    headers={"Authorization": "Bearer YOUR_JWT"},
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": {
    "personal": {
      "status": "active",
      "tier": "agent",
      "currentPeriodEnd": "2026-07-15T14:32:10.000Z",
      "canceledAt": "2026-07-15T14:32:10.000Z"
    },
    "team": {},
    "brokerage": {},
    "effective_coverage": "brokerage"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Subscription status per scope |
| `401` | Authentication required |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## Start checkout for a subscription

```http
POST /v1/billing/checkout
```

Creates a Stripe Checkout session for the authenticated user at the requested scope. Valid scope and plan combinations are personal+agent, team+team, brokerage+team, and brokerage+broker. Eligibility depends on your role and how your brokerage pays for seats; ineligible requests return 400 with a specific reason.

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `scope` | enum | Yes | Subscription scope the seat applies to (personal, team, or brokerage) · One of: `personal`, `team`, `brokerage` |
| `planKey` | enum | Yes | Subscription plan tier to subscribe to · One of: `agent`, `team`, `broker` |
| `interval` | enum | No | Billing interval (defaults to monthly) · One of: `monthly`, `yearly` · Default: `"monthly"` |

### Example request

```bash title="cURL"
curl -X POST "https://api.actuallycare.com/v1/billing/checkout" \
  -H "Authorization: Bearer YOUR_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "personal",
    "planKey": "agent"
  }'
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/billing/checkout", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_JWT",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "scope": "personal",
    "planKey": "agent"
  }),
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.actuallycare.com/v1/billing/checkout",
    headers={"Authorization": "Bearer YOUR_JWT"},
    json={
        "scope": "personal",
        "planKey": "agent"
    },
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": {
    "url": "example url",
    "sessionId": "example sessionId"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Checkout session created |
| `400` | Invalid scope/plan combination or eligibility check failed (the message explains why) |
| `401` | Authentication required |
| `404` | Team or brokerage not found |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## Open the billing portal

```http
POST /v1/billing/portal
```

Creates a Stripe customer portal session where the user can manage payment methods, view invoices, and change or cancel their subscription. Requires an existing billing account (returns 400 if the user has never subscribed).

### Example request

```bash title="cURL"
curl -X POST "https://api.actuallycare.com/v1/billing/portal" \
  -H "Authorization: Bearer YOUR_JWT"
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/billing/portal", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_JWT",
  },
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.actuallycare.com/v1/billing/portal",
    headers={"Authorization": "Bearer YOUR_JWT"},
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": {
    "url": "example url"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Portal session created |
| `400` | No billing account found — subscribe to a plan first |
| `401` | Authentication required |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |
