Skip to content

Self-serve plan config API

The self-serve plan/tier definitions — the model allowlist, cross-vendor fallback, monthly budget, cap-soft-degrade config, scheduled-tasks cap and seat cap — are stored in the database and editable through this endpoint. Prices are not part of this config: what a plan costs is the product catalog, which mirrors Stripe and is the only price authority. The trial credit a new trial workspace receives lives in the catalog too (trial_credit_eur on the trial row). Changing a plan takes effect immediately: the write invalidates the cached configuration across all workers, and the next request reads the new values.

These definitions used to be deployment settings; they were migrated into the database (one row per plan) and back-filled from the previous values on first boot, so behaviour is unchanged across the cutover. Secrets are never part of this config: the Stripe secret key and webhook secret, and the self-serve provider-key pool binding, are not editable here.

Custom plan models. Migration 0327 sets the self_serve_custom model list to claude-haiku-4-5, claude-sonnet-5, qwen3.8-27b, gemma-4-31b-it, mistral-small-4 on every environment (before it, the paid plan's list was empty on prod and beta, so every model was refused). The Myra-hosted entries are usable only with the EU-Gov add-on — see Tenant entitlements. Which models are Myra-hosted is decided by the provider (myra), not by this list; to offer a new model, add its id here.

Base URL: https://<your-gateway-host>/admin/v1

Every endpoint requires the platform admin role; a lower role is 403, an unauthenticated caller 401. Access is re-gated server-side — the client is never the authorization boundary.

List plans

GET /admin/v1/plan-config

Returns one object per configured plan:

[
  {
    "plan": "self_serve_custom",
    "budget_usd": 15,
    "models": ["claude-3-5-haiku-20241022", "gpt-4o-mini"],
    "efficient_models": ["claude-3-5-haiku-20241022"],
    "fallback": "myra:qwen3.8-27b",
    "degrade_provider": "openai",
    "degrade_model": "claude-3-5-haiku-20241022",
    "degrade_multiplier": 2.0,
    "scheduled_tasks": 10,
    "max_seats": 500,
    "updated_at": 1750000000,
    "updated_by": "<admin-user-id>"
  }
]

Updating a plan

PUT /admin/v1/plan-config

Upserts the plan named in the body. The change is audited (plan_config / update in the audit log) and takes effect immediately (the config cache is invalidated on success).

Model picker copy follows automatically (AGF-2930). The short model lines a self-serve user sees in the picker are written for the set of models the workspace is offered (the plan's models intersected with the EU-Gov add-on gate), relative to each other — e.g. which model is the cheapest or the only one that sees images. Changing models changes the set, so the next picker load generates copy for the new set; nothing needs to be edited by hand. See Tagline resolution and the kill switch plan_model_copy_enabled.

Accepted body

Field Type Rule
plan string Required. Must be self_serve_custom or self_serve_trial.
budget_usd number Required. >= 0.
models array of strings Required (may be empty). Exact model_price.model ids the plan may use — bare model ids, not provider:model. Every id is checked against the model catalog on write (see below).
efficient_models array of strings Optional (default []). The over-cap efficient subset; entries outside models are dropped when the config is read (the degrade set can never widen the plan).
fallback string or null Optional. provider:model (e.g. myra:qwen3.8-27b) or null to disable the plan's cross-vendor failover. The provider must be one a self-serve gateway can actually be keyed for — see the rejection rule below.
degrade_provider / degrade_model string or null Optional; both or neither. The cap-soft-degrade swap target.
degrade_multiplier number or null Optional. Must be in (1.0, 10.0]. null disables the secondary hard cap (disarms degrade).
scheduled_tasks integer Required. >= 0 (0 disables scheduled tasks — used by the self_serve_trial plan). Max active scheduled prompt-tasks per user.
max_seats integer Required. 1–10000. Max active (non-deleted) users a self-serve tenant on this plan may have; the invite path rejects over-cap adds with 403 seat_limit_reached.
(retired) — The former Stripe price-id fields and the trial credit are retired: prices come only from the product catalog, and the trial credit is edited there. A body that carries one of them with null (or not at all) is accepted and the field ignored — so a browser tab opened before the change keeps saving; a non-null value is refused (below). The GET no longer returns them.

Rejected (fail closed — nothing is persisted)

Every rejection is a 400 unless noted; on rejection the row is not written:

  • plan missing or not one of the two known plans (self_serve_custom, self_serve_trial).
  • budget_usd missing, non-numeric, or negative.
  • scheduled_tasks missing, non-integer, or < 0.
  • max_seats missing, non-integer, < 1, or > 10000.
  • models (or efficient_models) not a JSON array, or containing a non-string / empty entry.
  • models (or efficient_models) naming a model the gateway does not know — no model_price row for that id. A plan can only entitle a model that exists, because the model allowlist is fail-closed: an unknown id is not "ignored", it is simply never offered or routed, and nothing says why.
  • models (or efficient_models) naming a deprecated model (model_price.deprecated_at set). Entitling one denies it to every tenant on that tier.
  • models (or efficient_models) naming a model served by the platform key pool (provider anthropic) that has no price (both input_per_1k and output_per_1k are 0). Such a model would be served on Myra's own key while cost calculation prices it at 0, so spend would never be metered and the tenant budget would never be reached.
  • fallback present but not provider:model.
  • fallback naming a provider a self-serve gateway can never hold a key for. Self-serve tenants have no provider_config rows at all, so only two kinds of provider can carry their traffic: a platform-managed keyless provider (e.g. myra), or anthropic when the managed key pool is wired. Anything else is refused, because the cross-vendor failover swap would otherwise convert a key-pool outage into 424 provider_key_missing — telling a customer to add a provider key in gateway settings they do not have, for a vendor they never chose. Checked only when the value changes: this endpoint rewrites every column, so a row written before this rule keeps saving (an edit that only changes budget_usd is not blocked by a legacy fallback). Set it to null to clear one. A legacy unservable value is reported once per boot as a cross-vendor FAILOVER DISARMED warning, and at request time the failover is skipped and the turn fails closed with a retryable 503 provider_quota_exhausted rather than the misdirected 424.
  • only one of degrade_provider / degrade_model set.
  • degrade_multiplier present but not in (1.0, 10.0].
  • fallback's model half, and degrade_model, must pass a catalog check — an unknown or deprecated model is rejected, and the target must be priced (a 0/0 price is rejected for any provider here, unlike the entitlement list; see the zero-price note below) — and degrade_provider must be a servable self-serve provider just like fallback's provider. When any of these validations cannot read the model catalog, the write returns 503 (not 400).
  • a retired field with a value (absent, null and "" count as not sent and are ignored) → 400 {"error":"field_retired","field":"<name>","message":"The field '<name>' was removed. Reload the page."}. Retired: stripe_price_monthly, stripe_price_annual, trial_credit_eur.
  • a second plan-config edit is already in progress → 409 (retry).

Two different zero-price rules apply, by role:

  • In the entitlement list (models / efficient_models), a zero price is only rejected for pool-served models. Locally hosted models (provider myra) are legitimately zero-priced and stay entitleable — a blanket "must be billable" rule would reject every future edit of a plan that includes one, including an edit that only changes the budget or the seat cap, because this endpoint rewrites every column.
  • As a swap target (fallback's model half or degrade_model), a 0/0 price is rejected for every provider, including myra: the platform silently swaps traffic onto this target during an outage, so an unpriced target would be unmetered and the tenant budget would never be reached. So a locally hosted model that is fine to entitle at zero price cannot be used as the fallback/degrade target unless it is priced.

If the model catalog cannot be read, the request is rejected with 503 (retryable) rather than 400: the submitted model list may be perfectly valid and the database merely unreachable.

An empty models array remains valid and means deny all — a deliberate fail-safe state, not an error.

Prices are not stored here: signup and conversion checkout charge the plan's catalog slot (the component price the product catalog resolves from its Stripe mirror). When the catalog cannot resolve a price for the chosen plan and interval, checkout answers 503 checkout_unavailable rather than billing a wrong or missing price — never a silent mis-bill.