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

# Billing

> View your plan and credit usage, buy credit top-ups, and manage your subscription through Stripe.

Anona holds the Stripe relationship, so you never touch Stripe keys directly. Paid-plan
changes go through Stripe Checkout or the Customer Portal; free-plan changes happen in
place.

| Method and path | What it does | Role required |
| - | - | - |
| `GET /v1/billing` | Current plan, credit usage, and available plans. | Any member |
| `GET /v1/billing/topups` | Credit-pack purchase history. | Any member |
| `POST /v1/billing/upgrade` | Moves to a free plan, in place. | Owner or admin |
| `POST /v1/billing/checkout` | Starts Stripe Checkout for a paid plan. | Owner or admin |
| `POST /v1/billing/topup` | Starts Stripe Checkout for a one-time credit pack. | Owner or admin |
| `POST /v1/billing/portal` | Opens the Stripe Customer Portal. | Owner or admin |
| `POST /v1/billing/cancel` | Cancels at the end of the billing period. | Owner |
| `POST /v1/billing/resume` | Undoes a scheduled cancellation before it lands. | Owner |

These are control-plane calls, so they authenticate with a **dashboard session**, not an
API key, and neither memory SDK wraps them. `$SESSION` is the session token sign-in
returns.

```bash cURL theme={null}
curl https://api.anonalabs.com/v1/billing \
  -H "Authorization: Bearer $SESSION"
```

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

data = httpx.get(
    "https://api.anonalabs.com/v1/billing",
    headers={"Authorization": f"Bearer {SESSION}"},
).json()
```

```typescript TypeScript theme={null}
const res = await fetch("https://api.anonalabs.com/v1/billing", {
  headers: { Authorization: `Bearer ${SESSION}` },
});
const data = await res.json();
```

## View billing status

`GET /v1/billing` returns the current plan, credit usage, and every plan you could switch
to. `available_plans` lists only the plans on sale today. An organization on a plan we no
longer offer keeps it: `plan_id`, its allowance and its billing are unchanged, and it
simply does not see that plan listed back to itself.

`credits_reset_at` is when the credit allowance refreshes: on a paid plan, the end of the
Stripe billing period, so it falls on the day of the month you subscribed; on Developer,
the signup anniversary. It is **not** the 1st of the calendar month. A date landing in a month
too short for it (the 31st, in February) moves to the last day of that month.

`current_month_ops`, the per-operation counts and `line_items` are counted over the same
billing period as `credits_used`, from `period_start` to `credits_reset_at` (the field
keeps its old name for compatibility). `period_start` is null only for a legacy account
with no stored boundary, where the counts fall back to the calendar month.

```json theme={null}
{
  "plan_id": "growth",
  "plan_name": "Growth",
  "price_per_month_usd": 249.0,
  "credits_limit": 200000,
  "credits_used": 1240,
  "credits_remaining": 203760,
  "credits_banked": 5000,
  "credits_reset_at": "2026-09-20T14:32:11Z",
  "period_start": "2026-08-20T14:32:11Z",
  "rate_limit_per_min": 30,
  "has_subscription": true,
  "cancel_at_period_end": false,
  "subscription_ends_at": null,
  "current_month_ops": 680,
  "memories_added": 400,
  "searches": 200,
  "insights": 20,
  "line_items": [
    { "op_type": "Memories Added", "count": 400, "unit_price": 1.5, "subtotal": 600.0 },
    { "op_type": "Searches", "count": 200, "unit_price": 1.0, "subtotal": 200.0 },
    { "op_type": "Insights", "count": 20, "unit_price": 12.0, "subtotal": 240.0 },
    { "op_type": "Chat Turns", "count": 60, "unit_price": 3.33, "subtotal": 200.0 }
  ],
  "available_plans": [
    { "plan_id": "developer", "name": "Developer", "price_per_month_usd": 0.0, "credits": 5000, "rate_limit_per_min": 20, "max_spaces": 2 },
    { "plan_id": "growth", "name": "Growth", "price_per_month_usd": 249.0, "credits": 200000, "rate_limit_per_min": 30, "max_spaces": 100 }
  ],
  "topup_packs": [
    { "id": "small", "credits": 5000, "price_usd": 7.5 },
    { "id": "medium", "credits": 25000, "price_usd": 37.5 },
    { "id": "large", "credits": 100000, "price_usd": 150.0 }
  ],
  "can_buy_topups": true,
  "billed_to": null,
  "shared_orgs": []
}
```

| Field | Meaning |
| - | - |
| `credits_limit`, `credits_used` | The plan allowance for this period and how much of it is spent. |
| `credits_banked` | Purchased top-up credits left. Never reset. |
| `credits_remaining` | What you can still spend: the unspent allowance plus `credits_banked`. |
| `credits_reset_at`, `period_start` | The current billing period. |
| `has_subscription` | Whether a Stripe subscription is active. |
| `cancel_at_period_end`, `subscription_ends_at` | Whether that subscription is set to stop, and when access ends. Read from Stripe. |
| `topup_packs` | Packs this organization can buy now. Empty without a paid subscription, and inside a shared organization billed to another one. |
| `can_buy_topups` | Whether the caller may buy a pack right now: an owner or admin of a personal organization with an active subscription. The same rule `POST /v1/billing/topup` enforces, so a `false` here means that call returns 403. Show your buy buttons only when it is `true`. |
| `billed_to` | Set only inside a shared organization billed to another organization: the payer's name. Every credit figure is then the payer's pool, `price_per_month_usd` is null, the usage counts are zero, and `line_items`, `available_plans` and `topup_packs` are empty. |
| `shared_orgs` | For a personal organization, the shared organizations it pays for and the credits each used this period. |

Each line item's `subtotal` is the credits actually charged for that kind of operation this
period, and `unit_price` is that subtotal divided by `count`: an average, since what one
operation costs depends on the model and the tokens it used. A refunded charge reduces the
subtotal and is not counted as an operation. `current_month_ops` counts every kind,
chat turns included.

> [!NOTE]
>
> `line_items` is credit-cost accounting, not a Stripe invoice. The amount you are actually
> billed comes from Stripe.

## Change plan

### Free tiers

```http theme={null}
POST /v1/billing/upgrade

