openapi: 3.1.0
info:
  title: Archstone — public API surface
  version: "2.28.0"
  description: >-
    The public, unauthenticated surface of the Archstone console: discovering what a deployment
    can do, and verifying an OpenAPI document for free. Every operator-, collector-, and
    platform-scoped route is deliberately absent from this document — and so is the demo tourism
    search: its contract lives with the demo Worker in a separate repository this document does
    not control.
security: []
servers:
  - url: https://console.archstone.dev
    description: The hosted Archstone console and its control-plane API.
paths:
  /api/capabilities:
    get:
      operationId: getCapabilities
      summary: Discover this deployment's feature flags and version
      security: []
      responses:
        "200":
          description: This deployment's version, feature flags, auth mode, and configured limits.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Capabilities"
              example:
                version: "1.1.2"
                build: "v1.1.2"
                features:
                  multi_tenant: false
                  billing: false
                  offline_import: true
                  hosted_runtime: false
                  endpoint_listing: false
                  registry_publishing: false
                notifications:
                  webhook: false
                  smtp: false
                auth:
                  adapter: local
                  kind: credential
                  productionReady: true
                  notice: null
                retention:
                  defaultDays: 90
                  enforcementImplemented: true
                limits:
                  exportMaxRows: 5000
                hosted: null
  /api/v1/verify-openapi:
    post:
      operationId: verifyOpenapi
      summary: Submit an OpenAPI document address for a free, one-time verification run
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyOpenapiRequest"
            example:
              url: "https://example.com/openapi.yaml"
      responses:
        "200":
          description: The verification report — which operations are included, and the outcome of each live read attempted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicVerifyReport"
              example:
                verifiedAt: "2026-09-19T12:00:00.000Z"
                source:
                  url: "https://example.com/openapi.yaml"
                  sha256: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a0"
                  unresolvedReferences: []
                api:
                  name: "Example API"
                  serverAddress: "https://api.example.com"
                contractFingerprint: "sha256:c0ffee"
                summary:
                  operations: 2
                  included: 1
                  excludedWrites: 1
                  excludedIrreversible: 0
                  excludedOther: 0
                  livePassed: 1
                  liveFailed: 0
                  liveNotAttempted: 0
                documentNotes: []
                operations:
                  - key: "GET /v1/things"
                    method: "GET"
                    path: "/v1/things"
                    summary: "List things"
                    effect: read
                    decision:
                      included: true
                    authentication: none
                    requiredInputs: 0
                    requiredInputsWithExample: 0
                    live:
                      status: passed
                  - key: "POST /v1/things"
                    method: "POST"
                    path: "/v1/things"
                    summary: "Create a thing"
                    effect: write
                    decision:
                      included: false
                      reason: write
                    authentication: none
                    requiredInputs: 0
                    requiredInputsWithExample: 0
                    live: null
        "400":
          description: The `url` field was missing or not a URL this route may fetch.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvalidUrlRefusal"
        "404":
          description: This deployment does not carry the "Verify my API" feature.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotFoundRefusal"
        "422":
          description: >-
            The document could not be read (`error: "source_unreadable"`, `reason` one of
            `FetchSourceError`'s members), was not an OpenAPI document (`error:
            "document_not_openapi"`), or had nothing a public reader could verify (`error:
            "nothing_verifiable"`) — three distinct causes sharing this status code, each with
            its own example below.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyOpenapi422"
              examples:
                source_unreadable:
                  summary: The document could not be read.
                  value: { status: 422, error: source_unreadable, reason: host_not_allowed }
                document_not_openapi:
                  summary: The address did not resolve to an OpenAPI document.
                  value: { status: 422, error: document_not_openapi, reason: "not-yaml-or-json" }
                nothing_verifiable:
                  summary: The document had no operation a public reader could verify.
                  value: { status: 422, error: nothing_verifiable, reason: no_read_operation }
        "429":
          description: This remote address has run too many verifications in the current window.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitedRefusal"
        "503":
          description: This deployment is already running its configured maximum of concurrent verifications.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BusyRefusal"
  /api/v1/listings:
    get:
      operationId: listPublicListings
      summary: Every publicly listed hosted endpoint
      tags: [Listings]
      security: []
      responses:
        "200":
          description: Every visible listing (approved, or approved and under escalation re-review), minimal fields only.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PublicListing"
              example:
                - slug: "acme-weather"
                  displayName: "Acme Weather"
                  description: "Forecasts and current conditions for any city."
                  website: "https://acme.example.com"
                  logoRef: "data:image/png;base64,iVBORw0KGgo="
                  accessMode: public
                  capabilities:
                    - name: "get_forecast"
                      effect: read
        "404":
          description: This deployment does not carry multi-tenant hosting (self-hosted).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotFoundRefusal"
        "429":
          description: This remote address has read too many times in the current window.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitedRefusal"
  /api/v1/listings/{slug}:
    get:
      operationId: getPublicListing
      summary: One publicly listed hosted endpoint, by slug
      tags: [Listings]
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The listing's public fields.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicListing"
              example:
                slug: "acme-weather"
                displayName: "Acme Weather"
                description: "Forecasts and current conditions for any city."
                website: "https://acme.example.com"
                logoRef: "data:image/png;base64,iVBORw0KGgo="
                accessMode: public
                capabilities:
                  - name: "get_forecast"
                    effect: read
        "404":
          description: >-
            Not found — indistinguishable across every cause: self-hosted (no multi-tenant),
            no such slug, delisted, withdrawn, declined, or a suspended/removed deployment. An
            unauthenticated caller must not learn which reason applied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotFoundRefusal"
        "429":
          description: This remote address has read too many times in the current window.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitedRefusal"
  /api/v1/pricing:
    get:
      operationId: getPublicPricing
      summary: The plan ladder's public list prices, unauthenticated
      tags: [Pricing]
      security: []
      responses:
        "200":
          description: >-
            The money-facing per-plan fields GET /api/v1/plans also returns (name, displayPrice,
            amount, prices, retentionDaysMax), built by the identical shared assembly so those
            cannot drift, plus the largest crypto saving across every priced plan and period.
            Each plan also carries the same card figures GET /api/v1/plans returns, from the same
            assembly: `allowance` (the plan's own hosting allowance), `support`, and — Starter
            only — `hostingFee`, and — Starter and Growth only — `creditPacks`. `includedCalls` is
            the base plan's ingest fair-use cap; hosting capacity lives in `allowance.runtime` and
            `hostingFee.runtime`, so the top-level `runtime` is always null.
          headers:
            Cache-Control:
              schema:
                type: string
              description: "public, max-age=<configured cache lifetime, default 120>"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicPricing"
              example:
                plans:
                  - name: free
                    displayPrice: "$0"
                    amount: { amountCents: 0, currency: USD }
                    prices: null
                    includedCalls: 50000
                    retentionDaysMax: 30
                    runtime: null
                    allowance: { publishedHostedDeployments: 1, hostedCallPool: 5000, runtime: { concurrency: 2, requestsPerMinute: 60 } }
                    support: { channel: community, firstResponseBusinessDays: null }
                  - name: starter
                    displayPrice: "$29 / month"
                    amount: { amountCents: 2900, currency: USD }
                    prices:
                      card: [{ months: 1, amountCents: 2900 }, { months: 6, amountCents: 16530 }, { months: 12, amountCents: 29580 }]
                      crypto: [{ months: 1, amountCents: 2900 }, { months: 3, amountCents: 8700 }, { months: 6, amountCents: 15660 }, { months: 12, amountCents: 27840 }]
                    includedCalls: 6000000
                    retentionDaysMax: 90
                    runtime: null
                    allowance: { publishedHostedDeployments: 1, hostedCallPool: 5000, runtime: { concurrency: 2, requestsPerMinute: 60 } }
                    support: { channel: email, firstResponseBusinessDays: 2 }
                    hostingFee:
                      prices:
                        card: [{ months: 1, amountCents: 500 }, { months: 6, amountCents: 2850 }, { months: 12, amountCents: 5100 }]
                        crypto: [{ months: 1, amountCents: 500 }, { months: 3, amountCents: 1500 }, { months: 6, amountCents: 2700 }, { months: 12, amountCents: 4800 }]
                      hostedCallPool: 100000
                      runtime: { concurrency: 10, requestsPerMinute: 600 }
                    creditPacks:
                      - { sku: credit_pack_1m, credits: 1000000, prices: { card: 1000, crypto: 1000 } }
                      - { sku: credit_pack_5m, credits: 5000000, prices: { card: 4500, crypto: 4500 } }
                  - name: growth
                    displayPrice: "$199 / month"
                    amount: { amountCents: 19900, currency: USD }
                    prices:
                      card: [{ months: 1, amountCents: 19900 }, { months: 6, amountCents: 113430 }, { months: 12, amountCents: 202980 }]
                      crypto: [{ months: 1, amountCents: 19900 }, { months: 3, amountCents: 59700 }, { months: 6, amountCents: 107460 }, { months: 12, amountCents: 191040 }]
                    includedCalls: 10000000
                    retentionDaysMax: 365
                    runtime: null
                    allowance: { publishedHostedDeployments: 25, hostedCallPool: 1000000, runtime: { concurrency: 50, requestsPerMinute: 3000 } }
                    support: { channel: priority, firstResponseBusinessDays: 1 }
                    workspaceCap: 5
                    creditPacks:
                      - { sku: credit_pack_1m, credits: 1000000, prices: { card: 800, crypto: 800 } }
                      - { sku: credit_pack_5m, credits: 5000000, prices: { card: 3600, crypto: 3600 } }
                  - name: enterprise
                    displayPrice: Negotiated
                    amount: null
                    prices: null
                    includedCalls: null
                    retentionDaysMax: 2555
                    runtime: null
                    allowance: null
                    support: { channel: contract, firstResponseBusinessDays: null }
                cryptoSavingPercent: 20
        "404":
          description: This deployment does not have billing on (self-hosted, or a hosted deployment with billing off).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotFoundRefusal"
        "429":
          description: >-
            This remote address has read too many times in the current window — the SAME shared
            counter GET /api/v1/listings uses (public_listings_rate_events), not an independent one.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitedRefusal"

components:
  schemas:
    # ------------------------------------------------------------------------------------------
    # GET /api/capabilities
    # ------------------------------------------------------------------------------------------
    Capabilities:
      type: object
      required: [version, build, features, billing, notifications, auth, retention, limits, hosted]
      additionalProperties: false
      properties:
        version:
          type: string
          description: The console/API compatibility handshake version (BR-37) — never a build identifier.
        build:
          type: string
          description: The tag or commit this image was built from.
        features:
          type: object
          required: [multi_tenant, billing, offline_import, hosted_runtime, endpoint_listing, registry_publishing]
          additionalProperties: false
          properties:
            multi_tenant: { type: boolean }
            billing: { type: boolean }
            offline_import: { type: boolean }
            hosted_runtime: { type: boolean }
            endpoint_listing:
              type: boolean
              description: True only when both multi_tenant and hosted_runtime are true.
            registry_publishing:
              type: boolean
              description: >-
                True only when endpoint_listing is true and a registry signing key is configured
                and readable at boot.
        billing:
          type: object
          required: [methods, freePeriod]
          additionalProperties: false
          description: >-
            Which billing providers this deployment can actually charge through. Distinct from
            features.billing, which only says whether billing is on at all.
          properties:
            methods:
              type: array
              items:
                type: string
                enum: [card, crypto]
            freePeriod:
              description: >-
                Billing is visible and self-serve-choosable before any payment rail is live,
                gated by this runtime setting rather than compiled in. null when
                BILLING_PAYMENTS_OPEN_AT is unset — unchanged behavior for a deployment that never
                sets it.
              oneOf:
                - type: "null"
                - type: object
                  required: [opensAt, graceEndsAt]
                  additionalProperties: false
                  properties:
                    opensAt: { type: string, description: "ISO datetime checkout/downgrade/portal open at." }
                    graceEndsAt: { type: string, description: "ISO datetime; opensAt + BILLING_GRACE_DAYS." }
        notifications:
          type: object
          required: [webhook, smtp]
          additionalProperties: false
          properties:
            webhook: { type: boolean }
            smtp: { type: boolean }
        auth:
          type: object
          required: [adapter, kind, productionReady]
          additionalProperties: false
          properties:
            adapter: { type: string }
            kind:
              type: string
              enum: [credential, redirect]
            productionReady: { type: boolean }
            notice:
              type: [string, "null"]
        retention:
          type: object
          required: [defaultDays, enforcementImplemented]
          additionalProperties: false
          properties:
            defaultDays: { type: integer }
            enforcementImplemented: { type: boolean }
        limits:
          type: object
          required: [exportMaxRows]
          additionalProperties: false
          properties:
            exportMaxRows: { type: integer }
        hosted:
          oneOf:
            - type: "null"
            - type: object
              required: [runtimeUrl]
              additionalProperties: false
              properties:
                runtimeUrl: { type: string }
        legal:
          description: >-
            The legal documents' current versions and the material changes announced, decided on the
            server's clock. null unless this deployment is multi-tenant and every document is
            published. Additive (1.4.0): absent from a response of an earlier release.
          oneOf:
            - type: "null"
            - type: object
              required: [currentVersions, upcoming]
              additionalProperties: false
              properties:
                currentVersions:
                  type: object
                  required: [privacy, terms, dpa]
                  additionalProperties: false
                  properties:
                    privacy: { type: string }
                    terms: { type: string }
                    dpa: { type: string }
                upcoming:
                  type: array
                  description: Material changes announced and not yet in force; non-material ones are omitted.
                  items:
                    type: object
                    required: [document, version, effectiveAt, changeSummary, urgent]
                    additionalProperties: false
                    properties:
                      document: { type: string, enum: [privacy, terms, dpa] }
                      version: { type: string }
                      effectiveAt: { type: string, description: "ISO instant (UTC) the version takes effect." }
                      changeSummary: { type: string, description: "English, as published." }
                      urgent: { type: boolean, description: "True when it takes effect on shorter notice than usual." }

    # ------------------------------------------------------------------------------------------
    # POST /api/v1/verify-openapi
    # ------------------------------------------------------------------------------------------
    VerifyOpenapiRequest:
      type: object
      required: [url]
      additionalProperties: false
      properties:
        url:
          type: string
          description: The address of the OpenAPI document to verify. Fetched server-side; never a value the visitor's browser calls directly.

    Decision:
      description: >-
        Whether an operation is included in a run, and why not when it isn't. `reason` and `code`
        are present only when `included` is `false`; `code` only for `reason: "not_usable"`. Not
        expressed as a `oneOf` — two disjoint shapes picked apart only by a boolean would be
        polymorphism for its own sake, and every field below is already closed by its own enum.
        This schema validates field types and enum membership; it does not enforce that `reason`
        (or `code`) is present only when `included: false` — treat `included` as authoritative.
      type: object
      required: [included]
      additionalProperties: false
      properties:
        included:
          type: boolean
        reason:
          type: string
          enum: [write, irreversible, method_hint_contradiction, not_usable, unclassified]
        code:
          type: string
          description: Present only for reason "not_usable" — the document-side code explaining why the operation cannot be used at all.

    LiveOutcome:
      description: >-
        The outcome of the one live call attempted for an included, read-shaped operation. Which
        of `degraded`/`reachedServer`/`class`/`missing`/`reason` are present depends on `status`
        (and, when `status` is "failed", on `class`) — see each field's own description. This
        schema validates field types and enum membership; it does not enforce that `class` is
        present only when `status: "failed"` (or the equivalent dependency for the other fields)
        — treat `status` (and, for a "failed" outcome, `class`) as authoritative.
      type: object
      required: [status]
      additionalProperties: false
      properties:
        status:
          type: string
          enum: [passed, failed, not_attempted]
        degraded:
          description: Present only when status is "passed" and at least one field path degraded.
          type: array
          items: { type: string }
        reachedServer:
          description: Present only when status is "failed".
          type: boolean
        class:
          description: Present only when status is "failed" and reachedServer is true.
          type: string
          enum: [contract_violation, server_error]
        missing:
          description: Present only when class is "contract_violation" and fields were missing.
          type: array
          items: { type: string }
        reason:
          description: Present only when status is "not_attempted".
          type: string
          enum: [needs_credential, no_example_for_required_input, server_address_missing, call_budget_exhausted]

    VerifiedOperation:
      type: object
      required: [key, method, path, summary, effect, decision, authentication, requiredInputs, requiredInputsWithExample, live]
      additionalProperties: false
      properties:
        key: { type: string }
        method: { type: string }
        path: { type: string }
        summary: { type: string }
        effect:
          type: [string, "null"]
          enum: [read, write, irreversible, null]
        decision:
          $ref: "#/components/schemas/Decision"
        authentication:
          type: [string, "null"]
          enum: [none, header, null]
        requiredInputs: { type: integer }
        requiredInputsWithExample: { type: integer }
        live:
          oneOf:
            - type: "null"
            - $ref: "#/components/schemas/LiveOutcome"

    PublicVerifyReport:
      type: object
      required: [verifiedAt, source, api, contractFingerprint, summary, documentNotes, operations]
      additionalProperties: false
      properties:
        verifiedAt: { type: string, format: date-time }
        source:
          type: object
          required: [url, sha256, unresolvedReferences]
          additionalProperties: false
          properties:
            url: { type: string }
            sha256: { type: string }
            unresolvedReferences:
              type: array
              items: { type: string }
        api:
          type: object
          required: [name, serverAddress]
          additionalProperties: false
          properties:
            name: { type: string }
            serverAddress: { type: [string, "null"] }
        contractFingerprint: { type: string }
        summary:
          type: object
          required:
            [operations, included, excludedWrites, excludedIrreversible, excludedOther, livePassed, liveFailed, liveNotAttempted]
          additionalProperties: false
          properties:
            operations: { type: integer }
            included: { type: integer }
            excludedWrites: { type: integer }
            excludedIrreversible: { type: integer }
            excludedOther: { type: integer }
            livePassed: { type: integer }
            liveFailed: { type: integer }
            liveNotAttempted: { type: integer }
        documentNotes:
          type: array
          items: { type: string }
        operations:
          type: array
          items:
            $ref: "#/components/schemas/VerifiedOperation"

    VerifyOpenapi422:
      description: >-
        The merged shape of all three 422 causes on this operation (see the response's own
        `examples`). Not a `oneOf` of `SourceUnreadableRefusal`/`DocumentNotOpenapiRefusal`/
        `NothingVerifiableRefusal` below — those three schemas exist so a consumer can validate
        one concrete cause precisely; this one exists so the operation's response entry itself
        stays a single schema. This schema validates field types and enum membership; it does not
        enforce that `reason`'s closed `FetchSourceError` enum applies only when
        `error: "source_unreadable"` — treat `error` as authoritative.
      type: object
      required: [error]
      additionalProperties: false
      properties:
        status: { type: integer, enum: [422] }
        error:
          type: string
          enum: [source_unreadable, document_not_openapi, nothing_verifiable]
        reason:
          description: >-
            For "source_unreadable", one of `FetchSourceError`'s members (see
            `SourceUnreadableRefusal`); for the other two causes, free-form text naming the cause.
          type: string
        httpStatus:
          description: Present only for "source_unreadable", when the read reached an HTTP response.
          type: integer

    InvalidUrlRefusal:
      type: object
      required: [error]
      additionalProperties: false
      properties:
        status: { type: integer, enum: [400] }
        error: { type: string, enum: [invalid_url] }

    NotFoundRefusal:
      type: object
      required: [error]
      additionalProperties: false
      properties:
        error: { type: string, enum: [not_found] }

    SourceUnreadableRefusal:
      type: object
      required: [error, reason]
      additionalProperties: false
      properties:
        status: { type: integer, enum: [422] }
        error: { type: string, enum: [source_unreadable] }
        reason:
          type: string
          enum:
            - scheme_not_allowed
            - host_not_allowed
            - address_blocked
            - too_many_redirects
            - response_too_large
            - timeout
            - source_fetch_failed
            - source_too_large
        httpStatus: { type: integer }

    DocumentNotOpenapiRefusal:
      type: object
      required: [error, reason]
      additionalProperties: false
      properties:
        status: { type: integer, enum: [422] }
        error: { type: string, enum: [document_not_openapi] }
        reason: { type: string }

    NothingVerifiableRefusal:
      type: object
      required: [error, reason]
      additionalProperties: false
      properties:
        status: { type: integer, enum: [422] }
        error: { type: string, enum: [nothing_verifiable] }
        reason: { type: string }

    RateLimitedRefusal:
      type: object
      required: [error, retryAfterMinutes]
      additionalProperties: false
      properties:
        error: { type: string, enum: [rate_limited] }
        retryAfterMinutes: { type: integer }

    BusyRefusal:
      type: object
      required: [error]
      additionalProperties: false
      properties:
        error: { type: string, enum: [busy] }

    # ------------------------------------------------------------------------------------------
    # GET /api/v1/listings, GET /api/v1/listings/{slug}
    # ------------------------------------------------------------------------------------------
    PublicListingCapability:
      type: object
      required: [name, effect]
      additionalProperties: false
      properties:
        name: { type: string }
        effect: { type: string }

    PublicListing:
      description: >-
        The public field allow-list for a listed hosted endpoint (AC-7). `capabilities` is always
        the LIVE published capability list, never the last-approved snapshot — a re-review in
        progress (an escalation) is visible only as its effect on this array, never
        as a status field. No tenant id, deployment id, requested_* field, decision reason,
        timestamp, or escalation field is ever present.
      type: object
      required: [slug, displayName, description, website, logoRef, accessMode, capabilities]
      additionalProperties: false
      properties:
        slug: { type: string }
        displayName: { type: string }
        description: { type: string }
        website: { type: [string, "null"] }
        logoRef:
          type: [string, "null"]
          description: A `data:` URI (base64), or null when the listing carries no logo.
        accessMode:
          type: string
          enum: [public, oauth]
        capabilities:
          type: array
          items:
            $ref: "#/components/schemas/PublicListingCapability"

    # ------------------------------------------------------------------------------------------
    # GET /api/v1/pricing
    # ------------------------------------------------------------------------------------------
    PublicPricingPeriodPrice:
      type: object
      required: [months, amountCents]
      additionalProperties: false
      properties:
        months: { type: integer }
        amountCents: { type: integer }

    PublicPricingAllowance:
      description: >-
        The hosting allowance a self-serve plan carries by default — the plan's own figures, never
        a particular customer's add-on. Null on enterprise, which has no shared hosting pool.
      type: [object, "null"]
      required: [publishedHostedDeployments, hostedCallPool, runtime]
      additionalProperties: false
      properties:
        publishedHostedDeployments: { type: integer }
        hostedCallPool: { type: integer }
        runtime:
          type: object
          required: [concurrency, requestsPerMinute]
          additionalProperties: false
          properties:
            concurrency: { type: integer }
            requestsPerMinute: { type: integer }

    PublicPricingSupport:
      type: object
      required: [channel, firstResponseBusinessDays]
      additionalProperties: false
      properties:
        channel:
          type: string
          enum: [community, email, priority, contract]
        firstResponseBusinessDays: { type: [integer, "null"] }

    PublicPricingHostingFee:
      description: >-
        The flat hosting fee Starter can add: its price per method and period, and the hosted-call
        pool and runtime it brings. Null when the price list in effect does not sell it yet.
      type: [object, "null"]
      required: [prices, hostedCallPool, runtime]
      additionalProperties: false
      properties:
        prices:
          type: object
          required: [card, crypto]
          additionalProperties: false
          properties:
            card:
              type: array
              items:
                $ref: "#/components/schemas/PublicPricingPeriodPrice"
            crypto:
              type: array
              items:
                $ref: "#/components/schemas/PublicPricingPeriodPrice"
        hostedCallPool: { type: integer }
        runtime:
          type: object
          required: [concurrency, requestsPerMinute]
          additionalProperties: false
          properties:
            concurrency: { type: integer }
            requestsPerMinute: { type: integer }

    PublicPricingCreditPack:
      description: A one-off credit pack for this plan's tier, priced per method (no period).
      type: object
      required: [sku, credits, prices]
      additionalProperties: false
      properties:
        sku:
          type: string
          enum: [credit_pack_1m, credit_pack_5m]
        credits: { type: integer }
        prices:
          type: object
          required: [card, crypto]
          additionalProperties: false
          properties:
            card: { type: integer }
            crypto: { type: integer }

    PublicPricingPlan:
      description: >-
        The closed, tenant-free per-plan shape GET /api/v1/pricing (signed out) hands back. No
        `currentPlan`, `selfServe`, tenant id, or tenant-scoped price-list version identifier is
        ever present. `includedCalls` is the base plan's ingest fair-use cap, not a hosted-call
        figure. Hosting capacity is published in `allowance` (every plan; null on enterprise) and
        `hostingFee` (present on starter only), so the top-level `runtime` is always null.
        `creditPacks` is present on starter and growth only. Inside a present `hostingFee` or
        `creditPacks` key, null means the price list in effect does not sell it yet.
      type: object
      required: [name, displayPrice, amount, prices, includedCalls, retentionDaysMax, runtime, allowance, support]
      additionalProperties: false
      properties:
        name:
          type: string
          enum: [free, starter, growth, enterprise]
        displayPrice: { type: string }
        amount:
          type: [object, "null"]
          required: [amountCents, currency]
          additionalProperties: false
          properties:
            amountCents: { type: integer }
            currency: { type: string, enum: [USD] }
        prices:
          type: [object, "null"]
          additionalProperties: false
          properties:
            card:
              type: array
              items:
                $ref: "#/components/schemas/PublicPricingPeriodPrice"
            crypto:
              type: array
              items:
                $ref: "#/components/schemas/PublicPricingPeriodPrice"
        includedCalls: { type: [integer, "null"] }
        retentionDaysMax: { type: integer }
        runtime:
          type: [object, "null"]
          required: [concurrency, requestsPerMinute]
          additionalProperties: false
          properties:
            concurrency: { type: integer }
            requestsPerMinute: { type: integer }
        allowance:
          $ref: "#/components/schemas/PublicPricingAllowance"
        support:
          $ref: "#/components/schemas/PublicPricingSupport"
        hostingFee:
          $ref: "#/components/schemas/PublicPricingHostingFee"
        creditPacks:
          type: [array, "null"]
          items:
            $ref: "#/components/schemas/PublicPricingCreditPack"
        workspaceCap:
          type: integer
          description: >-
            Present on growth only: how many workspaces one Growth bill covers, the paying
            workspace included. Added in 1.3.0.

    PublicPricing:
      type: object
      required: [plans, cryptoSavingPercent]
      additionalProperties: false
      properties:
        plans:
          type: array
          items:
            $ref: "#/components/schemas/PublicPricingPlan"
        cryptoSavingPercent:
          type: [integer, "null"]
          description: >-
            The largest crypto saving against the card-monthly equivalent, across every priced
            plan and period, rounded down. Never 0 — null when no crypto price undercuts its
            card equivalent.
