Admin Credits
GET /api/admin/credit-rate-card
Section titled “GET /api/admin/credit-rate-card”Get Credit Rate Card
Every published version of the card, plus the live margin readout.
margins is what makes credits_per_usd safe to edit: it is one number
that reprices the whole product, and the only way to see what it does is
against the price points the plans actually sell at.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | CreditRateCardResponse |
| 422 | Validation Error | HTTPValidationError |
PUT /api/admin/credit-rate-card
Section titled “PUT /api/admin/credit-rate-card”Publish Credit Rate Card
Publish a new card version. Existing versions are never mutated.
Appends to the stored history rather than replacing it, so every burn
receipt that cites an older card_version still reconstructs to the same
number and re-deriving history from the ledger keeps reconciling (plan §3.1,
§13.1 scenario 11). with_card replaces by effective_from, so
re-publishing the same date is a correction of an unstarted version rather
than a second entry — publishing a change means a new date.
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| credits_per_usd | number | string | yes | |
| effective_from | string (date) | yes | |
| rules | CreditRuleInput[] | no | |
| version | integer | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | CreditRateCardResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/mode-presets/preview
Section titled “POST /api/admin/mode-presets/preview”Preview Mode Presets
Burn ratios the submitted presets would produce, without saving them.
Plan §7.4: re-pointing mode_normal at a dearer model changes every
credit org’s burn rate at once, and because normal is the denominator of
every ratio, re-pointing it rescales all of them. The save path in
admin_ai.py already writes its own audit row with the resulting ratios;
what was missing is seeing them before committing, which is this.
Read-only, so it deliberately writes no audit row — the save does.
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| mode_high_effort | object | no | |
| mode_normal | object | no | |
| mode_trivial | object | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ModePresetPreviewResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/orgs/{org_id}/credits
Section titled “GET /api/admin/orgs/{org_id}/credits”Get Org Credits
Balance, subscription and resolved entitlements for one org.
reconciled is the invariant the whole ledger design rests on: the cached
balance — both buckets summed — must equal the sum of every ledger row.
False means something wrote credit_balances without a ledger row, which
is a bug worth chasing before believing any other number on the screen.
Do not read purchase_total as the target for purchased_balance. It is
what the org has bought; the bucket is what is left of it after burns spilled
into it, and the two are equal only for an org that has bought and not spent.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | OrgCreditsResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/orgs/{org_id}/credits/adjust
Section titled “POST /api/admin/orgs/{org_id}/credits/adjust”Adjust Org Credits
Move an org’s balance by a signed delta, with a mandatory reason.
Writes two records, and both matter: a credit_ledger row carrying
the actor and the reason (the money, and what keeps
balance == SUM(delta) true) and an admin_audit_log row (the admin
action). Plan §13.1 scenario 12 asserts both — a mutating admin call with no
audit row is a finding regardless of whether the ledger is right.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| delta | number | string | yes | Signed movement in credits. Negative debits the org. |
| reason | string | yes | Why this adjustment was made. Mandatory: it lands on both the ledger row and the audit row, and an unexplained manual movement is indistinguishable from a bug later. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | AdjustCreditsResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/orgs/{org_id}/credits/forensics
Section titled “GET /api/admin/orgs/{org_id}/credits/forensics”Org Credit Forensics
Burn drill-down for one org, plus the reconciliation check.
The buckets are windowed; ledger_total and reconciled are not.
That split is deliberate — a drill-down answers “where did this month go”,
while reconciliation is an all-time property (balance + purchased_balance
must equal the sum of every row), and computing it over a window would make
it pass by accident. Plan §13.1 scenario 12 asserts the view sums to the
ledger.
Every bucket carries dollars alongside credits, read from the burn receipts rather than divided back out of the credits. The two are not the same question — credits are what the org was charged, dollars are what the usage cost us — and having both on one row is what makes a multiplier’s effect visible without opening the model catalog.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| days | query | integer | no | Window size, ending now. |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ForensicsResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/orgs/{org_id}/reset-baseline
Section titled “POST /api/admin/orgs/{org_id}/reset-baseline”Reset Org Baseline
Put an org back on a known plan, balance, floor, overrides and period.
Orgs cannot be deleted, so this is teardown for the credits e2e sweep’s persistent fixture orgs (plan §13.1). Without it they drift and a later run passes for the wrong reason — a scenario asserting “the gate refuses” trivially succeeds against an org somebody left at zero.
The balance lands via an adjustment, never an UPDATE credit_balances: a hand-written balance write leaves
balance != SUM(credit_ledger.delta) and silently disables the
reconciliation check every later run depends on.
Idempotent in the sense that matters — the second call finds the org already at baseline, computes a zero delta and writes no ledger row. It is not “no writes at all”: the ledger is append-only, so a reset that does move something adds to it rather than erasing history.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| plan_code | string | yes | |
| reason | string | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ResetBaselineResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/orgs/{org_id}/roll-period
Section titled “POST /api/admin/orgs/{org_id}/roll-period”Roll Org Period
Run the period close for one org, optionally forcing the boundary.
Two very different operations behind one endpoint:
- Un-forced is just running the scheduled job early, narrowed to this org. If the period has not ended the org is not due and nothing happens, so it is safe anywhere and needs no gate.
- Forced pulls
period_endback to now first, which expires a live balance and issues the next grant ahead of schedule. That is real money moving on a schedule nobody agreed to, so it is refused unlessCREDITS_FORCE_PERIOD_ROLL_ENABLEDis set — non-prod only, and default-deny so a fresh deployment is production-shaped. Staging turns it on for plan §13.1 scenarios 5 and 8, which cannot otherwise observe a rollover without waiting a month.
The close itself is the period job, unchanged — it opens its own session per org and commits there, so this handler commits its own work first.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| force | boolean | no | Pull period_end back to now first, so the close runs even though the period has not ended. Non-prod only — this expires a live balance and grants early. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | RollPeriodResponse |
| 422 | Validation Error | HTTPValidationError |
PUT /api/admin/orgs/{org_id}/subscription
Section titled “PUT /api/admin/orgs/{org_id}/subscription”Update Org Subscription
Change an org’s plan, its entitlement overrides, or its period boundary.
A plan change goes through change_plan, which expires the outgoing
balance and grants the new plan’s allocation — once ever per org per plan,
so an org returning to a plan it has already had gets nothing and must be
topped up with an adjustment instead.
Overrides are tri-state on the wire: a field omitted from overrides is
left as it is, an explicit null clears it back to inheriting the plan,
and a value sets it. Sending overrides: {} therefore changes nothing —
clearing them all means naming them all, or using reset-baseline.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| override_reason | string | no | |
| overrides | EntitlementOverrides | no | |
| period_end | string (date-time) | no | |
| plan_code | string | no | |
| status | string | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | OrgCreditsResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/plans
Section titled “GET /api/admin/plans”List Plans
Every plan, with its price point, margin and subscriber count.
The fields list is the entitlement set, derived from
ENTITLEMENT_FIELDS. The editor renders from it rather than from a
hand-written list, so a new entitlement cannot exist on the server and be
missing from the screen that is supposed to set it.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | PlanListResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/plans
Section titled “POST /api/admin/plans”Create Plan
Create a plan. code must be unique.
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| audit_log | boolean | no | |
| byo_keys | boolean | no | |
| code | string | yes | |
| direct_model_choice | boolean | no | |
| enforced_idp_sso | boolean | no | |
| enforces_credits | boolean | no | |
| hitl_routing | boolean | no | |
| initial_credits | number | string | no | |
| intake_classifier_calls_per_day | integer | no | |
| intake_messages_per_day | integer | no | |
| is_enterprise | boolean | no | |
| max_agents | integer | no | |
| max_concurrent_chats | integer | no | |
| max_custom_environments | integer | no | |
| max_custom_image_gb | integer | no | |
| max_custom_models | integer | no | |
| max_inboxes | integer | no | |
| max_integrations | integer | no | |
| max_queue_depth | integer | no | |
| max_seats | integer | no | |
| monthly_credits | number | string | no | |
| name | string | yes | |
| overdraft_pct | number | string | no | |
| per_agent_budgets | boolean | no | |
| price_currency | string | no | ISO 4217 alpha-3 the advertised price is quoted in. Set it to whatever the plan’s Polar product is priced in. |
| price_display | number | string | no | |
| renews | boolean | no | |
| shared_blocks | boolean | no | |
| sort_order | integer | no |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Successful Response | PlanResponse |
| 422 | Validation Error | HTTPValidationError |
PUT /api/admin/plans/{plan_id}
Section titled “PUT /api/admin/plans/{plan_id}”Update Plan
Replace a plan’s figures and entitlements.
Takes effect on the next entitlement read: EntitlementService caches
per instance and an instance lives for one request, so no running request is
retroactively re-permissioned and the one after this sees the new values.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| plan_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| audit_log | boolean | no | |
| byo_keys | boolean | no | |
| code | string | yes | |
| direct_model_choice | boolean | no | |
| enforced_idp_sso | boolean | no | |
| enforces_credits | boolean | no | |
| hitl_routing | boolean | no | |
| initial_credits | number | string | no | |
| intake_classifier_calls_per_day | integer | no | |
| intake_messages_per_day | integer | no | |
| is_enterprise | boolean | no | |
| max_agents | integer | no | |
| max_concurrent_chats | integer | no | |
| max_custom_environments | integer | no | |
| max_custom_image_gb | integer | no | |
| max_custom_models | integer | no | |
| max_inboxes | integer | no | |
| max_integrations | integer | no | |
| max_queue_depth | integer | no | |
| max_seats | integer | no | |
| monthly_credits | number | string | no | |
| name | string | yes | |
| overdraft_pct | number | string | no | |
| per_agent_budgets | boolean | no | |
| price_currency | string | no | ISO 4217 alpha-3 the advertised price is quoted in. Set it to whatever the plan’s Polar product is priced in. |
| price_display | number | string | no | |
| renews | boolean | no | |
| shared_blocks | boolean | no | |
| sort_order | integer | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | PlanResponse |
| 422 | Validation Error | HTTPValidationError |
DELETE /api/admin/plans/{plan_id}
Section titled “DELETE /api/admin/plans/{plan_id}”Delete Plan
Delete a plan. Refused while any org is subscribed to it.
409 rather than a cascade: the subscription’s plan_id is what every
entitlement read resolves through, so removing the plan under a live
subscriber turns that org UNSUBSCRIBED — everything denied — from a
click that looked like tidying up.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| plan_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 204 | Successful Response | |
| 422 | Validation Error | HTTPValidationError |