{"openapi":"3.1.0","info":{"title":"Modelglass Read API","version":"1.0.0","description":"Pricing-intelligence and comparison data for AI image-generation models. Public read API over the compiled registry + ontology artifact. Paid tiers (Starter/Pro) unlock full pricing history; the free tier sees the active price only.","license":{"name":"All rights reserved","url":"https://modelglass.com.au/terms"},"contact":{"name":"Modelglass","url":"https://modelglass.com.au"}},"servers":[{"url":"https://modelglass-api.vercel.app","description":"Production"},{"url":"http://localhost:8787","description":"Local dev"}],"tags":[{"name":"meta","description":"Service + artifact metadata"},{"name":"models","description":"Models, pricing, and capability data"},{"name":"providers","description":"Hosting providers"},{"name":"benchmarks","description":"Benchmark definitions"},{"name":"news","description":"Structured AI news feed (Pro tier)"},{"name":"billing","description":"Checkout, key lifecycle, portal, and webhooks"},{"name":"account","description":"Self-service account access via the magic-link flow"},{"name":"admin","description":"Internal ops diagnostics — ADMIN_SECRET-gated, not part of the public API"},{"name":"mcp","description":"Model Context Protocol transport"}],"security":[{"bearerAuth":[]}],"paths":{"/v1/health":{"get":{"tags":["meta"],"summary":"Health check","description":"Liveness probe. Public — no authentication required.","security":[],"responses":{"200":{"description":"Service is healthy.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"status":{"type":"string","example":"ok"}},"additionalProperties":true},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}}}}},"/v1/openapi.json":{"get":{"tags":["meta"],"summary":"This OpenAPI document","description":"The machine-readable OpenAPI 3.1 spec for this API. Public.","security":[],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/meta":{"get":{"tags":["meta"],"summary":"Artifact metadata","description":"Artifact version, build timestamp, content hash, and entity counts.","responses":{"200":{"description":"Artifact metadata.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","additionalProperties":true,"properties":{"artifact_version":{"type":"integer"},"built_at":{"type":"string","format":"date-time"},"content_hash":{"type":"string"},"counts":{"type":"object","additionalProperties":{"type":"integer"}}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/models":{"get":{"tags":["models"],"summary":"List models","description":"All tracked models with current pricing and technical profile. Free-tier keys see the active price only; Starter+ see full history.","parameters":[{"name":"provider","in":"query","description":"Filter to a provider slug, e.g. 'replicate'.","schema":{"type":"string"}},{"name":"unit","in":"query","description":"Filter to a billing unit.","schema":{"type":"string","enum":["per_image","per_megapixel","per_second","per_step","per_month","per_credit"]}},{"name":"status","in":"query","description":"Filter to a lifecycle status.","schema":{"type":"string","enum":["ga","preview","deprecated","retired"]}},{"name":"arch","in":"query","description":"Filter to an architecture vocabulary id, e.g. 'flow-matching'.","schema":{"type":"string"}}],"responses":{"200":{"description":"Matching models.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/ModelEntry"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/models/{modelId}":{"get":{"tags":["models"],"summary":"Get a model","description":"One model by its cross-host model id (may contain a slash).","parameters":[{"name":"modelId","in":"path","required":true,"description":"Cross-host model id, e.g. 'bfl/flux-1-schnell'.","schema":{"type":"string"}}],"responses":{"200":{"description":"The model.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/ModelEntry"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No model with that id (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/models/{modelId}/competitors":{"get":{"tags":["models"],"summary":"Get closest competitors","description":"Returns closest competitors for a model, enriched with live pricing data. Each competitor entry includes name, provider, current price, and a price_delta_ratio (competitor/source price, same unit only — null if units differ).","parameters":[{"name":"modelId","in":"path","required":true,"description":"Cross-host model id, e.g. 'bfl/flux-1-schnell'.","schema":{"type":"string"}}],"responses":{"200":{"description":"Enriched competitor list (empty array if none defined for this model).","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"model_id":{"type":"string"},"source_slug":{"type":"string","nullable":true},"competitors":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"step_distance":{"type":"integer","minimum":1,"maximum":3},"notes":{"type":"string","nullable":true},"model_id":{"type":"string","nullable":true},"model_name":{"type":"string","nullable":true},"provider":{"type":"string","nullable":true},"current_price":{"nullable":true,"type":"object","properties":{"amount":{"type":"number"},"currency":{"type":"string"},"unit":{"type":"string"}}},"price_delta_ratio":{"type":"number","nullable":true}}}}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No model with that id (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/providers":{"get":{"tags":["providers"],"summary":"List providers","responses":{"200":{"description":"All providers.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Provider"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/providers/{slug}":{"get":{"tags":["providers"],"summary":"Get a provider","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The provider.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/Provider"},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No provider with that slug (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/benchmarks":{"get":{"tags":["benchmarks"],"summary":"List benchmark definitions","responses":{"200":{"description":"All benchmark definitions.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Benchmark"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/news":{"get":{"tags":["news"],"summary":"List structured AI news entries","description":"Returns the curated news feed of AI model events (launches, price changes, deprecations, spec updates). Requires a Pro, Internal, or App plan key (the App plan is the ModelglassPro iOS app's own auto-provisioned, non-paid tier — SCO-174). Results are sorted reverse-chronologically.","parameters":[{"name":"modality","in":"query","schema":{"type":"string","enum":["image","llm","video","audio"]},"description":"Filter by model modality."},{"name":"type","in":"query","schema":{"type":"string","enum":["new_model","price_change","spec_update","deprecation","other"]},"description":"Filter by entry type."},{"name":"since","in":"query","schema":{"type":"string","format":"date"},"description":"Return entries on or after this date (YYYY-MM-DD)."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":250,"default":50},"description":"Maximum number of entries to return."}],"responses":{"200":{"description":"Structured news feed entries.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/NewsEntry"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Caller does not have a Pro, Internal, or App plan (FORBIDDEN).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/checkout/session":{"post":{"tags":["billing"],"summary":"Create a Stripe Checkout session","description":"Create a hosted Stripe Checkout session for a paid plan and return its URL. Public.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string","enum":["starter","pro"]},"successUrl":{"type":"string","format":"uri"},"cancelUrl":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"Checkout session created.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"url":{"type":"string","format":"uri"}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"400":{"description":"Invalid plan or missing fields (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Stripe error or misconfiguration (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/keys/session/{sessionId}":{"get":{"tags":["billing"],"summary":"Retrieve the key for a completed checkout","description":"One-time retrieval of the API key issued for a completed Stripe checkout session. The session id is the credential, so this route is public; the pending key is consumed on read (a second call 404s).","security":[],"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The issued key.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"apiKey":{"type":"string","example":"mg_starter_…"}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"404":{"description":"No key for this session — the webhook may still be processing (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"410":{"description":"This session's key has already been revealed and cannot be shown again (GONE).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/keys/provision":{"post":{"tags":["billing"],"summary":"Provision an API key","description":"Public bootstrap: `{ \"plan\": \"free\" }` issues an mg_free_… key with no payment; `{ \"plan\": \"app\" }` issues an mg_app_… key with no payment (auto-provisioned by the ModelglassPro iOS app — wider price-history window and /v1/news access, but not a paid tier). Any other plan falls through to an internal-only flow (requires an `internal` Bearer key).","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string","enum":["free","app","starter","pro"]},"label":{"type":"string","description":"Required for internal (non-free/app) provisioning."}}}}}},"responses":{"200":{"description":"Free or app-plan key issued (bare shape, not the data envelope).","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string","example":"mg_free_…"},"plan":{"type":"string","enum":["free","app"]},"rate_limit":{"type":"integer","example":100}}}}}},"201":{"description":"Key issued via the internal flow.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"Invalid plan/label (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Non-free/app plan without a valid key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Valid key but not `internal` (FORBIDDEN).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Free/app bootstrap limited to 20/day per origin (shared budget), or (internal flow) over the caller's plan rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/keys/validate":{"post":{"tags":["billing"],"summary":"Validate a raw API key","description":"Public — the key under test is presented in the body, not as a Bearer token (primary consumer: the iOS unlock screen, before the user has a Bearer credential to use). Always returns 200 and never reveals *why* a key is invalid (revoked / not found / malformed), to prevent key enumeration.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key"],"properties":{"key":{"type":"string"}}}}}},"responses":{"200":{"description":"Validity result.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["valid"],"properties":{"valid":{"type":"boolean"},"tier":{"type":"string","enum":["free","starter","pro","internal","app"]}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}}}}},"/v1/keys/list":{"get":{"tags":["billing"],"summary":"List all keys (internal only)","description":"Admin-style listing of every non-revoked key across all accounts. Requires an `internal`-plan Bearer key. Raw key strings are never returned.","responses":{"200":{"description":"All key records (masked).","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/KeySummary"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Valid key but not `internal` (FORBIDDEN).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/keys":{"get":{"tags":["billing"],"summary":"List the caller's own keys","description":"Self-service dashboard: lists every key on the same account as the caller's own Bearer key. Available to any authenticated plan.","responses":{"200":{"description":"The caller's key records (masked).","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"array","items":{"$ref":"#/components/schemas/KeySummary"}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Caller's key record not found (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["billing"],"summary":"Create an additional key on the caller's own account","description":"Self-service: adds another key to the caller's account at the same tier, subject to the tier's max-active-keys limit.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string"}}}}}},"responses":{"201":{"description":"Key created (raw key shown once).","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"apiKey":{"type":"string"},"keyId":{"type":"string"},"display":{"type":"string"},"tier":{"type":"string","enum":["free","starter","pro","internal","app"]},"label":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Caller's key record not found (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Account is at its tier's max-active-keys limit (KEY_LIMIT_EXCEEDED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/keys/{keyId}/roll":{"post":{"tags":["billing"],"summary":"Roll (rotate) a key","description":"Self-service: issues a new key and transitions the target key out of service — free plan revokes it instantly, starter/pro give it a grace period (default 24h; pro can configure up to 7 days via `gracePeriodHours`). The target key must belong to the caller's own account (a key may roll itself, or another key on the same account); otherwise 404 — this is exactly the 'I just leaked my key, roll it myself, one last time' path, and it never reveals whether a keyId belonging to a different account exists.","parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"gracePeriodHours":{"type":"number","description":"Pro/internal only; 400 on other tiers."}}}}}},"responses":{"201":{"description":"New key issued; the target key is rolled.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"apiKey":{"type":"string"},"keyId":{"type":"string"},"display":{"type":"string"},"tier":{"type":"string","enum":["free","starter","pro","internal","app"]},"createdAt":{"type":"string","format":"date-time"},"rolledKeyId":{"type":"string"},"rolledKeyStatus":{"type":"string","enum":["revoked","grace"]}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"400":{"description":"Missing keyId, target key not active, or gracePeriodHours set on a non-pro tier (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Caller's key record not found, or the target key doesn't exist / belongs to a different account (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/keys/{keyId}":{"delete":{"tags":["billing"],"summary":"Revoke (delete) a key","description":"Self-service: hard-revokes a key belonging to the caller's own account (a key may delete itself, or another key on the same account); 404 if it belongs to a different account or doesn't exist.","parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Key revoked.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"revoked":{"type":"boolean","enum":[true]},"keyId":{"type":"string"}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"400":{"description":"Key is already revoked (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, unknown, or revoked API key (UNAUTHORIZED / NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Caller's key record not found, or the target key doesn't exist / belongs to a different account (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error (INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/billing/portal":{"post":{"tags":["billing"],"summary":"Create a Stripe billing portal session","description":"Public — the magic `token` (from the emailed account link, see `/v1/auth/magic-link`) is the credential, not a Bearer key. Requires the Stripe Customer Portal to be configured in the Stripe Dashboard.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"}}}}}},"responses":{"200":{"description":"Billing portal session URL.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"url":{"type":"string","format":"uri"}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"400":{"description":"Missing `token` (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Token expired, already used, or unknown (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Stripe not configured, or a Stripe error (CONFIGURATION_ERROR / INTERNAL).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/auth/magic-link":{"post":{"tags":["account"],"summary":"Request a magic sign-in link","description":"Public. Looks up the Stripe customer by email and emails a 15-minute magic link to `/account`. Always returns `ok:true` regardless of whether the email has an account, to prevent email enumeration. Rate-limited per IP.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Request accepted, regardless of whether the email is known.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"sent":{"type":"boolean","enum":[true]}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"400":{"description":"Missing or invalid `email` (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many magic-link requests from this IP this hour (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/auth/account":{"get":{"tags":["account"],"summary":"View account key info via magic token","description":"Public — the magic `token` query param (from the emailed link) is the credential, not a Bearer key.","security":[],"parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Masked key info for the account.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"email":{"type":"string"},"keys":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"400":{"description":"Missing `token` query param (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Link expired, already used, or unknown (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/auth/regenerate":{"post":{"tags":["account"],"summary":"Roll a key via magic token","description":"Public — consumes the magic token (single-use for this action) and rolls the specified key, which must belong to the token's account.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","keyId"],"properties":{"token":{"type":"string"},"keyId":{"type":"string"}}}}}},"responses":{"201":{"description":"New key issued.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"apiKey":{"type":"string"},"keyId":{"type":"string"},"display":{"type":"string"},"tier":{"type":"string","enum":["free","starter","pro","internal","app"]},"planLabel":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"400":{"description":"Missing token/keyId, or the key isn't active (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Token expired/unknown, or key not found on this account (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Free-plan regeneration limit (5/day) reached (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/webhooks/stripe":{"post":{"tags":["billing"],"summary":"Stripe webhook receiver","description":"Receives Stripe events (checkout.session.completed, customer.subscription.deleted). Authenticates via the `Stripe-Signature` header over the raw request body — not an API key.","security":[],"parameters":[{"name":"Stripe-Signature","in":"header","required":true,"description":"Stripe webhook signature (HMAC over the raw body).","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"The raw Stripe event payload (verified byte-for-byte).","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Event received and acknowledged.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Missing or invalid Stripe signature (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/admin/kv-check":{"get":{"tags":["admin"],"summary":"KV connectivity diagnostic","description":"Internal ops diagnostic — confirms the Upstash KV env vars are set and a live read/write round-trip works. Not part of the public API; documented here only so the spec matches the router exactly.","security":[{"adminSecret":[]}],"responses":{"200":{"description":"KV diagnostic result.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"kv_url_set":{"type":"boolean"},"kv_token_set":{"type":"boolean"},"kv_ping":{"type":"string","enum":["ok","failed","not_configured"]},"test_write":{"type":"string","enum":["ok","failed","not_configured"]}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Invalid or missing admin secret (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/admin/keys/{keyId}":{"delete":{"tags":["admin"],"summary":"Hard-revoke any key (admin)","description":"Internal ops-only revoke by keyId, bypassing account-ownership checks. Idempotent — revoking an already-revoked key still returns 200. Not part of the public API.","security":[{"adminSecret":[]}],"parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Key revoked.","content":{"application/json":{"schema":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","enum":[true]},"data":{"type":"object","properties":{"revoked":{"type":"boolean","enum":[true]},"keyId":{"type":"string"}}},"meta":{"$ref":"#/components/schemas/ResponseMeta"}}}}}},"401":{"description":"Invalid or missing admin secret (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No key with that keyId (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/mcp":{"post":{"tags":["mcp"],"summary":"MCP transport (JSON-RPC)","description":"Stateless Model Context Protocol endpoint (JSON-RPC 2.0). Methods: initialize, ping, tools/list, tools/call. Gated by the same Bearer-key auth as /v1; tools read data as the caller's plan.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"type":["string","number","null"]},"method":{"type":"string","example":"tools/list"},"params":{"type":"object","additionalProperties":true}}}}}},"responses":{"200":{"description":"JSON-RPC result or error envelope.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"202":{"description":"Notification acknowledged (no id)."},"401":{"description":"Missing or invalid API key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Over the plan's daily rate limit (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key as a Bearer token: `Authorization: Bearer <key>`. Get a free key at https://modelglass.com.au/signup."},"adminSecret":{"type":"http","scheme":"bearer","description":"The `ADMIN_SECRET` env var as a Bearer token — NOT an API key, and not resolved via the key store. Internal ops use only."}},"schemas":{"ResponseMeta":{"type":"object","required":["artifact_version","built_at"],"properties":{"artifact_version":{"type":"integer"},"built_at":{"type":"string","format":"date-time"},"count":{"type":"integer","description":"Item count when `data` is a collection."}}},"ErrorResponse":{"type":"object","required":["ok","error"],"properties":{"ok":{"type":"boolean","enum":[false]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["NOT_FOUND","BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","RATE_LIMITED","CONFIGURATION_ERROR","GONE","KEY_LIMIT_EXCEEDED","INTERNAL"]},"message":{"type":"string"}}}}},"Price":{"type":"object","required":["amount","currency","unit","effective_from","source"],"properties":{"amount":{"type":"number"},"currency":{"type":"string","example":"USD"},"unit":{"type":"string","enum":["per_image","per_megapixel","per_second","per_step","per_month","per_credit"]},"region":{"type":"string"},"hardware":{"type":"string"},"effective_from":{"type":"string","format":"date"},"effective_to":{"type":"string","format":"date","description":"Absent while current."},"source":{"type":"object","required":["url","verified_at","method"],"properties":{"url":{"type":"string","format":"uri"},"verified_at":{"type":"string","format":"date"},"method":{"type":"string","enum":["manual","automated"]}}},"notes":{"type":"string"}}},"Tier":{"type":"object","required":["id","label","pricing"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"attributes":{"type":"object","additionalProperties":true},"pricing":{"type":"array","items":{"$ref":"#/components/schemas/Price"}}}},"Offering":{"type":"object","required":["slug","name","provider","model","tiers"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string","description":"Provider slug."},"model":{"type":"object","required":["id","creator","modality","status"],"properties":{"id":{"type":"string","example":"bfl/flux-1-schnell"},"creator":{"type":"string"},"family":{"type":"string"},"modality":{"type":"string"},"status":{"type":"string","enum":["ga","preview","deprecated","retired"]},"released":{"type":"string"}}},"tiers":{"type":"array","items":{"$ref":"#/components/schemas/Tier"}},"notes":{"type":"string"}},"additionalProperties":true},"ModelEntry":{"type":"object","description":"A model grouped across providers: the join of its pricing offerings and ontology knowledge (capability profile, benchmarks, architecture).","required":["model_id","name","offerings"],"properties":{"model_id":{"type":"string","example":"bfl/flux-1-schnell"},"name":{"type":"string"},"offerings":{"type":"array","items":{"$ref":"#/components/schemas/Offering"}},"knowledge":{"type":"object","additionalProperties":true}},"additionalProperties":true},"Provider":{"type":"object","required":["slug","name","url","pricing_page","status"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"url":{"type":"string","format":"uri"},"pricing_page":{"type":"string","format":"uri"},"status":{"type":"string","enum":["active","deprecated","retired"]},"notes":{"type":"string"}}},"Benchmark":{"type":"object","required":["id","name","summary","measures","score_range","limitations","url"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"summary":{"type":"string"},"measures":{"type":"array","items":{"type":"string"}},"category":{"type":"string"},"score_range":{"type":"object","required":["direction","description"],"properties":{"direction":{"type":"string","enum":["higher_is_better","lower_is_better"]},"min":{"type":"number"},"max":{"type":"number"},"unbounded":{"type":"boolean"},"unit":{"type":"string"},"description":{"type":"string"}}},"limitations":{"type":"array","items":{"type":"string"}},"url":{"type":"string","format":"uri"},"notes":{"type":"string"}}},"KeySummary":{"type":"object","description":"A masked key record — the raw key string is never returned. `GET /v1/keys/list` (internal) additionally exposes `accountId`; `GET /v1/keys` (self-service) additionally exposes `maxKeys`.","required":["keyId","display","tier","status"],"properties":{"keyId":{"type":"string"},"display":{"type":"string","example":"mg_pro_…ab12"},"tier":{"type":"string","enum":["free","starter","pro","internal","app"]},"label":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","grace","revoked","suspended"]},"formatVersion":{"type":"integer"},"isLegacyFormat":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time","nullable":true},"lastUsedAt":{"type":"string","format":"date-time","nullable":true}},"additionalProperties":true},"NewsEntry":{"type":"object","required":["date","title","type","modality","provider","model_slug","summary","source_url","impact"],"properties":{"date":{"type":"string","format":"date"},"title":{"type":"string"},"type":{"type":"string","enum":["new_model","price_change","spec_update","deprecation","other"]},"modality":{"type":"string","enum":["image","llm","video","audio"]},"provider":{"type":"string"},"model_slug":{"type":"string"},"summary":{"type":"string"},"source_url":{"type":"string","format":"uri"},"impact":{"type":"string","enum":["low","medium","high"]}}}}}}