---
title: "Grant a user a plan"
method: PUT
path: "/v1/admin/users/{user_id}/plan"
tags: ["Admin"]
---

# Grant a user a plan

`PUT /v1/admin/users/{user_id}/plan`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/admin/users/{user_id}/plan`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

Puts the account on a rung of the infra ladder. Needs the `admin` role.
`PUT` because an account is on exactly one rung at a time, but the change is **recorded rather than overwritten**: the plan history is append-only and the newest line is the account's plan. Setting the rung it already holds still writes a line — a re-confirmation is an act worth a record, and the `reason` given with it would otherwise have nowhere to go. Read the history back with `GET` on this path.
An upgrade takes effect at once. A downgrade is the same call with a lower rung and takes effect when the month turns: until then `plan` stays the rung held and `next_plan` names the new one. At the turn, active channels past the new rung's allowance are disabled, oldest first; an upgrade re-enables them.
A rung whose content-retention window a contract sets — `enterprise` — **requires `retention_days`**, since nothing else can answer what that window is and an unanswered one bounds nothing. Every other rung sets its own and refuses the field.

## Path parameters

- `user_id` string, required

## Request body

- UserPlanGrant
  - `plan` 'free' | 'pro' | 'business' | 'enterprise', required: The rung to put the account on.
  - `retention_days` integer, nullable: The content-retention window the contract negotiated, in days. **Required on `enterprise`**, whose window the pricing table leaves to a contract: without one nothing bounds what the account's projects store, so it is refused rather than defaulted. Send `null` for a contract that keeps content indefinitely — that is a decision the field records, not the absence of one. **Refused on every other rung**, whose window the plan itself sets.
  - `reason` string, nullable: Why the account was moved. Recorded on the line and never edited, so it is the only account of intent the history carries.

## Response `200`

The account, on its new plan.

- AdminUser
  - `id` string, required: Public user ID (user_ prefix).
  - `email` string, email, required
  - `name` string, nullable
  - `email_verified` boolean, required
  - `roles` string[], required: Roles on naturali itself, not on a project. Empty for almost every account; `admin` marks a naturali operator. Changed only through `PUT /v1/admin/users/{user_id}/roles`, by a caller holding the role that grants each one.
  - `usage_alerts` boolean, required: Whether the account is emailed when its model credit, runs or indexed storage reaches 75%, 90% and 100%. On for a new account.
  - `created_at` string, date-time, required
  - `plan` 'free' | 'pro' | 'business' | 'enterprise', required: The rung in effect: the highest the account held this month. `free` for an account with no subscription. It gates the feature lines and the resource counts of the pricing table.
  - `next_plan` 'free' | 'pro' | 'business' | 'enterprise', required: The rung the account moves to when the month turns; differs from `plan` only while a downgrade is pending.
  - `retention_days` integer, nullable, required: The longest content-retention window this account's projects may keep content for, in days. On a plan whose window a contract sets, this is the figure recorded with the grant; `null` means nothing bounds it. Resolved the same way the refusal is — the widest window held during the current billing cycle.
  - `credit_balance_usd` number, required: Model credit left, in US dollars — the ledger's sum, less the part of a past cycle's allowance that lapsed. `0` for an account that has never been credited, and negative for one that has overspent, which refuses its managed generations.
  - `earned_balance_usd` number, required: Marketplace earnings: the publisher's share of fees its consumers funded, less withdrawals. Never lapses.
  - `paid_balance_usd` number, required: What may fund a marketplace fee: purchased credit plus earnings, less the usage included and granted credit did not cover and the fees paid. Included and granted credit never fund a fee, so a call to a priced installed tool or agent is refused with `402 insufficient_credit` while this is not positive.
  - `spend_reconciled_at` string, date-time, nullable, required: When this account's spend was last read off the meter, which is how current `credit_balance_usd` is. `null` when it has never been read for at least one of the account's projects, or the account has none. The same figure the account reads about itself at `GET /v1/users/me/billing`.
  - `storage` object, required: The account's indexed-storage pool, in the metered gigabytes the ceiling is enforced in — the raw file plus every chunk's text and its embedding vector, so not the size of what you uploaded. Pooled across every project the account pays for; per project, read the `gb_day` component of `GET /v1/projects/{project_id}/usage`. An ingest past `limit` is refused with `403 plan_limit_reached`, `resource: "storage"`, whose `details` carry the same `limit` and `used`.
    - `used` number, required: GB held across the account's projects.
    - `limit` number, nullable, required: GB the plan allows; null where a contract sets the ceiling, or where the plan sets none.
    - `sampled_at` string, date-time, nullable, required: When the **oldest** sample behind `used` was taken — the total is only as current as its stalest part, measured every few minutes per project rather than on this request. Null when a project has never been sampled, or there are none.

## Other responses

- `400`: The request was malformed or failed validation.
- `401`: Missing or invalid credentials.
- `403`: The caller does not hold the `admin` role.
- `404`: No such user.

---

[API](https://skmtc.dev/naturali/apis/naturali-ai-api.md) · [All operations](https://skmtc.dev/naturali/apis/naturali-ai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/naturali/apis/naturali-ai-api/revisions/e4905b78f422?raw)
