openapi: 3.1.2
info:
  title: ohmyho.st API
  version: 0.0.0
  description: Public REST API for ohmyho.st hosting, projects, domains, email, credits and exports.
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
servers:
  - url: https://app.ohmyho.st
    description: Production control API
  - url: https://dev.app.ohmyho.st
    description: Development control API
security:
  - BearerAuth: []
paths:
  /v1/me/profile:
    get:
      operationId: getAccountProfile
      summary: Read the current interactive user profile and signup attribution
      responses:
        "200":
          description: Authenticated account data.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AccountProfile" }
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/me/signup-source:
    put:
      operationId: recordSignupSource
      summary: Record the authenticated user signup source once
      description: Interactive session required; the first accepted source is immutable, repeat requests return it unchanged and no credits or Paid rights are granted by this endpoint.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [source]
              properties:
                source: { type: [string, "null"], minLength: 1, maxLength: 64 }
      responses:
        "200":
          description: Authenticated account data.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AccountProfile" }
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/organizations/{organization_id}/account:
    get:
      operationId: getOrganizationAccount
      summary: Read effective plan and monthly versus one-time credit balance
      parameters:
        - name: organization_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      responses:
        "200":
          description: Authenticated account data.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrganizationAccount" }
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/contact-requests:
    post:
      operationId: submitContactRequest
      summary: Submit a contact or privacy question
      description: Stores a private contact request for twelve months, with idempotent replay and no account creation or marketing enrollment.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name, email, company, message, idempotency_key]
              properties:
                name: { type: string, minLength: 1, maxLength: 120 }
                email: { type: string, format: email, maxLength: 254 }
                company: { type: string, maxLength: 200, description: Empty for an individual. }
                message: { type: string, minLength: 1, maxLength: 8000 }
                idempotency_key: { type: string, format: uuid }
      responses:
        "202":
          description: Request durably received; repeat submissions with the same key do not create duplicates.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [accepted]
                properties:
                  accepted: { type: boolean, const: true }
        "400": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Problem" }
        "413": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/beta/eligibility:
    get:
      operationId: getBetaEligibility
      summary: Check a beta invitation source
      security: []
      parameters:
        - name: r
          in: query
          schema: { type: string, maxLength: 64 }
      responses:
        "200":
          description: Eligibility only; no account or project authority is granted.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [eligible]
                properties:
                  eligible: { type: boolean }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/beta/interests:
    post:
      operationId: registerBetaInterest
      summary: Register interest in the beta with explicit consent
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [email, consent, consent_version]
              properties:
                email: { type: string, format: email, maxLength: 254 }
                consent: { type: boolean, const: true }
                consent_version: { type: string, const: beta-interest-2026-09-13 }
      responses:
        "202":
          description: Interest is stored; repeat submissions return the same acknowledgment.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [accepted]
                properties:
                  accepted: { type: boolean, const: true }
        "400": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "413": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/beta/feature-interests:
    parameters:
      - in: header
        name: X-Ohmyho-Voter
        required: true
        description: Random UUIDv4 identifying only this anonymous browser's votes; not an account credential or proof of a unique person. Keep it out of URLs and logs.
        schema:
          type: string
          format: uuid
          pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$
    get:
      operationId: getFeatureInterests
      summary: Read this anonymous browser's current roadmap votes
      security: []
      responses:
        "200":
          description: Current choices; a missing, removed or expired choice is null.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeatureVoteState"
        "400": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
    post:
      operationId: registerFeatureInterest
      summary: Set or remove this anonymous browser's roadmap vote
      description: Store a desired choice once per request key for twelve months. Replaying an earlier request never overwrites a later choice. A null choice removes the vote; this is not toggle-on-every-request behavior.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [feature, choice, idempotency_key]
              properties:
                feature: { type: string, enum: [eu, iso27001, soc2] }
                choice: { type: [string, "null"], enum: [up, down, null] }
                idempotency_key: { type: string, format: uuid }
      responses:
        "202":
          description: The request is persisted, with the browser's current state after this request or replay.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [accepted, votes]
                properties:
                  accepted: { type: boolean, const: true }
                  votes:
                    $ref: "#/components/schemas/FeatureVoteChoices"
        "400": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Problem" }
        "413": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/beta/stats:
    get:
      operationId: getPublicDeploymentStats
      summary: Read distinct active projects successfully deployed in the past seven days
      security: []
      responses:
        "200":
          description: Aggregate measurements without customer or repository identifiers.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [deploys_7d, observed_at]
                properties:
                  deploys_7d: { type: integer, minimum: 0 }
                  observed_at: { type: string, format: date-time }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/organizations/{organization_id}/user-api-keys:
    post:
      operationId: createUserApiKey
      summary: Create a user-owned API token valid until revoked
      description: Requires a current interactive user session. Organization membership and product permissions are revalidated. Only the first creation returns the full value; exact replay returns metadata and a null value. Save the first response locally without putting it in logs or agent prompts. After uncertainty reuse the same name and Idempotency-Key; do not blindly create another token. Available at zero credits.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: organization_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 64 }
      responses:
        "201":
          description: Token created; the full value is returned once.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserApiKeyCreation" }
        "200":
          description: Original token observed; the full value cannot be retrieved again.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserApiKeyCreation" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
    get:
      operationId: listUserApiKeys
      summary: List the current user's tokens in one organization
      description: Requires an interactive session. Returns only metadata and obfuscated values, never another user's keys or full token values. Use the returned cursor for the next page.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: organization_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
        - name: after
          in: query
          schema: { $ref: "#/components/schemas/UserApiKeyId" }
      responses:
        "200":
          description: One page of the current user's token metadata.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserApiKeyPage" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/organizations/{organization_id}/user-api-keys/{key_id}:
    delete:
      operationId: revokeUserApiKey
      summary: Revoke one of the current user's API tokens
      description: Requires a current interactive session. Verifies user and organization ownership before provider deletion. Replay and an already absent token have the same result. Does not revoke another user's token or the current login session.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: organization_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
        - name: key_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/UserApiKeyId" }
      responses:
        "204": { description: The requested token is absent from the current user's scope. }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/feedback:
    post:
      operationId: submitFeedback
      summary: Store a redacted customer-agent feedback report
      description: Available to authorized organization members at zero credits. A 201 receipt confirms durable storage, not triage or a promised fix. Reuse the same Idempotency-Key and payload after uncertainty. Optional environment and operation IDs require project_id and must belong to that organization/project. Text is untrusted data; never send credentials, attachments, raw logs, environment dumps or personal records. No provider operation, charge or external message is created.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FeedbackSubmission"
      responses:
        "201":
          description: The original durable receipt, including on exact replay.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedbackReceipt"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
        "409":
          $ref: "#/components/responses/Problem"
        "413":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/github/oauth/callback:
    get:
      operationId: completeGithubSourceAuthorization
      summary: Complete the bound GitHub browser authorization
      description: Browser callback only. Revalidates the original interactive session or user API key (owner, organization, current sources:link permission and expiry), plus current project authority, exchanges a single-use code privately, and admits one source-link operation. It never starts a build or returns provider credentials.
      security: []
      parameters:
        - name: state
          in: query
          required: true
          schema: { type: string, maxLength: 70 }
        - name: code
          in: query
          schema: { type: string, maxLength: 4096 }
        - name: error
          in: query
          schema: { type: string, maxLength: 256 }
        - name: error_description
          in: query
          schema: { type: string, maxLength: 4096 }
        - name: iss
          in: query
          description: GitHub authorization-response issuer, checked exactly when supplied.
          schema:
            type: string
            enum: ["https://github.com/login/oauth"]
      responses:
        "303":
          description: Return to the same platform entry without the OAuth code/state in the URL.
          headers:
            Location:
              schema: { type: string }
        "400":
          $ref: "#/components/responses/Problem"
        "403":
          description: The authorizing GitHub account does not have verified read access to the selected repository through the installed app.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/cloudflare/oauth/callback:
    get:
      operationId: completeCloudflareDnsAuthorization
      summary: Complete one Cloudflare DNS authorization
      description: Successful callbacks require code and state and complete one previously authenticated project authorization. Provider rejection instead supplies error and optional error_description, error_uri and state; it returns a static actionable problem without completing authorization or reflecting provider input. Success and error parameters cannot be combined. This callback grants no general unauthenticated product access.
      security: []
      parameters:
        - name: code
          in: query
          required: false
          schema:
            type: string
            minLength: 8
            maxLength: 2048
            pattern: "^[A-Za-z0-9._~-]+$"
        - name: state
          in: query
          required: false
          schema:
            type: string
            minLength: 16
            maxLength: 2048
            pattern: "^[A-Za-z0-9._~-]+$"
        - name: error
          in: query
          schema:
            type: string
            pattern: "^[a-z_]{1,64}$"
        - name: error_description
          in: query
          schema:
            type: string
            maxLength: 2048
        - name: error_uri
          in: query
          schema:
            type: string
            maxLength: 2048
      responses:
        "200":
          description: Authorization completed; the browser window may be closed.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            text/html:
              schema:
                type: string
        "400":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/me:
    get:
      operationId: getCurrentIdentity
      summary: Get the current authenticated identity
      description: Returns the stable ohmyhost actor and internal organization identifiers derived from the bearer credential.
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: The current authenticated identity.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentIdentity"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/session:revoke:
    post:
      operationId: revokeCurrentSession
      summary: Revoke the current signed-in user session
      description: Revokes only the session proven by the bearer credential, confirms its absence from active WorkOS sessions and records local terminal denial. No organization is required. The request body and query must be empty; caller-supplied session or user identifiers are not accepted. Repeated private processing cannot revoke another session. Once revoked, the old bearer is no longer authorized, including for a public replay. This synchronous identity-lifecycle operation does not create a project operation.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Provider session revocation and local terminal denial are confirmed.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [revoked]
                properties:
                  revoked:
                    type: boolean
                    const: true
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/organizations:
    post:
      operationId: createOrganization
      summary: Create an organization for the signed-in user
      description: Creates an organization and its creator's Owner membership. Only a verified user session may call this endpoint; no existing organization is required. Repeating the same Idempotency-Key and name observes the same creation, never recreating revoked membership. After creation, repeat the public login flow to select the new organization before project mutations.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: Idempotency-Key
          in: header
          required: true
          description: Stable creator-scoped key; repeat unchanged to observe an incomplete creation.
          schema:
            type: string
            minLength: 1
            maxLength: 128
            pattern: "^[A-Za-z0-9._:-]+$"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrganizationRequest"
      responses:
        "201":
          description: Organization and Owner membership are persisted. Idempotent replay returns the same identity.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Organization"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "409":
          $ref: "#/components/responses/Problem"
        "413":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/organizations/{organization_id}/billing/checkouts:
    post:
      operationId: createBillingCheckout
      summary: Create or resume an owner's hosted Stripe Checkout
      description: Returns a human payment URL, never charges a saved card. Paid is USD 10/month; each top-up pack is USD 10 for 1000 credits, before tax. Retry the same offer, packs and Idempotency-Key after uncertainty. Browser return is not payment proof; read this checkout and the organization balance. A conflicting or existing subscription returns billing_purchase_conflict (409); read the original checkout or request an owner billing portal URL instead of another purchase. Works at zero credits.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: organization_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [offer, packs]
              properties:
                offer: { type: string, enum: [topup, paid] }
                packs:
                  {
                    type: integer,
                    minimum: 1,
                    maximum: 100,
                    description: Paid requires exactly one pack.,
                  }
      responses:
        "201":
          description: Original checkout and its current provider-observed status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillingCheckout" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/Problem" }
        "413": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/organizations/{organization_id}/billing/checkouts/{checkout_id}:
    get:
      operationId: getBillingCheckout
      summary: Observe and reconcile an owner's original checkout
      description: Reads Stripe and reconciles confirmed credits/refunds idempotently. payment_confirmed describes the original Checkout, not spendable credit or current Paid entitlement. Read organization credits for available funding; paid_until is the current paid coverage. No new purchase intent or payment is created; an uncertain original Checkout can resume using its stored identity.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: organization_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
        - name: checkout_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      responses:
        "200":
          description: Current observed checkout and credit reconciliation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillingCheckout" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/organizations/{organization_id}/billing/portal:
    post:
      operationId: createBillingPortal
      summary: Open the owner's Stripe billing portal
      description: Creates a short-lived human URL for invoices, payment method updates and cancellation at period end. Does not charge or change the subscription itself. Customer and return URL are server-selected. Request a fresh URL if the portal has expired. Works at zero credits.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: organization_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: false }
      responses:
        "200":
          description: Short-lived portal URL; do not log it or commit it as evidence.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillingPortal" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/Problem" }
        "413": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/organizations/{organization_id}/billing/recharge:
    get:
      operationId: getBillingRecharge
      summary: Read auto-recharge and the current payment handoff
      description: Owner-only. Reads saved consent and gross UTC-month spending; a pending Stripe card setup may be resumed. No charge is initiated by reading. Private Stripe URLs must not be logged.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: organization_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      responses:
        "200":
          description: Actual recharge settings; disabled by default.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillingRecharge" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
    put:
      operationId: configureBillingRecharge
      summary: Enable or disable explicitly authorized auto-recharge
      description: Owner-only. An agent must obtain explicit approval for off-session charges and the gross monthly spending cap before enabling. Each refill costs USD 9 before tax for 1000 non-expiring credits when available credits fall below 100. A saved Stripe card is required; follow setup_url if returned. Current revision prevents stale edits; retry the original Idempotency-Key and payload after uncertainty. Disable prevents new charge initiation; already initiated payments may complete. No subscription or Paid features are created.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: organization_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [enabled, monthly_limit_minor, consent, revision]
              properties:
                enabled: { type: boolean }
                monthly_limit_minor:
                  {
                    type: integer,
                    minimum: 1000,
                    maximum: 100000,
                    description: Gross USD cents per UTC calendar month including tax.,
                  }
                consent:
                  {
                    type: [string, "null"],
                    enum: [off_session_v1, null],
                    description: Explicit Owner consent when enabling; null when disabling.,
                  }
                revision: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Saved policy and optional Stripe card setup.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillingRecharge" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/Problem" }
        "413": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/organizations/{organization_id}/credits:
    get:
      operationId: getOrganizationCredits
      summary: Read the owner's shared organization credit pool
      description: Returns posted credits and reservations, in microcredits (one credit is 1000000 microcredits). Monthly entitlement posting is idempotent. Only the organization Owner may read this balance. active_meters names the currently billed sources; unreported usage is not included. platform_overrun_micros is recorded platform exposure, not customer debt. Remains available at zero credit.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: organization_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Ulid"
      responses:
        "200":
          description: Current organization credit balance.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationCredits"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/organizations/{organization_id}/credit-usage:
    get:
      operationId: getOrganizationCreditUsage
      summary: Read monthly measured usage by project and meter
      description: Owner-only event-month ledger totals, including signed corrections posted by as_of. Returns up to 20 projects per page in ID order; use next_cursor with the same month. Current unresolved reservations are separate from measured consumption. Null environment_id means project-shared cost, never guessed Dev allocation. Only posted measurements are included; this is not a complete provider invoice or zero-usage guarantee. Billing may arrive later. No credit is granted or charged by this read; it remains usable at zero credits.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: organization_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Ulid"
        - name: month
          in: query
          required: true
          schema:
            type: string
            pattern: "^20[0-9]{2}-(0[1-9]|1[0-2])$"
        - name: cursor
          in: query
          schema:
            $ref: "#/components/schemas/Ulid"
      responses:
        "200":
          description: Current posted usage for the selected UTC month and project page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationCreditUsage"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/projects/{project_id}/exports:
    post:
      operationId: createProjectExport
      summary: Request an asynchronous password-encrypted SQL ZIP
      description: Owner-only and available at zero credits. Exports each confirmed physical project database once, including separate Dev/Prod SQL or one shared SQL file. Excludes files, source code and configuration. At most one accepted export per project per rolling 24 hours; failed jobs still count and idempotent replay returns the original operation. The user retains the password. Poll getProjectExport; do not create another job while it is running.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [password]
              properties:
                password:
                  type: string
                  minLength: 1
                  maxLength: 1024
                  writeOnly: true
                  description: Non-blank user-controlled ZIP password, at most 1024 UTF-8 bytes. Never include it in logs or command-line arguments.
      responses:
        "202": { $ref: "#/components/responses/AcceptedOperation" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/Problem" }
        "429":
          description: Project export limit reached, or an ordinary API rate limit. Retry-After gives the delay before another attempt.
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/ProblemDetails" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/projects/{project_id}/exports/{export_id}:
    get:
      operationId: getProjectExport
      summary: Read export progress and its verified 24-hour download capability
      description: Owner-only, including at zero credits. Poll queued/running jobs after next_poll_after_seconds. A verified SQL ZIP is retained seven days. Its signed download URL is valid 24 hours and is issued only with at least 24 hours of retention left; otherwise download fields are null. Treat the URL as a secret bearer capability. Neither the password nor any permanent storage credential can be retrieved. Generic operation reads never contain this capability.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RequestId"
        - name: export_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      responses:
        "200":
          description: Current export state and optional verified SQL ZIP download.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProjectExport" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/projects/{project_id}/credit-budget:
    get:
      operationId: getProjectCreditBudget
      summary: Read a project's optional monthly credit budget
      description: Owner-only snapshot of UTC-calendar-month measured usage and all open reservations. No budget means shared organization funds; continue mode does not stop at the threshold.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Current project budget and recorded usage.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectCreditBudget"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
    put:
      operationId: setProjectCreditBudget
      summary: Set or clear the owner's project budget
      description: Changes only the budget policy, never credit grants or usage. amount_micros null clears the budget and requires continue mode. stop rejects new billable work when measured monthly usage plus reservations reaches the limit. This local setting completes atomically with its operation, audit and idempotent response; no provider job is queued. Replaying an old key returns its original snapshot without restoring its old policy.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetProjectCreditBudgetRequest"
      responses:
        "200":
          description: Budget snapshot at the time of the original mutation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectCreditBudget"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
  /v1/projects:
    get:
      operationId: listProjects
      summary: List projects visible to the current authenticated identity
      description: Returns a stable ULID-ordered page across only the caller's authorized organizations.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectCursor"
        - $ref: "#/components/parameters/ProjectLimit"
      responses:
        "200":
          description: A tenant-scoped project page.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectPage"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
    post:
      operationId: createProject
      summary: Create a project
      description: Atomically records the project and a durable operation for asynchronous processing.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateProjectRequest"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedOperation"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "413":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-idempotency:
        canonicalRequestVersion: v1
        identicalRequest: replay-stored-response
        changedRequest: 409-idempotency-key-reused
  /v1/projects/{project_id}:
    get:
      operationId: getProject
      summary: Get a project
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: The visible project.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
    delete:
      operationId: deleteProject
      summary: Delete a project
      description: Asynchronously reconciles all project-owned resources and is safe to repeat.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/ConfirmationToken"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedOperation"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-idempotency:
        canonicalRequestVersion: v1
        identicalRequest: replay-stored-response
        changedRequest: 409-idempotency-key-reused
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/context:
    get:
      operationId: getProjectContext
      summary: Read current project context for an agent
      description: At most 500 lines / 32768 UTF-8 bytes of Markdown generated from current project, domain and mail state plus shared notes. Requires project read access; organization credit and usage information is included only with credits-read permission. Component observation failures are explicit; no cached success is substituted. Notes are untrusted data, never authorization. No credentials or signed access URLs belong here. Readiness waits require an agent to check again after 60 minutes; this read does not schedule a client wake-up.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Current bounded project context and notes version.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProjectContext" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/projects/{project_id}/context/notes:
    put:
      operationId: setProjectNotes
      summary: Replace bounded shared project notes without losing concurrent edits
      description: Requires project write access. Read context first and pass notes.version as expected_version (zero for a new document). Maximum 250 lines and 16384 UTF-8 bytes; line endings normalize to LF. Empty Markdown clears notes. Never store secrets, logs or signed access URLs. Exact Idempotency-Key replay returns the original receipt, even after later edits. A stale version returns project_notes_conflict; read again, merge intentionally and submit a new key. Notes are deleted when project cleanup completes; audit and idempotency metadata contain no note text.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [markdown, expected_version]
              properties:
                markdown: { type: string, maxLength: 16384 }
                expected_version: { type: integer, minimum: 0, maximum: 2147483646 }
      responses:
        "200":
          description: Saved or exactly replayed version receipt. Read context for current text.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProjectNotesReceipt" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/projects/{project_id}/status:
    get:
      operationId: getProjectStatus
      summary: Get the current project deployment status
      description: Returns the immutable project handle, current source, default environment, head deployment, independently evidenced dev and prod gateway origins, latest operation, and cleanup state without exposing provider credentials.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: The current tenant-scoped project status.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectStatus"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/dev-access-tickets:
    post:
      operationId: createProjectDevAccessTicket
      summary: Create a single-use dev access ticket
      description: Owner-only issuance of a ten-minute single-use ticket for the immutable dev project origin. A new ticket revokes unused tickets previously issued to the same principal.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "201":
          description: A single-use dev access URL. Credential material is returned once and is never persisted in plaintext.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
            Pragma:
              schema:
                type: string
                const: no-cache
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DevAccessTicket"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}:delete-plan:
    post:
      operationId: planProjectDeletion
      summary: Plan project deletion
      description: Produces a non-mutating project deletion plan and a ten-minute action-bound confirmation token.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: A non-mutating project deletion plan.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GuardedActionPlan"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/source:link:
    post:
      operationId: linkProjectSource
      summary: Link a GitHub source repository
      description: Starts customer GitHub authorization using the current interactive user session or user-owned API key with sources:link permission. The original credential is bound to the ten-minute consent; switching credentials requires a new Idempotency-Key. No raw deployment token is stored or placed in the browser URL. Install the App for the selected repository if needed, then open authorization_url. The callback admits one source-link operation without building. Repeat the identical request/key to observe authorization and operation status; caller installation IDs are never accepted.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LinkProjectSourceRequest"
      responses:
        "201":
          description: Bound GitHub authorization handoff or its current replay status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GithubSourceAuthorization"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-idempotency:
        canonicalRequestVersion: v1
        identicalRequest: replay-stored-response
        changedRequest: 409-idempotency-key-reused
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/source:
    get:
      operationId: getProjectSource
      summary: Get the linked source status
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: The current GitHub source connection and status.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectSource"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/source/auto-deploy:
    put:
      operationId: configureProjectSourceAutoDeploy
      summary: Configure automatic GitHub push deployment
      description: Configures exactly one Git branch whose signed pushes deploy immutable commits to dev. Production remains an explicit promotion.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConfigureSourceAutoDeployRequest"
      responses:
        "200":
          description: The current automatic deployment configuration.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SourceAutoDeploy"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
    get:
      operationId: getProjectSourceAutoDeploy
      summary: Get automatic GitHub push deployment status
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: The current automatic deployment configuration and grant status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SourceAutoDeploy"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/cloudflare-dns/authorization:
    post:
      operationId: createCloudflareDnsAuthorization
      summary: Start project-scoped Cloudflare DNS authorization
      description: Creates or replays one short-lived authorization URL for the customer Cloudflare zone bound to the project's Paid domain. Pending requests replay the original handoff. Expired or consumed requests return cloudflare_authorization_closed (409); read current DNS authorization status and reuse a valid matching grant, or request a fresh authorization with a new key. No provider credential is returned.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCloudflareDnsAuthorizationRequest"
      responses:
        "201":
          description: A short-lived Cloudflare authorization URL.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CloudflareDnsAuthorization"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/Problem"
        "413":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-idempotency:
        canonicalRequestVersion: v1
        identicalRequest: replay-stored-response
        changedRequest: 409-idempotency-key-reused
        closedAuthorization: 409-cloudflare-authorization-closed
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/cloudflare-dns:
    get:
      operationId: getCloudflareDnsAuthorizationStatus
      summary: Get project-scoped Cloudflare DNS authorization status
      description: Returns only the fixed zone, closed scope set, expiry, and authorization state; provider credentials are never exposed.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: The current credential-free Cloudflare DNS authorization status.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CloudflareDnsAuthorizationStatus"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/domains:
    put:
      operationId: configureProjectDomains
      summary: Apply the server-derived project domains
      description: Starts an idempotent durable operation for the exact project-derived development, production, and mail hostnames. The request has no body.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedOperation"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-idempotency:
        canonicalRequestVersion: v1
        identicalRequest: replay-stored-response
        changedRequest: 409-idempotency-key-reused
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/paid-domain:plan:
    post:
      operationId: planPaidProjectDomain
      summary: Plan one Paid customer-owned hostname
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PaidDomainRequest" }
      responses:
        "200":
          description: Manual CNAME instructions and exact provider effects.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaidDomainPlan" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/paid-domain:
    put:
      operationId: applyPaidProjectDomain
      summary: Apply one Paid customer-owned hostname
      description: Requires a confirmed current Paid service period and no expired credit-exhaustion grace. Returns paid_plan_required without a Paid period, or insufficient_organization_credits after seven days with no available credits, without provider mutation. Top-ups restore exhausted Paid access but do not create or extend a Paid service period.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PaidDomainRequest" }
      responses:
        "200":
          description: Current Cloudflare for SaaS hostname and certificate state.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaidDomain" }
        "402": { $ref: "#/components/responses/Problem" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
    get:
      operationId: getPaidProjectDomain
      summary: Read the Paid customer-owned hostname
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: Current credential-free hostname, CNAME and certificate state.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaidDomain" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
    delete:
      operationId: deletePaidProjectDomain
      summary: Delete one exact Paid customer-owned hostname
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PaidDomainRequest" }
      responses:
        "200":
          description: Exact route and custom hostname are absent.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaidDomain" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/mail-domain:
    put:
      operationId: configureProjectMailDomain
      summary: Configure the project sender subdomain
      description: Create-only admission for one canonical sender subdomain, requiring a confirmed current Paid service period. Top-ups and promotional credits do not enable Paid. Provider mutation occurs only inside a later authorized deployment Workflow.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConfigureProjectMailDomainRequest"
      responses:
        "200":
          description: The immutable project sender-domain configuration and current credential-free status.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectMailDomain"
        "402":
          $ref: "#/components/responses/Problem"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-idempotency:
        canonicalRequestVersion: v1
        identicalRequest: replay-stored-response
        changedRequest: 409-idempotency-key-reused
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
    get:
      operationId: getProjectMailDomain
      summary: Get sender-domain delegation status
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: The current credential-free sender-domain status.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectMailDomain"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/deployments:plan:
    post:
      operationId: planDeployment
      summary: Plan a deployment
      description: Resolves immutable inputs and estimates effects without provider mutation or billable work. Mail and the currently supported mail-backed Better Auth contract require a confirmed Paid service period; it is checked again before build reservation. An old sender configuration is not Paid authority.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlanDeploymentRequest"
      responses:
        "200":
          description: A deployment plan valid for fifteen minutes.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeploymentPlan"
        "402":
          $ref: "#/components/responses/Problem"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/deployments:
    post:
      operationId: createDeployment
      summary: Create a deployment
      description: Reserves the quoted build credits from the shared organization pool and starts the exact reviewed plan as one durable operation. Insufficient available credits or an explicit project stop budget rejects before operation creation or provider work. Repeating the accepted request never reserves twice.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateDeploymentRequest"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedOperation"
        "402":
          $ref: "#/components/responses/Problem"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-idempotency:
        canonicalRequestVersion: v1
        identicalRequest: replay-stored-response
        changedRequest: 409-idempotency-key-reused
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
    get:
      operationId: listDeployments
      summary: List deployments
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/DeploymentCursor"
        - $ref: "#/components/parameters/DeploymentLimit"
      responses:
        "200":
          description: One page of project deployments in stable descending order.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeploymentPage"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/deployments/{deployment_id}:
    get:
      operationId: getDeployment
      summary: Get deployment status
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/DeploymentId"
      responses:
        "200":
          description: The deployment and its current status.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Deployment"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/deployments/{deployment_id}/logs:
    get:
      operationId: getDeploymentLogs
      summary: List normalized deployment diagnostics
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/DeploymentId"
        - name: cursor
          in: query
          schema: { type: string, maxLength: 2048 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 100 }
      responses:
        "200":
          description: Tenant-scoped normalized diagnostic page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeploymentDiagnosticPage" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/deployments/{deployment_id}/logs/events:
    get:
      operationId: streamDeploymentLogs
      summary: Stream normalized deployment diagnostics
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/DeploymentId"
      responses:
        "200":
          description: A bounded normalized diagnostic event stream.
          content:
            text/event-stream:
              schema: { $ref: "#/components/schemas/DeploymentDiagnostic" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "503": { $ref: "#/components/responses/Problem" }
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/database/compute:
    post:
      operationId: changeDatabaseCompute
      summary: Select standard or Paid performance database compute
      description: Requires project-write access and explicit confirmation. Reads the organization plan at acceptance (Free 0.25 CU/1 GB/60 idle seconds or Paid 0.5 CU/2 GB/60 idle seconds). Changes the existing database in place without copying or resetting data. Shared Dev/Prod data changes both environments. A brief connection interruption is possible. Poll the returned operation every 60 seconds; do not submit a second change while it runs. Completion requires actual provider settings and settled operations. Actual CU consumption remains metered. Performance requires effective Paid access and an active rate; it selects fixed 1 CU/4 GB/300 idle seconds at 2.5 times Paid-standard database compute credits per equal active minute. Only database compute changes price. Raw CU-second measurements are preserved; mixed or uncertain transition hours waive the premium. Returning to standard restores the effective plan size.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [environment, profile, confirm]
              properties:
                environment: { type: string, enum: [dev, prod] }
                profile: { type: string, enum: [standard, performance] }
                confirm: { type: boolean, const: true }
      responses:
        "202": { $ref: "#/components/responses/AcceptedOperation" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "409": { $ref: "#/components/responses/Problem" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
    get:
      operationId: getDatabaseCompute
      summary: Read actual managed database compute configuration
      description: Project readers can observe current Dev or Prod compute without receiving provider IDs or credentials and without executing SQL or waking the database. Explicitly shared data resolves to the same physical database for both environments. A null database means the owned environment has no confirmed managed placement. Provider failures return a problem, never invented defaults. Configuration describes observed settings, not a completed resize operation. suspend_timeout_seconds is the configured provider value; 0 means provider default and -1 means never suspend.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RequestId"
        - name: environment
          in: query
          schema: { type: string, enum: [dev, prod], default: dev }
      responses:
        "200":
          description: Fresh, tenant-scoped compute observation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DatabaseCompute" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
  /v1/projects/{project_id}/database/query:
    post:
      operationId: queryProjectDatabase
      summary: Run one bounded read-only Dev database query
      description: Executes one SELECT through the project's least-privilege read-only role. Connection credentials are never returned.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectDatabaseQueryRequest" }
      responses:
        "200":
          description: At most 100 JSON rows from the Dev project database.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProjectDatabaseQueryResult" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "403": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/ResourceNotFound" }
        "429": { $ref: "#/components/responses/Problem" }
        "503": { $ref: "#/components/responses/Problem" }
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/deployments/{target_deployment_id}:rollback-plan:
    post:
      operationId: planDeploymentRollback
      summary: Plan a deployment rollback
      description: Produces a non-mutating rollback plan for an immutable deployment artifact.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/TargetDeploymentId"
      responses:
        "200":
          description: A rollback plan with a ten-minute action-bound confirmation token.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GuardedActionPlan"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/deployments/{target_deployment_id}:rollback:
    post:
      operationId: rollbackDeployment
      summary: Roll back to an immutable deployment
      description: Republishes the target deployment artifact without starting a new build.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/TargetDeploymentId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/ConfirmationToken"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedOperation"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-idempotency:
        canonicalRequestVersion: v1
        identicalRequest: replay-stored-response
        changedRequest: 409-idempotency-key-reused
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/deployments/{source_deployment_id}:promote-plan:
    post:
      operationId: planDeploymentPromotion
      summary: Plan promotion of the current dev deployment
      description: Produces a non-mutating, ten-minute plan that binds the current succeeded dev deployment and current prod head without rebuilding the artifact.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/SourceDeploymentId"
      responses:
        "200":
          description: A promotion plan with a ten-minute action-bound confirmation token.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GuardedActionPlan"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/deployments/{source_deployment_id}:promote:
    post:
      operationId: promoteDeployment
      summary: Promote a verified dev artifact to prod
      description: Activates the immutable artifact from the current succeeded dev deployment in prod without rebuilding it.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/SourceDeploymentId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/ConfirmationToken"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedOperation"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-idempotency:
        canonicalRequestVersion: v1
        identicalRequest: replay-stored-response
        changedRequest: 409-idempotency-key-reused
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/environments/{environment_id}/secrets:
    get:
      operationId: listEnvironmentSecrets
      summary: List environment secret metadata
      description: Returns names and revisions only. Secret values are never readable through the public API.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/EnvironmentId"
      responses:
        "200":
          description: Environment secret metadata ordered by name.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvironmentSecretPage"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/projects/{project_id}/environments/{environment_id}/secrets/{secret_name}:
    put:
      operationId: putEnvironmentSecret
      summary: Create or rotate an environment secret
      description: Accepts a write-only value and returns metadata only. Replays require the same canonical value digest.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/EnvironmentId"
        - $ref: "#/components/parameters/SecretName"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PutEnvironmentSecretRequest"
      responses:
        "200":
          description: Stored secret metadata. The value is never returned.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvironmentSecret"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
    delete:
      operationId: deleteEnvironmentSecret
      summary: Delete an environment secret
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/EnvironmentId"
        - $ref: "#/components/parameters/SecretName"
      responses:
        "200":
          description: Idempotent deletion outcome.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteEnvironmentSecretResult"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/operations/{operation_id}:
    get:
      operationId: getOperation
      summary: Get an operation
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/OperationId"
      responses:
        "200":
          description: The durable operation and its current state.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Operation"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/operations/{operation_id}:reconcile:
    post:
      operationId: reconcileOperation
      summary: Reconcile an uncertain platform delivery or provider mutation
      description: Owner-only, idempotent recovery for an operation retained after an uncertain platform delivery or provider mutation. The control plane derives the exact recovery decision, including any deterministic internal Workflow handoff; the request has no body. Exhausted lifecycle recovery returns reconciliation_exhausted (409, retryable false); stop retries and report the original operation through feedback.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/OperationId"
      responses:
        "202":
          description: The reconciliation attempt was accepted or replayed.
          headers:
            Location:
              description: Relative URL of the original durable operation resource.
              required: true
              schema:
                type: string
                pattern: ^/v1/operations/[0-9A-HJKMNP-TV-Z]{26}$
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderReconciliationAttempt"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/Problem"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/operations/{operation_id}/events:
    get:
      operationId: streamOperationEvents
      summary: Stream operation events
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/OperationId"
      responses:
        "200":
          description: A server-sent event stream for the operation.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            text/event-stream:
              schema:
                $ref: "#/components/schemas/OperationEvent"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
  /v1/audit-events:
    get:
      operationId: listAuditEvents
      summary: List audit events
      description: Returns events visible to the authenticated organization in stable descending order.
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/ProjectFilter"
        - $ref: "#/components/parameters/OperationFilter"
        - $ref: "#/components/parameters/AuditCursor"
        - $ref: "#/components/parameters/AuditLimit"
      responses:
        "200":
          description: One page of visible audit events.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuditEventPage"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/Problem"
        "503":
          $ref: "#/components/responses/Problem"
      x-ohmyhost-audit-order:
        - occurred_at:desc
        - audit_event_id:desc
      x-ohmyhost-resource-disclosure: indistinguishable-not-found
components:
  headers:
    ETag:
      description: Opaque current resource version used by If-Match.
      required: true
      schema:
        type: string
        minLength: 3
        maxLength: 128
    XRequestId:
      description: Correlates the request with operations, events, logs, and audit records.
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
  parameters:
    AuditCursor:
      name: cursor
      in: query
      description: Opaque continuation cursor returned by the preceding page.
      required: false
      schema:
        type: string
        pattern: ^[A-Za-z0-9_-]+$
    AuditLimit:
      name: limit
      in: query
      description: Maximum number of audit events to return.
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Identifies one mutation and its canonical request payload.
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
    IfMatch:
      name: If-Match
      in: header
      description: Exact ETag returned for the current project or deployment state.
      required: true
      schema:
        type: string
        minLength: 3
        maxLength: 128
    ConfirmationToken:
      name: X-Confirmation-Token
      in: header
      description: Ten-minute token bound to the planned action, project, target resource, and resource ETag.
      required: true
      schema:
        $ref: "#/components/schemas/ConfirmationToken"
    RequestId:
      name: X-Request-Id
      in: header
      description: Optional caller-provided correlation identifier.
      required: false
      schema:
        type: string
        minLength: 1
        maxLength: 128
    ProjectId:
      name: project_id
      in: path
      description: Project identifier.
      required: true
      schema:
        $ref: "#/components/schemas/Ulid"
    ProjectCursor:
      name: cursor
      in: query
      description: Opaque continuation cursor returned by the preceding project page.
      required: false
      schema:
        $ref: "#/components/schemas/Ulid"
    ProjectLimit:
      name: limit
      in: query
      description: Maximum number of projects to return.
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    EnvironmentId:
      name: environment_id
      in: path
      description: Environment identifier.
      required: true
      schema:
        $ref: "#/components/schemas/Ulid"
    SecretName:
      name: secret_name
      in: path
      description: Uppercase environment-variable name.
      required: true
      schema:
        type: string
        pattern: ^[A-Z][A-Z0-9_]{0,127}$
    OperationId:
      name: operation_id
      in: path
      description: Operation identifier.
      required: true
      schema:
        $ref: "#/components/schemas/Ulid"
    DeploymentId:
      name: deployment_id
      in: path
      description: Deployment identifier.
      required: true
      schema:
        $ref: "#/components/schemas/Ulid"
    TargetDeploymentId:
      name: target_deployment_id
      in: path
      description: Immutable deployment selected as the rollback target.
      required: true
      schema:
        $ref: "#/components/schemas/Ulid"
    SourceDeploymentId:
      name: source_deployment_id
      in: path
      required: true
      description: Current succeeded dev deployment whose immutable artifact will be promoted.
      schema:
        $ref: "#/components/schemas/Ulid"
    DeploymentCursor:
      name: cursor
      in: query
      description: Opaque continuation cursor returned by the preceding deployment page.
      required: false
      schema:
        type: string
        pattern: ^[A-Za-z0-9_-]+$
    DeploymentLimit:
      name: limit
      in: query
      description: Maximum number of deployments to return.
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    ProjectFilter:
      name: project_id
      in: query
      description: Restricts the page to one visible project.
      required: false
      schema:
        $ref: "#/components/schemas/Ulid"
    OperationFilter:
      name: operation_id
      in: query
      description: Restricts the page to one visible operation.
      required: false
      schema:
        $ref: "#/components/schemas/Ulid"
  responses:
    AcceptedOperation:
      description: The mutation was accepted for asynchronous processing.
      headers:
        Location:
          description: Relative URL of the durable operation resource.
          required: true
          schema:
            type: string
            pattern: ^/v1/operations/[0-9A-HJKMNP-TV-Z]{26}$
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Operation"
    Problem:
      description: The request failed.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
    ResourceNotFound:
      description: The resource does not exist or is not visible to the authenticated principal.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          examples:
            resourceNotFound:
              value:
                type: https://docs.ohmyho.st/errors/resource-not-found
                title: Resource not found
                status: 404
                code: resource_not_found
                request_id: req_01J00000000000000000000000
                retryable: false
                suggested_action: Check the resource identifier and your access scope.
    IdempotencyConflict:
      description: The idempotency key was already used with a different canonical request.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/IdempotencyConflictProblem"
          examples:
            reusedKey:
              value:
                type: https://docs.ohmyho.st/errors/idempotency-key-reused
                title: Idempotency key reused
                status: 409
                code: idempotency_key_reused
                request_id: req_01J00000000000000000000000
                retryable: false
                suggested_action: Retry with a new idempotency key.
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: WorkOS access JWT or a user-owned WorkOS API key. User keys are bound to one organization and restricted to their enabled product permissions. Session-only onboarding and session revocation require an interactive access JWT. No cookie session is assumed.
  schemas:
    FeatureVoteChoices:
      type: array
      minItems: 3
      maxItems: 3
      description: One entry for each roadmap topic; no personal data or other browsers' votes.
      items:
        type: object
        additionalProperties: false
        required: [feature, choice]
        properties:
          feature: { type: string, enum: [eu, iso27001, soc2] }
          choice: { type: [string, "null"], enum: [up, down, null] }
    FeatureVoteState:
      type: object
      additionalProperties: false
      required: [votes]
      properties:
        votes:
          $ref: "#/components/schemas/FeatureVoteChoices"
    UserApiKeyId:
      type: string
      pattern: "^api_key_[A-Za-z0-9_]{1,120}$"
    UserApiKey:
      type: object
      additionalProperties: false
      required:
        [
          id,
          organization_id,
          name,
          obfuscated_value,
          permissions,
          expires_at,
          created_at,
          last_used_at,
        ]
      properties:
        id: { $ref: "#/components/schemas/UserApiKeyId" }
        organization_id: { $ref: "#/components/schemas/Ulid" }
        name: { type: string, minLength: 1, maxLength: 128 }
        obfuscated_value: { type: string, pattern: '^sk_(?:\.\.\.|…)[A-Za-z0-9_-]{1,12}$' }
        permissions:
          type: array
          maxItems: 100
          uniqueItems: true
          items: { type: string, pattern: "^[A-Za-z0-9_.*:-]{1,128}$" }
        expires_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        last_used_at: { type: [string, "null"], format: date-time }
    UserApiKeyCreation:
      type: object
      additionalProperties: false
      required: [request_id, replayed, key, value]
      properties:
        request_id: { $ref: "#/components/schemas/Ulid" }
        replayed: { type: boolean }
        key: { $ref: "#/components/schemas/UserApiKey" }
        value: { type: [string, "null"], pattern: "^sk_[A-Za-z0-9_-]{20,128}$" }
      oneOf:
        - properties: { replayed: { const: false }, value: { type: string } }
        - properties: { replayed: { const: true }, value: { type: "null" } }
    UserApiKeyPage:
      type: object
      additionalProperties: false
      required: [data, next_cursor]
      properties:
        data:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/UserApiKey" }
        next_cursor: { type: [string, "null"], pattern: "^api_key_[A-Za-z0-9_]{1,120}$" }
    CreditMicros:
      type: string
      pattern: ^(0|[1-9][0-9]{0,18})$
      description: Non-negative microcredits, at most 9223372036854775807. One credit equals 1000000 microcredits.
    BillingRecharge:
      type: object
      additionalProperties: false
      required:
        [
          organization_id,
          enabled,
          revision,
          status,
          monthly_limit_minor,
          spent_minor,
          currency,
          amount_minor,
          credits,
          threshold_credits,
          setup_url,
          invoice_url,
        ]
      properties:
        organization_id: { $ref: "#/components/schemas/Ulid" }
        enabled: { type: boolean }
        revision: { type: integer, minimum: 0 }
        status:
          {
            type: string,
            enum: [off, setup_required, on, payment_required, monthly_limit, needs_reconciliation],
          }
        monthly_limit_minor: { type: integer, minimum: 1000, maximum: 100000 }
        spent_minor: { type: integer, minimum: 0 }
        currency: { type: string, const: usd }
        amount_minor: { type: integer, const: 900 }
        credits: { type: integer, const: 1000 }
        threshold_credits: { type: integer, const: 100 }
        setup_url: { type: [string, "null"], format: uri, maxLength: 8192 }
        invoice_url: { type: [string, "null"], format: uri, maxLength: 8192 }
    BillingCheckout:
      type: object
      additionalProperties: false
      required:
        [
          organization_id,
          checkout_id,
          offer,
          state,
          payment_confirmed,
          url,
          expires_at,
          packs,
          credited_micros,
          revoked_micros,
          paid_until,
          required_action,
        ]
      properties:
        organization_id: { $ref: "#/components/schemas/Ulid" }
        checkout_id: { $ref: "#/components/schemas/Ulid" }
        offer: { type: string, enum: [topup, paid] }
        state: { type: string, enum: [open, complete, expired] }
        payment_confirmed: { type: boolean }
        url: { type: [string, "null"], format: uri, maxLength: 8192 }
        expires_at: { type: string, format: date-time }
        packs: { type: integer, minimum: 1, maximum: 100 }
        credited_micros: { type: string, pattern: "^(0|[1-9][0-9]{0,18})$" }
        revoked_micros: { type: string, pattern: "^(0|[1-9][0-9]{0,18})$" }
        paid_until: { type: [string, "null"], format: date-time }
        required_action:
          type: string
          enum: [none, complete_checkout, open_billing_portal]
          description: Human payment handoff. Complete the returned Checkout URL, or request a fresh billing portal URL to resolve an unpaid renewal. none includes a draft invoice awaiting automatic processing; it is not a claim of current Paid coverage. Never infer renewal success from the historical payment_confirmed flag.
    BillingPortal:
      type: object
      additionalProperties: false
      required: [organization_id, url, created_at]
      properties:
        organization_id: { $ref: "#/components/schemas/Ulid" }
        url: { type: string, format: uri, maxLength: 8192 }
        created_at: { type: string, format: date-time }
    OrganizationCredits:
      type: object
      additionalProperties: false
      required:
        [
          organization_id,
          unit,
          as_of,
          available_micros,
          reserved_micros,
          spent_micros,
          expired_micros,
          platform_overrun_micros,
          grace_started_at,
          grace_expires_at,
          active_meters,
          rate_cards,
        ]
      properties:
        organization_id:
          $ref: "#/components/schemas/Ulid"
        unit:
          type: string
          const: microcredits
        as_of:
          type: string
          format: date-time
        available_micros:
          $ref: "#/components/schemas/CreditMicros"
        reserved_micros:
          $ref: "#/components/schemas/CreditMicros"
        spent_micros:
          $ref: "#/components/schemas/CreditMicros"
        expired_micros:
          $ref: "#/components/schemas/CreditMicros"
        platform_overrun_micros:
          $ref: "#/components/schemas/CreditMicros"
        grace_started_at:
          type: [string, "null"]
          format: date-time
          description: First confirmed exhaustion in this continuous period; null while funded.
        grace_expires_at:
          type: [string, "null"]
          format: date-time
          description: Seven days after first confirmed exhaustion. Existing services and domains are retained during this grace; no automatic data deletion. Explicit project stop budgets are separate.
        active_meters:
          type: array
          items:
            type: string
            pattern: ^[a-z0-9_.-]{1,128}$
        rate_cards:
          type: array
          maxItems: 4
          description: Published integer prices, newest first. A published rate is not proof of enabled billing; only active_meters names currently billed sources. Builds retain their accepted quote. Provider cost is a list-price basis before platform allowances, not an invoice.
          items:
            $ref: "#/components/schemas/PublishedCreditRateCard"
    UsageTotal:
      type: string
      description: Non-negative integer aggregate, serialized exactly without floating-point rounding.
      pattern: "^(0|[1-9][0-9]{0,37})$"
    OrganizationCreditUsage:
      type: object
      additionalProperties: false
      required: [organization_id, month, as_of, unit, data, next_cursor]
      properties:
        organization_id:
          $ref: "#/components/schemas/Ulid"
        month:
          type: string
          pattern: "^20[0-9]{2}-(0[1-9]|1[0-2])$"
        as_of:
          type: string
          format: date-time
        unit:
          type: string
          const: microcredits
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Ulid"
            - type: "null"
        data:
          type: array
          maxItems: 20
          items:
            type: object
            additionalProperties: false
            required: [project_id, reserved_micros, meters]
            properties:
              project_id:
                $ref: "#/components/schemas/Ulid"
              reserved_micros:
                $ref: "#/components/schemas/UsageTotal"
              meters:
                type: array
                maxItems: 1000
                items:
                  type: object
                  additionalProperties: false
                  required:
                    [
                      environment_id,
                      meter,
                      unit,
                      rate_card_id,
                      quantity,
                      charged_micros,
                      funded_micros,
                      platform_overrun_micros,
                    ]
                  properties:
                    environment_id:
                      oneOf:
                        - $ref: "#/components/schemas/Ulid"
                        - type: "null"
                    meter:
                      type: string
                      pattern: "^[A-Za-z0-9_.:/-]{1,128}$"
                    unit:
                      type: string
                      pattern: "^[A-Za-z0-9_.:/-]{1,128}$"
                    rate_card_id:
                      type: string
                      pattern: "^[A-Za-z0-9_.:/-]{1,128}$"
                    quantity:
                      $ref: "#/components/schemas/UsageTotal"
                    charged_micros:
                      $ref: "#/components/schemas/UsageTotal"
                    funded_micros:
                      $ref: "#/components/schemas/UsageTotal"
                    platform_overrun_micros:
                      $ref: "#/components/schemas/UsageTotal"
    PublishedCreditRateCard:
      type: object
      additionalProperties: false
      required: [id, currency, published_at, effective_from, rates]
      properties:
        id:
          type: string
          pattern: ^[a-z0-9_.-]{1,128}$
        currency:
          type: string
          const: USD
        published_at:
          type: string
          format: date-time
        effective_from:
          type: string
          format: date-time
        rates:
          type: array
          minItems: 1
          maxItems: 32
          items:
            type: object
            additionalProperties: false
            required: [meter, unit, units_per_charge, provider_cost_micros, credit_micros]
            properties:
              meter:
                type: string
                pattern: ^[a-z0-9_.-]{1,128}$
              unit:
                type: string
                pattern: ^[a-z0-9_.-]{1,128}$
              units_per_charge:
                type: string
                pattern: ^[1-9][0-9]{0,18}$
              provider_cost_micros:
                type: string
                pattern: ^(0|[1-9][0-9]{0,18})$
                description: Provider list-cost basis for units_per_charge, in USD micro-units (1000000 equals 1 USD), before shared allowances and discounts.
              credit_micros:
                $ref: "#/components/schemas/CreditMicros"
    SetProjectCreditBudgetRequest:
      type: object
      additionalProperties: false
      required: [amount_micros, mode]
      properties:
        amount_micros:
          oneOf:
            - $ref: "#/components/schemas/CreditMicros"
            - type: "null"
        mode:
          type: string
          enum: [continue, stop]
    ProjectCreditBudget:
      type: object
      additionalProperties: false
      required:
        [
          project_id,
          amount_micros,
          mode,
          used_micros,
          reserved_micros,
          period_start,
          period_end,
          as_of,
        ]
      properties:
        project_id:
          $ref: "#/components/schemas/Ulid"
        amount_micros:
          oneOf:
            - $ref: "#/components/schemas/CreditMicros"
            - type: "null"
        mode:
          type: string
          enum: [continue, stop]
        used_micros:
          $ref: "#/components/schemas/CreditMicros"
        reserved_micros:
          $ref: "#/components/schemas/CreditMicros"
        period_start:
          type: string
          format: date-time
        period_end:
          type: string
          format: date-time
        as_of:
          type: string
          format: date-time
    CreateCloudflareDnsAuthorizationRequest:
      type: object
      additionalProperties: false
      required:
        - zone
      properties:
        zone:
          type: string
          minLength: 4
          maxLength: 253
          pattern: '^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$'
    CloudflareDnsAuthorization:
      type: object
      description: Short-lived credential-free redirect information for one Cloudflare OAuth authorization.
      additionalProperties: false
      required:
        - authorization_id
        - authorization_url
        - expires_at
      properties:
        authorization_id:
          $ref: "#/components/schemas/Ulid"
        authorization_url:
          type: string
          format: uri
          minLength: 20
          maxLength: 4096
          pattern: '^https://dash\.cloudflare\.com/oauth2/auth\?'
        expires_at:
          type: string
          format: date-time
    CloudflareDnsAuthorizationStatus:
      type: object
      description: Credential-free project authorization state. Without an authorization, status is not_authorized, zone and expires_at are null, and scopes is empty. All other states name the actual customer zone; no platform or customer zone is inferred.
      additionalProperties: false
      required:
        - status
        - zone
        - scopes
        - expires_at
      properties:
        status:
          type: string
          enum:
            - not_authorized
            - pending
            - authorized
            - expired
            - revoked
        zone:
          type: [string, "null"]
          minLength: 4
          maxLength: 253
          pattern: '^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$'
        scopes:
          type: array
          uniqueItems: true
          maxItems: 2
          items:
            type: string
            enum:
              - dns.write
              - zone.read
        expires_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
    PaidDomainRequest:
      type: object
      additionalProperties: false
      required: [hostname]
      properties:
        hostname:
          type: string
          minLength: 4
          maxLength: 253
          pattern: '^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$'
    PaidDomainPlan:
      type: object
      additionalProperties: false
      required: [status, hostname, environment, cname_target, effects, risks]
      properties:
        status: { type: string, const: planned }
        hostname: { type: string, minLength: 4, maxLength: 253 }
        environment: { type: string, const: prod }
        cname_target: { type: string, enum: [customers.omh.st, customers.ohmyho.st] }
        effects:
          type: array
          minItems: 1
          maxItems: 8
          items: { type: string, minLength: 1, maxLength: 512 }
        risks:
          type: array
          minItems: 1
          maxItems: 8
          items: { type: string, minLength: 1, maxLength: 512 }
    PaidDomain:
      type: object
      additionalProperties: false
      required:
        - status
        - suspension_reason
        - hostname
        - environment
        - cname_target
        - custom_hostname_status
        - ssl_status
        - validation_records
        - url
      properties:
        status:
          type: string
          enum: [not_configured, pending, active, reconciliation_required, deleted, suspended]
        suspension_reason:
          type: [string, "null"]
          enum:
            [paid_plan_required, insufficient_organization_credits, project_budget_exceeded, null]
          description: Non-null only while suspended by the same Paid, credit-grace or stop-budget policy used by the gateway. DNS/TLS receipts remain intact; url is null while suspended.
        hostname:
          oneOf:
            - type: string
              minLength: 4
              maxLength: 253
            - type: "null"
        environment: { type: string, const: prod }
        cname_target: { type: string, enum: [customers.omh.st, customers.ohmyho.st] }
        custom_hostname_status:
          type: [string, "null"]
        ssl_status:
          type: [string, "null"]
        validation_records:
          type: array
          maxItems: 16
          items:
            type: object
            additionalProperties: false
            required: [type, name, content, purpose]
            properties:
              type: { type: string, enum: [CNAME, TXT] }
              name: { type: string, minLength: 1, maxLength: 253 }
              content: { type: string, minLength: 1, maxLength: 2048 }
              purpose: { type: string, enum: [traffic, ownership, ssl, dcv_delegation] }
        url:
          oneOf:
            - type: string
              format: uri
              pattern: ^https://
            - type: "null"
    ConfigureProjectMailDomainRequest:
      type: object
      additionalProperties: false
      required:
        - domain
      properties:
        domain:
          type: string
          minLength: 4
          maxLength: 253
          pattern: '^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$'
    ProjectMailDomain:
      oneOf:
        - $ref: "#/components/schemas/ConfiguredProjectMailDomain"
        - $ref: "#/components/schemas/DelegatedProjectMailDomain"
    ConfiguredProjectMailDomain:
      type: object
      additionalProperties: false
      required:
        - domain
        - status
        - configured_at
      properties:
        domain:
          type: string
          minLength: 4
          maxLength: 253
        status:
          type: string
          const: configured
        configured_at:
          type: string
          format: date-time
    DelegatedProjectMailDomain:
      type: object
      additionalProperties: false
      required:
        - domain
        - status
        - configured_at
        - name_servers
        - change_id
      properties:
        domain:
          type: string
          minLength: 4
          maxLength: 253
        status:
          type: string
          enum:
            - delegation_required
            - verification_pending
            - verification_failed
            - ready
        configured_at:
          type: string
          format: date-time
        name_servers:
          type: array
          minItems: 4
          maxItems: 4
          uniqueItems: true
          items:
            type: string
            pattern: '^ns-[0-9]{1,4}\.awsdns-[0-9]{1,2}\.(?:com|net|org|co\.uk)$'
        change_id:
          type: string
          pattern: ^C[A-Z0-9]{8,31}$
        observed_at:
          type: string
          format: date-time
          description: Time of the fresh read-only provider verification, not the original configuration time.
        next_check_after_seconds:
          type: [integer, "null"]
          enum: [3600, null]
          description: Ask the agent to check pending or failed verification again after one hour; null when ready.
        verification_issue:
          type: [string, "null"]
          enum:
            [
              identity_missing,
              identity_verification_failed,
              dkim_verification_failed,
              dkim_records_changed,
              tenant_association_missing,
              null,
            ]
          description: A missing tenant association is platform provisioning, not a request to rewrite DNS. Read the original operation and its failure action.
    PutEnvironmentSecretRequest:
      type: object
      additionalProperties: false
      required:
        - value
      properties:
        value:
          type: string
          description: Write-only plaintext value. It is encrypted before persistence and never returned.
          minLength: 1
          maxLength: 5120
          writeOnly: true
    EnvironmentSecret:
      type: object
      description: Environment secret metadata. The plaintext and ciphertext are never returned.
      additionalProperties: false
      required:
        - name
        - revision
        - key_version
        - created_at
        - updated_at
        - desired_generation
        - applied_generation
        - delivery_state
        - runtime_provider
        - applied_secret_count
        - last_error
      properties:
        name:
          type: string
          pattern: ^[A-Z][A-Z0-9_]{0,127}$
        revision:
          type: integer
          minimum: 1
        key_version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        desired_generation:
          type: integer
          minimum: 0
        applied_generation:
          type: integer
          minimum: 0
        delivery_state:
          type: string
          enum: [not_deployed, pending, ready, failed]
        runtime_provider:
          type: [string, "null"]
          enum: [wfp, container, null]
        applied_secret_count:
          type: integer
          minimum: 0
          maximum: 100
        last_error:
          type: [string, "null"]
          pattern: ^[A-Z][A-Z0-9_]{0,63}$
    EnvironmentSecretPage:
      type: object
      additionalProperties: false
      required:
        - items
        - desired_generation
        - applied_generation
        - delivery_state
        - runtime_provider
        - applied_secret_count
        - last_error
      properties:
        items:
          type: array
          maxItems: 256
          items:
            $ref: "#/components/schemas/EnvironmentSecret"
        desired_generation:
          type: integer
          minimum: 0
        applied_generation:
          type: integer
          minimum: 0
        delivery_state:
          type: string
          enum: [not_deployed, pending, ready, failed]
        runtime_provider:
          type: [string, "null"]
          enum: [wfp, container, null]
        applied_secret_count:
          type: integer
          minimum: 0
          maximum: 100
        last_error:
          type: [string, "null"]
          pattern: ^[A-Z][A-Z0-9_]{0,63}$
    DeleteEnvironmentSecretResult:
      type: object
      additionalProperties: false
      required:
        - name
        - status
        - desired_generation
        - applied_generation
        - delivery_state
        - runtime_provider
        - applied_secret_count
        - last_error
      properties:
        name:
          type: string
          pattern: ^[A-Z][A-Z0-9_]{0,127}$
        status:
          type: string
          enum:
            - deleted
            - absent
        desired_generation:
          type: integer
          minimum: 0
        applied_generation:
          type: integer
          minimum: 0
        delivery_state:
          type: string
          enum: [not_deployed, pending, ready, failed]
        runtime_provider:
          type: [string, "null"]
          enum: [wfp, container, null]
        applied_secret_count:
          type: integer
          minimum: 0
          maximum: 100
        last_error:
          type: [string, "null"]
          pattern: ^[A-Z][A-Z0-9_]{0,63}$
    LinkProjectSourceRequest:
      type: object
      additionalProperties: false
      required:
        - repository_owner
        - repository_name
      properties:
        repository_owner:
          type: string
          minLength: 1
          maxLength: 39
          pattern: ^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$
        repository_name:
          type: string
          minLength: 1
          maxLength: 100
          pattern: ^[A-Za-z0-9._-]+$
    ConfigureSourceAutoDeployRequest:
      type: object
      additionalProperties: false
      required: [branch, enabled]
      properties:
        branch:
          type: string
          description: Exact Git branch accepted by Git check-ref-format branch rules; control/space, dot-lock, reflog, range and metacharacter forms are rejected.
          minLength: 1
          maxLength: 1024
          pattern: '^(?!.*(?:\.\.|@\{|//|\\|[~^:?*\[\]\x00-\x20\x7f]))(?!.*(?:^|/)\.)(?!.*(?:^|/)[^/]*\.lock(?:/|$))(?!.*\.$)[A-Za-z0-9][A-Za-z0-9._/-]{0,1023}$'
        enabled:
          type: boolean
    SourceAutoDeploy:
      type: object
      additionalProperties: false
      required: [branch, ref, environment, enabled, generation, grant_status, updated_at]
      properties:
        branch:
          type: string
        ref:
          type: string
          pattern: ^refs/heads/
        environment:
          type: string
          enum: [dev]
        enabled:
          type: boolean
        generation:
          type: integer
          minimum: 1
        grant_status:
          type: string
          enum: [active, revoked]
        updated_at:
          type: string
          format: date-time
    GithubSourceAuthorization:
      type: object
      additionalProperties: false
      required:
        [
          authorization_id,
          status,
          authorization_url,
          installation_url,
          expires_at,
          operation_id,
          installation_id,
          repository_id,
          repository_owner,
          repository_name,
        ]
      properties:
        authorization_id:
          $ref: "#/components/schemas/Ulid"
        status:
          type: string
          enum: [pending, authorizing, authorized, failed, expired]
        authorization_url:
          type: [string, "null"]
          format: uri
          description: Open only when pending; contains the single-use state and PKCE challenge, never a provider credential.
        installation_url:
          type: [string, "null"]
          format: uri
          description: Install or update the GitHub App's selected repository access before authorizing, if required.
        expires_at:
          type: string
          format: date-time
        operation_id:
          type: [string, "null"]
          pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
        installation_id:
          type: [string, "null"]
          pattern: ^[1-9][0-9]*$
        repository_id:
          type: [string, "null"]
          pattern: ^[1-9][0-9]*$
        repository_owner:
          type: string
        repository_name:
          type: string
    ProjectSource:
      type: object
      additionalProperties: false
      required:
        - provider
        - installation_id
        - repository_full_name
        - status
        - linked_at
        - updated_at
      properties:
        provider:
          type: string
          const: github
        installation_id:
          type: string
          pattern: ^[1-9][0-9]*$
          maxLength: 20
        repository_full_name:
          type: string
          minLength: 3
          maxLength: 201
          pattern: ^[^/]+/[^/]+$
        status:
          type: string
          enum:
            - pending
            - ready
            - failed
            - revoked
        linked_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        failure_summary:
          type: string
          minLength: 1
          maxLength: 512
    DeploymentDiagnostic:
      type: object
      additionalProperties: false
      required: [id, source, code, message, occurred_at]
      properties:
        id: { $ref: "#/components/schemas/Ulid" }
        source: { type: string, enum: [build, control, runtime, function] }
        code: { type: string, pattern: "^[A-Z][A-Z0-9_]{1,63}$" }
        message: { type: string, pattern: "^[A-Z][A-Z0-9_]{1,127}$" }
        exception_class: { type: string, maxLength: 128 }
        file: { type: string, maxLength: 512 }
        line: { type: integer, minimum: 1 }
        column: { type: integer, minimum: 0 }
        route: { type: string, maxLength: 512 }
        request_id: { type: string, maxLength: 128 }
        trace_id: { type: string, maxLength: 128 }
        duration_ms: { type: integer, minimum: 0, maximum: 900000 }
        occurred_at: { type: string, format: date-time }
    DeploymentDiagnosticPage:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          maxItems: 100
          items: { $ref: "#/components/schemas/DeploymentDiagnostic" }
        next_cursor: { type: string, maxLength: 2048 }
    ProjectDatabaseQueryRequest:
      type: object
      additionalProperties: false
      required: [environment, statement, parameters]
      properties:
        environment:
          type: string
          const: dev
        statement:
          type: string
          minLength: 1
          maxLength: 4096
          pattern: ^\s*(?:SELECT|WITH)\b
        parameters:
          type: array
          maxItems: 32
          items:
            oneOf:
              - type: string
              - type: number
              - type: boolean
              - type: "null"
    ProjectDatabaseQueryResult:
      type: object
      additionalProperties: false
      required: [environment, rows, row_count, truncated]
      properties:
        environment:
          type: string
          const: dev
        rows:
          type: array
          maxItems: 100
          items:
            type: object
            additionalProperties: true
        row_count:
          type: integer
          minimum: 0
          maximum: 100
        truncated:
          type: boolean
    DevAccessTicket:
      type: object
      additionalProperties: false
      required:
        - ticket_id
        - project_id
        - environment_id
        - origin
        - redeem_url
        - expires_at
      properties:
        ticket_id:
          $ref: "#/components/schemas/Ulid"
        project_id:
          $ref: "#/components/schemas/Ulid"
        environment_id:
          $ref: "#/components/schemas/Ulid"
        origin:
          type: string
          format: uri
          pattern: ^https://dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st$
        redeem_url:
          type: string
          format: uri
          pattern: ^https://dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st/\.ohmyhost/dev-access/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$
        expires_at:
          type: string
          format: date-time
    PlanDeploymentRequest:
      type: object
      additionalProperties: false
      required:
        - commit_sha
      properties:
        commit_sha:
          $ref: "#/components/schemas/GitCommitSha"
    CreateDeploymentRequest:
      type: object
      additionalProperties: false
      required:
        - plan_id
      properties:
        plan_id:
          $ref: "#/components/schemas/Ulid"
    DeploymentPlan:
      type: object
      description: Immutable pre-build deployment plan. It expires exactly fifteen minutes after creation.
      additionalProperties: false
      x-ohmyhost-ttl-seconds: 900
      required:
        - id
        - project_id
        - commit_sha
        - source_digest
        - build_plan_digest
        - application_root
        - runtime
        - route
        - resource_effects
        - limits
        - estimated_cost
        - risks
        - destructive_effects
        - required_confirmations
        - created_at
        - expires_at
      properties:
        id:
          $ref: "#/components/schemas/Ulid"
        project_id:
          $ref: "#/components/schemas/Ulid"
        commit_sha:
          $ref: "#/components/schemas/GitCommitSha"
        source_digest:
          $ref: "#/components/schemas/Sha256Digest"
        build_plan_digest:
          $ref: "#/components/schemas/Sha256Digest"
        application_root:
          type: string
          description: Canonical repository-relative application root selected from immutable source evidence.
          readOnly: true
          maxLength: 64
          pattern: '^(?:\.|[A-Za-z0-9][A-Za-z0-9._-]{0,62}(?:/[A-Za-z0-9][A-Za-z0-9._-]{0,62}){0,7})$'
        artifact_digest:
          $ref: "#/components/schemas/Sha256Digest"
        runtime:
          type: string
          enum:
            - cloudflare_workers_static_assets
            - cloudflare_workers_edge_ssr
            - cloudflare_containers
        route:
          type: string
          description: Server-derived project-isolated public deployment URL; clients cannot select its gateway host or target Worker.
          format: uri
          pattern: ^https://
        resource_effects:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 512
        limits:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 512
        estimated_cost:
          $ref: "#/components/schemas/CostEstimate"
        risks:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 512
        destructive_effects:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 512
        required_confirmations:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 128
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
    CostEstimate:
      type: object
      description: Build-compute reservation quote, not the lifetime cost of the application. amount_micros is the customer USD equivalent of credit_micros; provider_cost_micros is the corresponding published provider-list basis. The versioned price is fixed by this immutable deployment plan. Actual measured usage settles the reservation and unused credits are released. Storage, mail and runtime costs are separate.
      additionalProperties: false
      required:
        - amount_micros
        - currency
        - rate_card_version
        - credit_micros
        - provider_cost_micros
        - scope
        - reserved_minutes
      properties:
        amount_micros:
          type: string
          pattern: ^[0-9]+$
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        rate_card_version:
          type: string
          minLength: 1
          maxLength: 128
        credit_micros:
          type: string
          pattern: ^[0-9]+$
        provider_cost_micros:
          type: string
          pattern: ^[0-9]+$
        scope:
          type: string
          const: build_compute
        reserved_minutes:
          type: string
          pattern: ^[1-9][0-9]*$
    Deployment:
      type: object
      additionalProperties: false
      required:
        - id
        - project_id
        - operation_id
        - commit_sha
        - source_digest
        - build_plan_digest
        - status
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Ulid"
        project_id:
          $ref: "#/components/schemas/Ulid"
        operation_id:
          $ref: "#/components/schemas/Ulid"
        commit_sha:
          $ref: "#/components/schemas/GitCommitSha"
        source_digest:
          $ref: "#/components/schemas/Sha256Digest"
        build_plan_digest:
          $ref: "#/components/schemas/Sha256Digest"
        artifact_digest:
          $ref: "#/components/schemas/Sha256Digest"
        status:
          type: string
          enum:
            - queued
            - building
            - publishing
            - active
            - failed
            - rolled_back
            - deleting
            - deleted
            - reconciliation_required
        url:
          type: string
          description: Project-isolated public gateway URL for the observed deployment.
          format: uri
          pattern: ^https://
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DeploymentPage:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Deployment"
        next_cursor:
          type:
            - string
            - "null"
          pattern: ^[A-Za-z0-9_-]+$
    GuardedActionPlan:
      type: object
      description: Non-mutating guarded-action plan with a confirmation token that expires exactly ten minutes after creation.
      additionalProperties: false
      x-ohmyhost-ttl-seconds: 600
      required:
        - action
        - project_id
        - resource_etag
        - effects
        - risks
        - confirmation_token
        - created_at
        - expires_at
      properties:
        action:
          type: string
          enum:
            - rollback
            - promote
            - delete
        project_id:
          $ref: "#/components/schemas/Ulid"
        target_deployment_id:
          $ref: "#/components/schemas/Ulid"
        target_artifact_digest:
          $ref: "#/components/schemas/Sha256Digest"
        source_deployment_id:
          $ref: "#/components/schemas/Ulid"
        source_artifact_digest:
          $ref: "#/components/schemas/Sha256Digest"
        source_environment:
          type: string
          const: dev
        target_environment:
          type: string
          const: prod
        resource_etag:
          type: string
          minLength: 3
          maxLength: 128
        effects:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 512
        risks:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 512
        confirmation_token:
          $ref: "#/components/schemas/ConfirmationToken"
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
      oneOf:
        - properties:
            action:
              const: rollback
          required:
            - target_deployment_id
            - target_artifact_digest
        - properties:
            action:
              const: promote
          required:
            - source_deployment_id
            - source_artifact_digest
            - source_environment
            - target_environment
        - properties:
            action:
              const: delete
          not:
            anyOf:
              - required:
                  - target_deployment_id
              - required:
                  - target_artifact_digest
              - required:
                  - source_deployment_id
              - required:
                  - source_artifact_digest
              - required:
                  - source_environment
              - required:
                  - target_environment
    ConfirmationToken:
      type: string
      description: Opaque, single-action token bound to the planned action, project, target resource, and resource ETag.
      minLength: 32
      maxLength: 4096
      pattern: ^[A-Za-z0-9._~-]+$
    GitCommitSha:
      type: string
      description: Exact full Git commit object identifier; abbreviated or branch references are forbidden.
      pattern: ^[0-9a-f]{40}$
    Sha256Digest:
      type: string
      pattern: ^sha256:[0-9a-f]{64}$
    ProjectDataMode:
      type: string
      enum: [shared, isolated]
      default: shared
      description: Chosen when the project is created. shared uses one database and a shared file namespace across Dev/Prod; isolated keeps their database/Auth records and files separate. Skills recommend isolated development, but the customer chooses the additional measured database consumption. Promotion applies pending schema migrations to an isolated Prod database without copying Dev data. Changing an established mode requires an explicit data migration.
    FeedbackSubmission:
      type: object
      additionalProperties: false
      required: [organization_id, kind, title, description]
      dependentRequired:
        environment_id: [project_id]
        operation_id: [project_id]
      properties:
        organization_id:
          $ref: "#/components/schemas/Ulid"
        project_id:
          $ref: "#/components/schemas/Ulid"
        environment_id:
          $ref: "#/components/schemas/Ulid"
        operation_id:
          $ref: "#/components/schemas/Ulid"
        kind:
          type: string
          enum: [bug, issue, feature_request]
        title:
          type: string
          minLength: 1
          maxLength: 160
          description: Trimmed title without control characters.
        description:
          type: string
          minLength: 1
          maxLength: 8000
          description: Trimmed, redacted expected/actual behavior and minimal reproduction. Line breaks and tabs are accepted; other control characters are rejected. Never include raw logs, environment files, credentials or personal records.
        error_code:
          type: string
          pattern: "^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$"
          maxLength: 128
        client_version:
          type: string
          pattern: "^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$"
          maxLength: 128
    FeedbackReceipt:
      type: object
      additionalProperties: false
      required: [id, organization_id, submitted_at]
      properties:
        id:
          $ref: "#/components/schemas/Ulid"
        organization_id:
          $ref: "#/components/schemas/Ulid"
        submitted_at:
          type: string
          format: date-time
    CreateProjectRequest:
      type: object
      description: Phase 1 project creation request. The server assigns resource identifiers.
      additionalProperties: false
      required:
        - organization_id
        - name
      properties:
        organization_id:
          $ref: "#/components/schemas/Ulid"
        data_mode:
          $ref: "#/components/schemas/ProjectDataMode"
        name:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^\\S(?:.*\\S)?$"
    CreateOrganizationRequest:
      type: object
      additionalProperties: false
      required: [name, signup_source]
      properties:
        signup_source:
          type: string
          pattern: "^[a-z0-9][a-z0-9_-]{0,63}$"
          minLength: 1
          maxLength: 64
          description: Public acquisition source from the invitation URL's r parameter. Eligibility and promotional credits are server-configured; attribution is fixed for the user and rewards are never multiplied by retries or additional organizations.
        name:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^\\S(?:.*\\S)?$"
    Organization:
      type: object
      additionalProperties: false
      required: [id, name]
      properties:
        id:
          $ref: "#/components/schemas/Ulid"
        name:
          type: string
          minLength: 1
          maxLength: 128
    AccountProfile:
      type: object
      additionalProperties: false
      required:
        [
          user_id,
          name,
          email,
          email_verified,
          organizations,
          organization_ids,
          signup_source,
          attributed_at,
          initial_workspace_id,
        ]
      properties:
        user_id: { type: string, pattern: "^user_[A-Za-z0-9_]{1,123}$" }
        name: { type: string, maxLength: 256 }
        email: { type: string, format: email, maxLength: 254 }
        email_verified: { type: boolean }
        organizations:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [id, workos_id, name]
            properties:
              id: { $ref: "#/components/schemas/Ulid" }
              workos_id: { type: string }
              name: { type: string }
        organization_ids: { type: array, items: { $ref: "#/components/schemas/Ulid" } }
        signup_source: { type: [string, "null"], maxLength: 64 }
        attributed_at: { type: [string, "null"], format: date-time }
        initial_workspace_id: { type: [string, "null"], pattern: "^[0-9A-HJKMNP-TV-Z]{26}$" }
    OrganizationAccount:
      type: object
      additionalProperties: false
      required:
        [
          organization_id,
          name,
          workos_organization_id,
          plan,
          plan_source,
          paid_until,
          available_micros,
          monthly_micros,
          one_time_micros,
          reserved_micros,
          next_expiry,
          as_of,
        ]
      properties:
        organization_id: { $ref: "#/components/schemas/Ulid" }
        name: { type: string }
        workos_organization_id: { type: string }
        plan: { type: string, enum: [free, paid] }
        plan_source: { type: string, enum: [free, stripe, beta, manual] }
        paid_until: { type: [string, "null"], format: date-time }
        available_micros: { $ref: "#/components/schemas/CreditMicros" }
        monthly_micros: { $ref: "#/components/schemas/CreditMicros" }
        one_time_micros: { $ref: "#/components/schemas/CreditMicros" }
        reserved_micros: { $ref: "#/components/schemas/CreditMicros" }
        next_expiry: { type: [string, "null"], format: date-time }
        as_of: { type: string, format: date-time }
    CurrentIdentity:
      type: object
      description: Minimal customer-visible identity and organization scope for authenticated agent clients.
      additionalProperties: false
      required:
        - actor_id
        - organization_ids
      properties:
        actor_id:
          type: string
          minLength: 1
          maxLength: 256
        organization_ids:
          type: array
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Ulid"
    Project:
      type: object
      additionalProperties: false
      required:
        - id
        - organization_id
        - name
        - handle
        - data_mode
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Ulid"
        organization_id:
          $ref: "#/components/schemas/Ulid"
        name:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^\\S(?:.*\\S)?$"
        handle:
          type: string
          minLength: 5
          maxLength: 59
          pattern: ^[a-z]+-[a-z]+-[a-z]+$
        data_mode:
          $ref: "#/components/schemas/ProjectDataMode"
        created_at:
          type: string
          format: date-time
    ProjectPage:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Project"
        next_cursor:
          type:
            - string
            - "null"
          pattern: ^[0-9A-HJKMNP-TV-Z]{26}$
    ProjectEnvironment:
      type: object
      additionalProperties: false
      required:
        - id
        - name
      properties:
        id:
          $ref: "#/components/schemas/Ulid"
        name:
          type: string
          minLength: 1
          maxLength: 128
    ProjectStatus:
      type: object
      additionalProperties: false
      required:
        - project_id
        - handle
        - lifecycle
        - source
        - default_environment
        - head_deployment
        - dev_url
        - prod_url
        - latest_operation
        - cleanup_state
      properties:
        project_id:
          $ref: "#/components/schemas/Ulid"
        handle:
          type: string
          minLength: 5
          maxLength: 59
          pattern: ^[a-z]+-[a-z]+-[a-z]+$
        lifecycle:
          type: string
          enum:
            - active
            - deleting
            - deleted
        source:
          oneOf:
            - $ref: "#/components/schemas/ProjectSource"
            - type: "null"
        default_environment:
          oneOf:
            - $ref: "#/components/schemas/ProjectEnvironment"
            - type: "null"
        environments:
          type: array
          maxItems: 2
          description: Tenant-scoped Dev and Prod environment IDs for secrets and other environment-scoped commands. Select by name; never infer the Prod ID from the default Dev environment.
          items:
            $ref: "#/components/schemas/ProjectEnvironment"
        head_deployment:
          oneOf:
            - $ref: "#/components/schemas/Deployment"
            - type: "null"
        dev_url:
          oneOf:
            - type: string
              format: uri
              pattern: ^https://dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st$
            - type: "null"
        prod_url:
          oneOf:
            - type: string
              format: uri
              pattern: ^https://[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st$
            - type: "null"
        latest_operation:
          oneOf:
            - $ref: "#/components/schemas/Operation"
            - type: "null"
        cleanup_state:
          type: string
          enum:
            - not_started
            - pending
            - completed
            - reconciliation_required
    ProblemDetails:
      type: object
      description: RFC 9457 Problem Details extended with stable ohmyhost recovery fields.
      additionalProperties: false
      required:
        - type
        - title
        - status
        - code
        - request_id
        - retryable
        - suggested_action
      properties:
        type:
          type: string
          format: uri-reference
        title:
          type: string
          minLength: 1
        status:
          type: integer
          minimum: 400
          maximum: 599
        detail:
          type: string
        instance:
          type: string
          format: uri-reference
        code:
          type: string
          enum:
            - invalid_request
            - unauthenticated
            - forbidden
            - resource_not_found
            - idempotency_key_reused
            - project_handle_unavailable
            - project_identity_unavailable
            - deployment_plan_expired
            - deployment_plan_incompatible
            - confirmation_expired
            - confirmation_invalid
            - etag_mismatch
            - promotion_source_stale
            - promotion_target_stale
            - promotion_invalid_target
            - mail_domain_conflict
            - mail_domain_required
            - framework_conversion_required
            - migration_filename_noncanonical
            - environment_secret_mutation_blocked
            - container_runtime_required
            - payload_too_large
            - rate_limited
            - insufficient_organization_credits
            - paid_plan_required
            - project_budget_exceeded
            - compute_performance_paid_required
            - compute_performance_unavailable
            - compute_change_pending
            - compute_change_conflict
            - billing_purchase_conflict
            - billing_recharge_conflict
            - cloudflare_authorization_closed
            - project_notes_conflict
            - project_export_not_ready
            - interactive_login_required
            - api_key_creation_uncertain
            - api_key_permissions_unavailable
            - reconciliation_exhausted
            - service_unavailable
        request_id:
          type: string
          minLength: 1
          maxLength: 128
        retryable:
          type: boolean
        retry_after_seconds:
          type: integer
          minimum: 1
          maximum: 86400
          description: Optional machine-readable retry delay for a rate limit, matching Retry-After.
        suggested_action:
          type: string
          minLength: 1
    Operation:
      type: object
      description: Durable record returned for an accepted asynchronous mutation. Optional reconciliation is a current observation for queued/running work, separate from immutable terminal state. A completed reconciliation attempt alone does not mean the operation succeeded.
      additionalProperties: false
      required:
        - id
        - state
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Ulid"
        state:
          type: string
          enum:
            - queued
            - running
            - succeeded
            - failed
            - cancelled
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        result:
          type: object
          additionalProperties: true
        error:
          $ref: "#/components/schemas/OperationFailure"
        progress:
          $ref: "#/components/schemas/OperationDeploymentProgress"
        reconciliation:
          $ref: "#/components/schemas/OperationReconciliation"
    OperationDeploymentProgress:
      type: object
      additionalProperties: false
      description: Current deployment dependency observation, separate from immutable operation history. Poll the same operation; mail readiness does not mean application activation.
      required:
        [phase, project_id, deployment_id, observed_at, next_poll_after_seconds, suggested_action]
      properties:
        build_completed_at:
          type: string
          format: date-time
          description: Actual successful CodeBuild completion time when this operation ran a build; omitted for reused artifacts without a current build.
        phase:
          type: string
          enum: [queued, building, publishing, waiting_for_mail, mail_status_unavailable]
        project_id:
          $ref: "#/components/schemas/Ulid"
        deployment_id:
          $ref: "#/components/schemas/Ulid"
        observed_at:
          type: string
          format: date-time
        next_poll_after_seconds:
          type: integer
          const: 60
        suggested_action:
          type: string
          minLength: 1
          maxLength: 500
    OperationReconciliation:
      type: object
      additionalProperties: false
      required: [state, attempt_id, observed_at, suggested_action]
      properties:
        state:
          type: string
          enum: [required, pending]
        attempt_id:
          oneOf:
            - $ref: "#/components/schemas/Ulid"
            - type: "null"
          description: Active attempt for pending reconciliation; null when a new confirmed reconciliation is required.
        observed_at:
          type: string
          format: date-time
        suggested_action:
          type: string
          minLength: 1
          maxLength: 500
    ProviderReconciliationAttempt:
      type: object
      description: Provider-neutral status of one idempotent reconciliation attempt.
      additionalProperties: false
      required:
        - reconciliation_id
        - operation_id
        - state
      properties:
        reconciliation_id:
          $ref: "#/components/schemas/Ulid"
        operation_id:
          $ref: "#/components/schemas/Ulid"
        state:
          type: string
          enum:
            - completed
            - pending
            - uncertain
    IdempotencyConflictProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemDetails"
        - type: object
          properties:
            code:
              const: idempotency_key_reused
    OperationEvent:
      type: object
      additionalProperties: false
      required:
        - event_id
        - operation_id
        - project_id
        - type
        - occurred_at
      properties:
        event_id:
          $ref: "#/components/schemas/Ulid"
        operation_id:
          $ref: "#/components/schemas/Ulid"
        project_id:
          $ref: "#/components/schemas/Ulid"
        type:
          type: string
          enum:
            - OperationQueued
            - OperationStarted
            - OperationSucceeded
            - OperationFailed
            - OperationCancelled
        occurred_at:
          type: string
          format: date-time
        failure:
          $ref: "#/components/schemas/OperationFailure"
    ProjectNotesReceipt:
      type: object
      additionalProperties: false
      required: [version, updated_at]
      properties:
        version: { type: integer, minimum: 1, maximum: 2147483647 }
        updated_at: { type: string, format: date-time }
    DatabaseCompute:
      type: object
      additionalProperties: false
      required: [project_id, environment, data_mode, observed_at, database]
      properties:
        project_id: { $ref: "#/components/schemas/Ulid" }
        environment: { type: string, enum: [dev, prod] }
        data_mode: { type: string, enum: [shared, isolated] }
        observed_at: { type: string, format: date-time }
        database:
          oneOf:
            - type: "null"
            - type: object
              additionalProperties: false
              required:
                [
                  min_cu,
                  max_cu,
                  min_memory_gb,
                  max_memory_gb,
                  suspend_timeout_seconds,
                  state,
                  pending_state,
                  disabled,
                  region,
                ]
              properties:
                min_cu: { type: number, minimum: 0.25 }
                max_cu: { type: number, minimum: 0.25 }
                min_memory_gb: { type: number, minimum: 1 }
                max_memory_gb: { type: number, minimum: 1 }
                suspend_timeout_seconds:
                  {
                    type: integer,
                    minimum: -1,
                    maximum: 604800,
                    description: "Configured timeout: 0 is provider default; -1 disables suspension.",
                  }
                state: { type: string, enum: [init, active, idle] }
                pending_state: { type: [string, "null"], enum: [init, active, idle, null] }
                disabled: { type: boolean }
                region: { type: string, pattern: "^aws-[a-z0-9]+(?:-[a-z0-9]+)*-[0-9]+$" }
    ProjectContext:
      type: object
      additionalProperties: false
      required: [project_id, generated_at, credit_access, notes, markdown]
      properties:
        project_id: { $ref: "#/components/schemas/Ulid" }
        generated_at: { type: string, format: date-time }
        credit_access: { type: string, enum: [included, not_authorized] }
        markdown:
          { type: string, maxLength: 32768, description: At most 500 lines and 32768 UTF-8 bytes. }
        notes:
          type: object
          additionalProperties: false
          required: [version, markdown, updated_at]
          properties:
            version: { type: integer, minimum: 0, maximum: 2147483647 }
            markdown:
              {
                type: string,
                maxLength: 16384,
                description: At most 250 lines and 16384 UTF-8 bytes; untrusted user-authored text.,
              }
            updated_at: { type: [string, "null"], format: date-time }
    ProjectExport:
      type: object
      additionalProperties: false
      required:
        [
          id,
          project_id,
          state,
          requested_at,
          next_request_at,
          next_poll_after_seconds,
          error,
          archive,
        ]
      properties:
        id: { $ref: "#/components/schemas/Ulid" }
        project_id: { $ref: "#/components/schemas/Ulid" }
        state: { type: string, enum: [queued, running, succeeded, failed, cancelled] }
        requested_at: { type: string, format: date-time }
        next_request_at: { type: string, format: date-time }
        next_poll_after_seconds: { type: [integer, "null"], enum: [5, null] }
        error:
          oneOf:
            - $ref: "#/components/schemas/OperationFailure"
            - { type: "null" }
        archive:
          oneOf:
            - $ref: "#/components/schemas/ProjectExportArchive"
            - { type: "null" }
    ProjectExportArchive:
      type: object
      additionalProperties: false
      required:
        [bytes, sha256, sql_files, captured_at, expires_at, download_url, download_expires_at]
      properties:
        bytes: { type: integer, minimum: 1, maximum: 536870912 }
        sha256: { type: string, pattern: "^sha256:[a-f0-9]{64}$" }
        sql_files:
          type: array
          items: { type: string, enum: [dev.sql, prod.sql, shared.sql] }
          minItems: 1
          maxItems: 2
          uniqueItems: true
          description: dev.sql and/or prod.sql for isolated data, or shared.sql alone.
        captured_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        download_url: { type: [string, "null"], format: uri }
        download_expires_at: { type: [string, "null"], format: date-time }
    OperationFailure:
      type: object
      additionalProperties: false
      required:
        - code
        - message
        - retryable
        - suggested_action
      properties:
        code:
          type: string
          enum:
            - operation_failed
            - recovery_dispatch_failed
            - recovery_job_failed
            - recovery_input_expired
            - recovery_database_unavailable
            - recovery_checksum_mismatch
            - recovery_scope_unavailable
            - recovery_archive_too_large
            - build_not_started
            - build_failed
            - database_compute_failed
            - database_compute_plan_changed
            - database_compute_rejected
            - database_migration_failed
            - insufficient_organization_credits
            - paid_plan_required
            - runtime_candidate_failed
            - container_runtime_secrets_unsupported
            - provider_state_absent
        message:
          type: string
          minLength: 1
          maxLength: 512
        retryable:
          type: boolean
          const: false
          description: A terminal operation cannot be retried in place; follow suggested_action.
        suggested_action:
          type: string
          minLength: 1
          maxLength: 500
    AuditEvent:
      type: object
      additionalProperties: false
      required:
        - audit_event_id
        - project_id
        - event_type
        - actor_type
        - actor_id
        - payload
        - occurred_at
      properties:
        audit_event_id:
          $ref: "#/components/schemas/Ulid"
        project_id:
          $ref: "#/components/schemas/Ulid"
        operation_id:
          $ref: "#/components/schemas/Ulid"
        event_type:
          type: string
          minLength: 1
        actor_type:
          type: string
          minLength: 1
        actor_id:
          type: string
          minLength: 1
        payload:
          $ref: "#/components/schemas/AuditPayloadSummary"
        occurred_at:
          type: string
          format: date-time
    AuditPayloadSummary:
      type: object
      description: A redacted, public summary of an internal audit payload.
      additionalProperties: false
      required:
        - summary
      properties:
        summary:
          type: string
          minLength: 1
          maxLength: 512
    AuditEventPage:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/AuditEvent"
        next_cursor:
          type:
            - string
            - "null"
          pattern: ^[A-Za-z0-9_-]+$
    Ulid:
      type: string
      pattern: ^[0-9A-HJKMNP-TV-Z]{26}$