{ "plan_id": "developer" }
```

Valid only for plans priced at zero. Any paid plan returns `400 payment_required` and
directs you to `/v1/billing/checkout` instead.

### Paid plans

```http theme={null}
POST /v1/billing/checkout

{ "plan_id": "growth" }
```

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

Redirect the user to `checkout_url`. Stripe's webhook flips the organization onto the
new plan and syncs its quota once payment completes, so polling `/v1/billing` right
after the redirect may still show the old plan for a few seconds.

## Manage an existing subscription

`POST /v1/billing/portal` requires an existing Stripe customer, meaning you have
completed checkout at least once.

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

Redirect the user there to update a payment method, view invoices, switch between paid
plans, or cancel. Plan switches are prorated by Stripe, which is why they belong in the
portal rather than in a second checkout.

An **upgrade** applies immediately, and the organization's unused credits are added on top
of the new plan's allowance for the current period. A **downgrade** is scheduled for the
end of the period already paid for, exactly like a cancellation: the organization keeps
the larger plan and its limits until then, and `GET /v1/billing` keeps reporting the
larger plan until the change lands.

## Top-up packs

An organization on a paid plan can buy extra credits at any time, at \$1.50 per 1,000.

| `pack` | Credits | Price |
| - | - | - |
| `small` | 5,000 | \$7.50 |
| `medium` | 25,000 | \$37.50 |
| `large` | 100,000 | \$150.00 |

```http theme={null}
POST /v1/billing/topup
```

```json theme={null}
{ "pack": "small" }
```

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

Redirect the user to `checkout_url`; it is a one-time card payment, not a subscription.
Credits are added once Stripe confirms payment, so `GET /v1/billing` may take a few
seconds to show them. It reports them as `credits_banked`, and `credits_remaining` is
your plan allowance still unspent plus `credits_banked`. `topup_packs` lists the packs
on offer.

Purchased credits are spent only after the plan allowance is used up, they are **not**
reset at the end of the billing period, and they never expire. A free Developer
organization gets `403 topup_requires_paid_plan` and has to upgrade first.

If a pack is refunded, its credits are removed from `credits_banked` in proportion to
the amount refunded; a chargeback that is lost removes the whole pack. Credits from the
pack that were already spent are not reversed, and the balance never goes below zero.
If a refund later fails or is canceled, the credits it removed are put back.

Inside a shared organization billed to another organization, `/topup` answers
`403 billing_personal_org_only`: buy from the owner's personal organization, whose pool
the shared organization draws on. See
[Credit top-ups](https://docs.memory.anonalabs.com/guides/plans-and-billing.md#credit-top-ups) for the full rules.

### Purchase history

```http theme={null}
GET /v1/billing/topups
```

```json theme={null}
{
  "items": [
    {
      "id": "9f2c41d07ab3",
      "purchased_at": "2026-10-12T09:41:27Z",
      "pack": "medium",
      "credits": 25000,
      "amount_usd": 37.5,
      "refunded_credits": 5000,
      "status": "partially_refunded"
    },
    {
      "id": "51e0c8a2fd64",
      "purchased_at": "2026-09-30T16:02:10Z",
      "pack": "small",
      "credits": 5000,
      "amount_usd": 7.5,
      "refunded_credits": 0,
      "status": "active"
    }
  ]
}
```

Newest first, at most 50.

| Field | Meaning |
| - | - |
| `id` | Opaque, stable identifier for the purchase. |
| `purchased_at` | When the credits were added. |
| `pack`, `credits`, `amount_usd` | The pack bought, its credits and what it cost. |
| `refunded_credits` | Credits taken back by a refund or a lost chargeback, including any already spent. |
| `status` | `active`, `partially_refunded`, or `refunded` once `refunded_credits` reaches `credits`. |

Inside a shared organization billed to another organization, `items` is always empty:
the purchases are the payer's.

## Cancel

```http theme={null}
POST /v1/billing/cancel
```

```json theme={null}
{ "message": "Subscription will cancel at end of billing period." }
```

Cancellation takes effect at the end of the current billing period, the same date as
`credits_reset_at`. The organization keeps its paid plan and credits until then, and
purchased credits stay after it. `POST /v1/billing/resume` undoes it any time before
that date:

```json theme={null}
{ "message": "Subscription resumed. Your plan will renew." }
```

## Error responses

| Status | Code | Cause |
| - | - | - |
| 400 | `payment_required` | `/upgrade` was called with a paid plan. Use `/checkout`. |
| 400 | `invalid_plan` | `/checkout` was called with a `plan_id` that is free, unknown, or no longer on sale. |
| 400 | `invalid_plan` | `/upgrade` was called with a `plan_id` that is unknown or no longer on sale. A free `plan_id` is the valid case here — a paid one is `payment_required` below. |
| 400 | `no_subscription` | `/portal`, `/cancel` or `/resume` was called with no Stripe subscription on file. |
| 403 | `insufficient_role` | The caller's role does not permit the billing action. |
| 403 | `topup_requires_paid_plan` | `/topup` was called by an organization with no paid subscription. Upgrade first. |
| 403 | `billing_personal_org_only` | A billing change was attempted from a shared organization billed to another one. Switch to the personal organization. |
| 409 | `subscription_exists` | A subscription is already active. A second checkout would create a **second** subscription and bill twice, so plan changes go through `/portal`. Also returned by `/upgrade` when downgrading to Free while Stripe is still billing. Cancel in the portal instead. |
| 409 | `checkout_in_progress` | A checkout for a *different* plan is already open for this organization. Finish or abandon it first; retrying the **same** plan is allowed and returns the original session. |
| 503 | `billing_not_configured` | Stripe is not configured on this deployment. |
| 503 | `price_not_configured` | No Stripe price is configured for that plan or top-up pack. |

See the full [error reference](https://docs.memory.anonalabs.com/api-reference/errors.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.