openapi: 3.0.3
info:
  title: Pipe Store Public API
  version: "1.0.0"
  description: |
    Public user authentication, profile, credits, and S3 gateway endpoints.
    This spec avoids internal implementation details and documents only client-facing behavior.

    Quickstart (authenticated):
    - Set your API base and token:
      ```bash
      export API_BASE=https://us-west-01-firestarter.pipenetwork.com
      export PIPE_TOKEN=YOUR_ACCESS_TOKEN
      ```
    - Upload a file (async):
      ```bash
      curl -X POST "$API_BASE/upload?file_name=hello.txt" \
        -H "Authorization: Bearer $PIPE_TOKEN" \
        -H "Content-Type: application/octet-stream" \
        --data-binary @hello.txt
      ```
    - Download raw bytes (stream):
      ```bash
      curl -H "Authorization: Bearer $PIPE_TOKEN" \
        "$API_BASE/download-stream?file_name=hello.txt" \
        -o hello.txt
      ```
    - Create a public link:
      ```bash
      curl -H "Authorization: Bearer $PIPE_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{"file_name":"hello.txt"}' \
        "$API_BASE/createPublicLink"
      ```

    Notes:
    - `file_name` is relative to your root; URL-encode `/` when using query params.
    - Use `/download` for base64, `/download-stream` for raw bytes.
    - `/openapi.yaml` is public (no auth required).
servers:
  - url: https://{host}
    variables:
      host:
        default: us-west-01-firestarter.pipenetwork.com
        description: API host

tags:
  - name: meta
    description: Spec and metadata endpoints
  - name: auth
    description: Password authentication and token lifecycle
  - name: siws
    description: Sign-in with Solana
  - name: user
    description: User profile and settings
  - name: credits
    description: Prepaid credits and payments
  - name: usage
    description: Usage and billing summaries
  - name: s3
    description: S3 gateway and bucket settings
  - name: integrations
    description: Site and CMS integrations
  - name: files
    description: File upload, download, and listing
  - name: udp
    description: UDP+RaptorQ transport control-plane (experimental)
  - name: public
    description: Public links and public file access

x-tagGroups:
  - name: Authentication
    tags: [auth, siws, user]
  - name: Billing
    tags: [credits, usage]
  - name: Storage
    tags: [files, s3, integrations, udp]
  - name: Public
    tags: [public]
  - name: Meta
    tags: [meta]

paths:
  /openapi.yaml:
    get:
      summary: OpenAPI specification
      tags: [meta]
      responses:
        "200":
          description: OpenAPI YAML
          content:
            application/yaml:
              schema:
                type: string
  /auth/login:
    post:
      summary: Password login
      tags: [auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "200":
          description: Tokens issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthTokens"
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "426":
          description: Password migration required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Too many attempts
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Auth service unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/refresh:
    post:
      summary: Refresh access token
      tags: [auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefreshTokenRequest"
      responses:
        "200":
          description: Access token issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccessTokenResponse"
        "401":
          description: Invalid or expired refresh token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Auth service unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/logout:
    post:
      summary: Logout and revoke current session
      tags: [auth]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Logged out
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/set-password:
    post:
      summary: Set password for migrated accounts
      tags: [auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetPasswordRequest"
      responses:
        "200":
          description: Password set, tokens issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SetPasswordResponse"
        "400":
          description: Invalid password
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: User not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/sessions:
    get:
      summary: List active sessions
      tags: [auth]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Session list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionListResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/sessions/{session_id}:
    delete:
      summary: Revoke a session
      tags: [auth]
      security:
        - bearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Session revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResponse"
        "400":
          description: Cannot revoke current session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Not authorized to revoke this session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/siws/challenge:
    post:
      summary: Sign In With Solana challenge
      tags: [siws]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SiwsChallengeRequest"
      responses:
        "200":
          description: Challenge created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SiwsChallengeResponse"
        "400":
          description: Invalid wallet public key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Too many requests
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /auth/siws/verify:
    post:
      summary: Verify SIWS signature and issue tokens
      tags: [siws]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SiwsVerifyRequest"
      responses:
        "200":
          description: Tokens issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SiwsVerifyResponse"
        "400":
          description: Invalid signature or payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid or expired nonce, signature verification failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Too many requests
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /user/me:
    get:
      summary: Get current user profile
      tags: [user]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: User profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserMeResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /user/email:
    post:
      summary: Update user email
      tags: [user]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateUserEmailRequest"
      responses:
        "200":
          description: Email updated (empty string clears email)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UpdateUserEmailResponse"
        "400":
          description: Invalid email
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /user/username:
    post:
      summary: Update username
      tags: [user]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateUserUsernameRequest"
      responses:
        "200":
          description: Username updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UpdateUserUsernameResponse"
        "400":
          description: Invalid username
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Username already taken
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/credits/status:
    get:
      summary: Get credits status
      tags: [credits]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      responses:
        "200":
          description: Credits status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditsStatusResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Prepaid credits not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/credits/intent:
    post:
      summary: Create or reuse a credits intent
      tags: [credits]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCreditsIntentRequest"
      responses:
        "200":
          description: Credits intent created or reused
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditsIntentResponse"
        "409":
          description: Another active credits intent is blocking this request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditsIntentConflictResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Prepaid credits not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/credits/intent/{intent_id}:
    get:
      summary: Get a credits intent snapshot
      tags: [credits]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      parameters:
        - name: intent_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Credits intent detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditsIntentStatus"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Intent not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Prepaid credits not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/credits/x402:
    post:
      summary: Challenge or confirm an x402 credits payment
      tags: [credits]
      description: >
        Agent-friendly credits top-up flow. Call with `amount_usdc_raw` JSON and no
        `Payment-Signature` header to receive a `402 Payment Required` challenge. After sending
        the Solana USDC transfer, call the same endpoint again with the `Payment-Signature`
        header. The confirm call does not require a request body. Prefer `Authorization: ApiKey`
        for headless clients; the browser portal should use the explicit intent/submit flow.
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      parameters:
        - name: Payment-Signature
          in: header
          required: false
          schema:
            type: string
          description: Base64-encoded JSON `{"intent_id":"...","tx_sig":"..."}` used to confirm a challenged payment.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCreditsIntentRequest"
      responses:
        "200":
          description: Credits payment processed
          headers:
            Payment-Response:
              schema:
                type: string
              description: Base64-encoded JSON matching `X402PaymentResponse`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitCreditsPaymentResponse"
        "202":
          description: Payment is still processing
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before polling the credits intent detail endpoint.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitCreditsPaymentResponse"
        "400":
          description: Invalid request body, malformed x402 payment signature header, or recoverable invalid payment
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/SubmitCreditsPaymentResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: Payment required
          headers:
            Payment-Required:
              schema:
                type: string
              description: Base64-encoded JSON matching `X402PaymentRequired`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditsX402ChallengeResponse"
        "409":
          description: Intent conflict or intent not in a payable state
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/CreditsIntentConflictResponse"
                  - $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Prepaid credits not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/credits/submit:
    post:
      summary: Submit credits payment signature
      tags: [credits]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubmitCreditsPaymentRequest"
      responses:
        "200":
          description: Credits payment processed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitCreditsPaymentResponse"
        "202":
          description: Payment is still processing
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before polling the credits intent detail endpoint.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitCreditsPaymentResponse"
        "400":
          description: Invalid request or recoverable invalid payment
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/SubmitCreditsPaymentResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Intent not in a payable state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Prepaid credits not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/credits/cancel:
    post:
      summary: Cancel credits intent
      tags: [credits]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CancelCreditsIntentRequest"
      responses:
        "200":
          description: Intent cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditsCancelResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Intent is currently processing and cannot be cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditsIntentConflictResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Prepaid credits not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/subscription/lifetime/status:
    get:
      summary: Get lifetime subscription status
      tags: [credits]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      responses:
        "200":
          description: Lifetime status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifetimeStatusResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Lifetime subscriptions not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/subscription/lifetime/intent:
    post:
      summary: Create or reuse a lifetime payment intent
      tags: [credits]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      responses:
        "200":
          description: Lifetime intent created or reused
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifetimeIntentResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Lifetime is already active for this account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Lifetime subscriptions not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/subscription/lifetime/submit:
    post:
      summary: Submit lifetime payment signature
      tags: [credits]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubmitLifetimePaymentRequest"
      responses:
        "200":
          description: Lifetime payment accepted or activated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitLifetimePaymentResponse"
        "202":
          description: Payment is still processing
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before polling the lifetime status endpoint.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitLifetimePaymentResponse"
        "400":
          description: Invalid request or recoverable invalid payment
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/SubmitLifetimePaymentResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Intent conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: Lifetime subscriptions not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/price:
    get:
      summary: Get the current PIPE reference price
      tags: [usage]
      responses:
        "200":
          description: Fixed PIPE to USDC reference used by pipe-store credits views
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PipePriceResponse"

  /api/token-usage:
    get:
      summary: Usage totals (storage + egress)
      tags: [usage]
      security:
        - bearerAuth: []
      parameters:
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [24h, 7d, 30d, 90d, 365d, all]
          description: Time window for egress totals. Storage uses daily snapshots over the same window when available.
        - name: detailed
          in: query
          required: false
          schema:
            type: boolean
          description: When true, includes tier breakdowns for egress.
      responses:
        "200":
          description: Usage totals
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/TokenUsageResponse"
                  - $ref: "#/components/schemas/EnhancedTokenUsageResponse"
              examples:
                summary:
                  summary: Usage summary
                  value:
                    user_id: "6f1c3d2e-1b2a-4f2f-9a3d-9d0f1c2b3a4b"
                    period: "30d"
                    breakdown:
                      storage:
                        gb_transferred: 128.532
                        tokens_spent: 0
                        tokens_burned: 0
                        tokens_to_treasury: 0
                        usdc_charged: 3.2133
                      bandwidth:
                        gb_transferred: 12.75
                        tokens_spent: 0.012
                        tokens_burned: 0.006
                        tokens_to_treasury: 0.006
                        usdc_charged: 0.01275
                      total:
                        gb_transferred: 141.282
                        tokens_spent: 0.012
                        tokens_burned: 0.006
                        tokens_to_treasury: 0.006
                        usdc_charged: 3.22605
                detailed:
                  summary: Usage summary with egress tiers
                  value:
                    user_id: "6f1c3d2e-1b2a-4f2f-9a3d-9d0f1c2b3a4b"
                    period: "7d"
                    breakdown:
                      storage:
                        gb_transferred: 42.1
                        tokens_spent: 0
                        tokens_burned: 0
                        tokens_to_treasury: 0
                        transfer_count: 0
                        tier_details: {}
                        usdc_charged: 1.0525
                      bandwidth:
                        gb_transferred: 2.0
                        tokens_spent: 0.002
                        tokens_burned: 0.001
                        tokens_to_treasury: 0.001
                        transfer_count: 3
                        tier_details:
                          Normal:
                            tier_name: Normal
                            transfer_count: 3
                            gb_transferred: 2.0
                            base_cost: 0.002
                            final_cost: 0.002
                            avg_multiplier: 1.0
                            tokens_burned: 0.001
                            tokens_to_treasury: 0.001
                        usdc_charged: 0.002
                      total:
                        gb_transferred: 44.1
                        tokens_spent: 0.002
                        tokens_burned: 0.001
                        tokens_to_treasury: 0.001
                        usdc_charged: 1.0545
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Usage requires PostgreSQL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/receipts:
    get:
      summary: List billing receipts
      tags: [usage]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 1
            maximum: 500
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 0
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum: [all, storage, bandwidth]
          description: Filters uploads as `storage` and downloads as `bandwidth`.
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          required: false
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Receipt summary and items
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReceiptsResponse"
        "400":
          description: Invalid filter values
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Database not available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/s3/keys:
    get:
      summary: List S3 access keys
      tags: [s3]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Keys list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/S3KeyListResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      summary: Create an S3 access key
      tags: [s3]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateS3KeyRequest"
      responses:
        "200":
          description: S3 key created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateS3KeyResponse"
        "400":
          description: Invalid key name
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Key name already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/s3/keys/{access_key_id}:
    delete:
      summary: Revoke an S3 access key
      tags: [s3]
      security:
        - bearerAuth: []
      parameters:
        - name: access_key_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Key revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/S3KeyRevokeResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/s3/info:
    get:
      summary: Get S3 gateway endpoint info
      tags: [s3]
      responses:
        "200":
          description: Endpoint info
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/S3InfoResponse"

  /api/s3/bucket:
    get:
      summary: Get bucket settings
      tags: [s3]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Bucket settings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BucketSettingsResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      summary: Update bucket settings
      tags: [s3]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchBucketSettingsRequest"
      responses:
        "200":
          description: Bucket settings updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BucketSettingsResponse"
        "400":
          description: Invalid settings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/integrations/wordpress/site:
    get:
      summary: Get WordPress site integration status
      tags: [integrations]
      security:
        - bearerAuth: []
      parameters:
        - name: install_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: WordPress site integration status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WordPressSiteIntegrationStatusResponse"
        "400":
          description: Invalid install identifier
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Feature disabled or integration not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    put:
      summary: Connect or reconnect a WordPress site
      tags: [integrations]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WordPressSiteIntegrationRequest"
      responses:
        "200":
          description: WordPress site integration connected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WordPressSiteIntegrationConnectResponse"
        "400":
          description: Invalid integration request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Feature disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Existing integration bucket is not compatible
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      summary: Disconnect a WordPress site and revoke write credentials
      tags: [integrations]
      security:
        - bearerAuth: []
      parameters:
        - name: install_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: WordPress site integration moved to read-only mode
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WordPressSiteIntegrationDisconnectResponse"
        "400":
          description: Invalid install identifier
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Feature disabled or integration not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/integrations/wordpress/account:
    get:
      summary: Get WordPress account status
      tags: [integrations]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      responses:
        "200":
          description: WordPress account status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WordPressAccountStatusResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/integrations/wordpress/account/activate:
    post:
      summary: Activate dedicated WordPress account mode
      tags: [integrations]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      responses:
        "200":
          description: WordPress account mode activated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WordPressAccountStatusResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Feature disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Account is not eligible for WordPress mode
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/integrations/wordpress/sites:
    get:
      summary: List connected WordPress sites
      tags: [integrations]
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      responses:
        "200":
          description: Connected WordPress sites
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WordPressSiteListResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /s3:
    get:
      summary: S3 ListBuckets
      tags: [s3]
      description: |
        S3-compatible gateway root. Use standard S3 clients with path-style addressing.
        Requests must be signed with AWS SigV4 using the S3 access key and secret.
        The bucket name is returned by `/api/s3/keys` or `/api/s3/bucket`.
      responses:
        "200":
          description: S3 XML response
          content:
            application/xml:
              schema:
                $ref: "#/components/schemas/S3XmlResponse"

  /s3/{bucket}:
    get:
      summary: S3 ListObjectsV2
      tags: [s3]
      description: S3-compatible list objects for a bucket.
      parameters:
        - name: bucket
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: S3 XML response
          content:
            application/xml:
              schema:
                $ref: "#/components/schemas/S3XmlResponse"
    head:
      summary: S3 HeadBucket
      tags: [s3]
      parameters:
        - name: bucket
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Bucket exists
    put:
      summary: S3 CreateBucket (idempotent)
      tags: [s3]
      parameters:
        - name: bucket
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Bucket created or already exists
          content:
            application/xml:
              schema:
                $ref: "#/components/schemas/S3XmlResponse"

  /s3/{bucket}/{key}:
    get:
      summary: S3 GetObject
      tags: [s3]
      parameters:
        - name: bucket
          in: path
          required: true
          schema:
            type: string
        - name: key
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Object bytes
          content:
            application/octet-stream:
              schema:
                $ref: "#/components/schemas/BinaryResponse"
    head:
      summary: S3 HeadObject
      tags: [s3]
      parameters:
        - name: bucket
          in: path
          required: true
          schema:
            type: string
        - name: key
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Object metadata
    put:
      summary: S3 PutObject
      tags: [s3]
      parameters:
        - name: bucket
          in: path
          required: true
          schema:
            type: string
        - name: key
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              $ref: "#/components/schemas/BinaryRequest"
      responses:
        "200":
          description: Object stored
    delete:
      summary: S3 DeleteObject
      tags: [s3]
      parameters:
        - name: bucket
          in: path
          required: true
          schema:
            type: string
        - name: key
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Object deleted

  /upload:
    post:
      summary: Upload a file (async)
      tags: [files]
      description: |
        Upload raw file bytes. The upload is accepted immediately and processed asynchronously.
        Use `file_name` to specify the destination path relative to your root.
      security:
        - bearerAuth: []
      parameters:
        - name: file_name
          in: query
          required: true
          schema:
            type: string
          description: File name or path relative to your root (URL-encode `/`).
        - name: tier
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/UploadTier"
          description: "Upload tier (default: normal)."
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              $ref: "#/components/schemas/BinaryRequest"
      responses:
        "202":
          description: Upload accepted. File transfer continues in the background.
          headers:
            Location:
              schema:
                type: string
              description: Status URL for polling operation state.
            X-Operation-Id:
              schema:
                type: string
              description: Durable async operation ID.
            X-Bytes-Uploaded:
              schema:
                type: string
            X-Upload-Tier:
              schema:
                type: string
            X-Cost-Per-GB:
              schema:
                type: string
            X-Credits-Charged:
              schema:
                type: string
            X-Credits-Raw:
              schema:
                type: string
          content:
            text/plain:
              schema:
                type: string
        "400":
          description: Invalid filename or request parameters
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "402":
          description: Insufficient prepaid credits for storage
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StorageBudgetError"
        "429":
          description: Rate limit exceeded
          content:
            text/plain:
              schema:
                type: string
        "503":
          description: Storage temporarily unavailable
          content:
            text/plain:
              schema:
                type: string

  /priorityUpload:
    post:
      summary: Priority upload (async)
      tags: [files]
      description: |
        Priority upload for latency-sensitive workloads. The upload is accepted immediately
        and processed asynchronously.
      security:
        - bearerAuth: []
      parameters:
        - name: file_name
          in: query
          required: true
          schema:
            type: string
          description: File name or path relative to your root (URL-encode `/`).
        - name: tier
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/UploadTier"
          description: "Upload tier (default: priority)."
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              $ref: "#/components/schemas/BinaryRequest"
      responses:
        "202":
          description: Upload accepted. File transfer continues in the background.
          headers:
            Location:
              schema:
                type: string
              description: Status URL for polling operation state.
            X-Operation-Id:
              schema:
                type: string
              description: Durable async operation ID.
            X-Bytes-Uploaded:
              schema:
                type: string
            X-Upload-Tier:
              schema:
                type: string
            X-Cost-Per-GB:
              schema:
                type: string
            X-Credits-Charged:
              schema:
                type: string
            X-Credits-Raw:
              schema:
                type: string
          content:
            text/plain:
              schema:
                type: string
        "400":
          description: Invalid filename or request parameters
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "402":
          description: Insufficient prepaid credits for storage
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StorageBudgetError"
        "429":
          description: Rate limit exceeded
          content:
            text/plain:
              schema:
                type: string
        "503":
          description: Storage temporarily unavailable
          content:
            text/plain:
              schema:
                type: string


  /checkUploadStatus:
    get:
      summary: Check upload status
      tags: [files]
      security:
        - bearerAuth: []
      parameters:
        - name: operation_id
          in: query
          required: false
          schema:
            type: string
          description: Durable async operation ID returned by `/upload` or `/priorityUpload`.
        - name: file_name
          in: query
          required: false
          schema:
            type: string
          description: Backward-compatible lookup by file name (returns latest operation for that file).
      responses:
        "200":
          description: Upload status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadStatus"
        "400":
          description: Missing or invalid query parameters
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Upload not found
          content:
            text/plain:
              schema:
                type: string

  /api/udp/capabilities:
    get:
      summary: Get UDP transport capabilities
      tags: [udp]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Capability snapshot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UdpCapabilitiesResponse"
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "501":
          description: UDP transport disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/udp/upload/init:
    post:
      summary: Initialize UDP upload session
      tags: [udp]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UdpUploadInitRequest"
      responses:
        "200":
          description: Session initialized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UdpUploadInitResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "402":
          description: Insufficient prepaid credits for storage
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StorageBudgetError"
        "501":
          description: UDP transport disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/udp/upload/commit:
    post:
      summary: Commit UDP upload session
      tags: [udp]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UdpUploadCommitRequest"
      responses:
        "200":
          description: Session committed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UdpUploadCommitResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Session conflict (missing object, aborted, or size mismatch)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "410":
          description: Session expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: UDP transport disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/udp/upload/abort:
    post:
      summary: Abort UDP upload session
      tags: [udp]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UdpUploadAbortRequest"
      responses:
        "200":
          description: Session aborted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UdpUploadAbortResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: UDP transport disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/udp/download/init:
    post:
      summary: Initialize UDP download session
      tags: [udp]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UdpDownloadInitRequest"
      responses:
        "200":
          description: Download session initialized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UdpDownloadInitResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: File not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: UDP transport disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/udp/download/send:
    post:
      summary: Send UDP download datagrams
      tags: [udp]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UdpDownloadSendRequest"
      responses:
        "200":
          description: UDP datagrams sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UdpDownloadSendResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "402":
          description: Insufficient credits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Target address policy violation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Session is not active/download-capable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "416":
          description: Requested offset is out of range
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: UDP transport or download egress disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/udp/sessions/{session_id}:
    get:
      summary: Get UDP session status
      tags: [udp]
      security:
        - bearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Session status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UdpSessionStatusResponse"
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: UDP transport disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/udp/sessions/{session_id}/touch:
    post:
      summary: Extend UDP session TTL
      tags: [udp]
      security:
        - bearerAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UdpSessionTouchRequest"
      responses:
        "200":
          description: Session expiration extended
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UdpSessionTouchResponse"
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Session already finished
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: UDP transport disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /download:
    get:
      summary: Download a file (base64)
      tags: [files]
      description: Returns base64-encoded file contents.
      security:
        - bearerAuth: []
      parameters:
        - name: file_name
          in: query
          required: true
          schema:
            type: string
          description: File name or path relative to your root (URL-encode `/`).
      responses:
        "200":
          description: Base64-encoded file contents
          headers:
            X-Bytes-Downloaded:
              schema:
                type: string
            X-Credits-Charged:
              schema:
                type: string
            X-Credits-Raw:
              schema:
                type: string
          content:
            text/plain:
              schema:
                type: string
                format: byte
        "400":
          description: Invalid filename or request parameters
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "402":
          description: Insufficient prepaid credits for bandwidth
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InsufficientCreditsError"
        "403":
          description: Access denied
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: File not found
          content:
            text/plain:
              schema:
                type: string
    post:
      summary: Download a file (base64)
      tags: [files]
      description: Returns base64-encoded file contents.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DownloadRequest"
      responses:
        "200":
          description: Base64-encoded file contents
          headers:
            X-Bytes-Downloaded:
              schema:
                type: string
            X-Credits-Charged:
              schema:
                type: string
            X-Credits-Raw:
              schema:
                type: string
          content:
            text/plain:
              schema:
                type: string
                format: byte
        "400":
          description: Invalid filename or request parameters
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "402":
          description: Insufficient prepaid credits for bandwidth
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InsufficientCreditsError"
        "403":
          description: Access denied
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: File not found
          content:
            text/plain:
              schema:
                type: string

  /download-stream:
    get:
      summary: Download a file (stream)
      tags: [files]
      description: Streams raw file bytes without base64 encoding.
      security:
        - bearerAuth: []
      parameters:
        - name: file_name
          in: query
          required: true
          schema:
            type: string
          description: File name or path relative to your root (URL-encode `/`).
      responses:
        "200":
          description: Raw file bytes
          headers:
            X-Bytes-Downloaded:
              schema:
                type: string
            X-Credits-Charged:
              schema:
                type: string
            X-Credits-Raw:
              schema:
                type: string
          content:
            application/octet-stream:
              schema:
                $ref: "#/components/schemas/BinaryResponse"
        "400":
          description: Invalid filename or request parameters
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "402":
          description: Insufficient prepaid credits for bandwidth
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InsufficientCreditsError"
        "403":
          description: Access denied
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: File not found
          content:
            text/plain:
              schema:
                type: string

  /priorityDownload:
    get:
      summary: Priority download (base64)
      tags: [files]
      description: Returns base64-encoded file contents with priority scheduling.
      security:
        - bearerAuth: []
      parameters:
        - name: file_name
          in: query
          required: true
          schema:
            type: string
          description: File name or path relative to your root (URL-encode `/`).
      responses:
        "200":
          description: Base64-encoded file contents
          headers:
            X-Bytes-Downloaded:
              schema:
                type: string
            X-Credits-Charged:
              schema:
                type: string
            X-Credits-Raw:
              schema:
                type: string
          content:
            text/plain:
              schema:
                type: string
                format: byte
        "400":
          description: Invalid filename or request parameters
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "402":
          description: Insufficient prepaid credits for bandwidth
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InsufficientCreditsError"
        "403":
          description: Access denied
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: File not found
          content:
            text/plain:
              schema:
                type: string

  /listFiles:
    get:
      summary: List uploaded files
      tags: [files]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 1
            maximum: 1000
          description: Maximum number of files to return (default 100).
        - name: continuation_token
          in: query
          required: false
          schema:
            type: string
          description: Token from a previous response to fetch the next page.
        - name: prefix
          in: query
          required: false
          schema:
            type: string
          description: Return only files with this path prefix.
      responses:
        "200":
          description: Files list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileListResponse"
        "401":
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Database not available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/files:
    get:
      summary: List uploaded files (portal view)
      tags: [files]
      description: Offset-based file listing optimized for the web portal. This is the recommended listing endpoint for browser clients.
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 1
            maximum: 200
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 0
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: Case-insensitive substring match against the stored path.
        - name: sort_by
          in: query
          required: false
          schema:
            type: string
            enum: [date, name, size]
        - name: sort_order
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
      responses:
        "200":
          description: Portal-friendly file list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PortalFilesResponse"
        "401":
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Database not available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /deleteFile:
    post:
      summary: Delete a file
      tags: [files]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeleteFileRequest"
      responses:
        "200":
          description: File deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteFileResponse"
        "400":
          description: Invalid filename or request parameters
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "403":
          description: Access denied
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: File not found
          content:
            text/plain:
              schema:
                type: string

  /createPublicLink:
    post:
      summary: Create a public download link
      tags: [public]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePublicLinkRequest"
      responses:
        "200":
          description: Public link created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatePublicLinkResponse"
        "400":
          description: Invalid filename or request parameters
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string

  /deletePublicLink:
    post:
      summary: Delete a public download link
      tags: [public]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeletePublicLinkRequest"
      responses:
        "200":
          description: Public link deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeletePublicLinkResponse"
        "401":
          description: Authentication required
          content:
            text/plain:
              schema:
                type: string
        "403":
          description: Access denied
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Public link not found
          content:
            text/plain:
              schema:
                type: string

  /publicDownload:
    get:
      summary: Public download via link hash
      tags: [public]
      parameters:
        - name: hash
          in: query
          required: true
          schema:
            type: string
          description: Public link hash.
        - name: preview
          in: query
          required: false
          schema:
            type: boolean
          description: When true, returns an HTML preview page.
        - name: thumbnail
          in: query
          required: false
          schema:
            type: boolean
          description: When true, returns a thumbnail image if available.
      responses:
        "200":
          description: File contents or preview page
          content:
            application/octet-stream:
              schema:
                $ref: "#/components/schemas/BinaryResponse"
            text/html:
              schema:
                type: string
            image/jpeg:
              schema:
                $ref: "#/components/schemas/BinaryResponse"
            image/png:
              schema:
                $ref: "#/components/schemas/BinaryResponse"
        "404":
          description: Link not found
          content:
            text/html:
              schema:
                type: string

  /public/{user_id}/{path}:
    get:
      summary: Public file access by user and path
      tags: [public]
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: string
          description: User ID or public slug.
        - name: path
          in: path
          required: true
          schema:
            type: string
          style: simple
          allowReserved: true
          description: File path relative to the user root (may include `/`).
      responses:
        "200":
          description: Raw file bytes
          content:
            application/octet-stream:
              schema:
                $ref: "#/components/schemas/BinaryResponse"
        "400":
          description: Invalid file path
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: File not found
          content:
            text/plain:
              schema:
                type: string

  /{account}/{hash}:
    get:
      summary: Deterministic object URL by account and content hash
      tags: [public]
      description: |
        Deterministic object endpoint for agent/tool handoff:
        `https://us-west-01-firestarter.pipenetwork.com/{account}/{hash}`.
        - `account`: user ID or public slug
        - `hash`: 64-character lowercase/uppercase hex content hash

        Public accounts can be fetched anonymously. Private accounts require authentication.
      security:
        - bearerAuth: []
        - {}
      parameters:
        - name: account
          in: path
          required: true
          schema:
            type: string
          description: User ID or public slug.
        - name: hash
          in: path
          required: true
          schema:
            type: string
            minLength: 64
            maxLength: 64
            pattern: "^[A-Fa-f0-9]{64}$"
          description: Content hash (hex).
      responses:
        "200":
          description: Raw file bytes
          content:
            application/octet-stream:
              schema:
                $ref: "#/components/schemas/BinaryResponse"
        "400":
          description: Invalid hash format
          content:
            text/plain:
              schema:
                type: string
        "401":
          description: Authentication required for private object
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Object not found
          content:
            text/plain:
              schema:
                type: string

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    apiKeyAuth:
      type: apiKey
      in: header
      name: Authorization
      description: "Use `Authorization: ApiKey <user_app_key>` for agent or server-to-server requests."
  schemas:
    LoginRequest:
      type: object
      required: [username, password]
      properties:
        username:
          type: string
        password:
          type: string
    RefreshTokenRequest:
      type: object
      required: [refresh_token]
      properties:
        refresh_token:
          type: string
    SetPasswordRequest:
      type: object
      required: [user_id, user_app_key, new_password]
      properties:
        user_id:
          type: string
        user_app_key:
          type: string
        new_password:
          type: string
    AuthTokens:
      type: object
      required: [access_token, refresh_token, token_type, expires_in]
      properties:
        access_token:
          type: string
        refresh_token:
          type: string
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          format: int64
        csrf_token:
          type: string
          nullable: true
    AccessTokenResponse:
      type: object
      required: [access_token, token_type, expires_in]
      properties:
        access_token:
          type: string
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          format: int64
    SetPasswordResponse:
      type: object
      required: [message, access_token, refresh_token, token_type, expires_in]
      properties:
        message:
          type: string
        access_token:
          type: string
        refresh_token:
          type: string
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          format: int64
    SiwsChallengeRequest:
      type: object
      required: [wallet_public_key]
      properties:
        wallet_public_key:
          type: string
          description: Base58 Solana public key
    SiwsChallengeResponse:
      type: object
      required: [nonce, message]
      properties:
        nonce:
          type: string
        message:
          type: string
    SiwsVerifyRequest:
      type: object
      required: [wallet_public_key, nonce, message, signature_b64]
      properties:
        wallet_public_key:
          type: string
        nonce:
          type: string
        message:
          type: string
        signature_b64:
          type: string
          description: Base64-encoded signature of message
    SiwsVerifyResponse:
      type: object
      required: [access_token, refresh_token]
      properties:
        access_token:
          type: string
        refresh_token:
          type: string
        csrf_token:
          type: string
          nullable: true
    UserMeResponse:
      type: object
      required: [user_id, username, user_app_key, email, wallet_public_key, fees_exempt]
      properties:
        user_id:
          type: string
        username:
          type: string
        user_app_key:
          type: string
        email:
          type: string
          nullable: true
        wallet_public_key:
          type: string
        fees_exempt:
          type: boolean
    UpdateUserEmailRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          description: Empty string clears email
    UpdateUserEmailResponse:
      type: object
      required: [status, email]
      properties:
        status:
          type: string
          example: ok
        email:
          type: string
          nullable: true
    UpdateUserUsernameRequest:
      type: object
      required: [username]
      properties:
        username:
          type: string
    UpdateUserUsernameResponse:
      type: object
      required: [status, username]
      properties:
        status:
          type: string
          example: ok
        username:
          type: string
    SessionListResponse:
      type: object
      required: [sessions, count]
      properties:
        sessions:
          type: array
          items:
            $ref: "#/components/schemas/SessionInfo"
        count:
          type: integer
          format: int32
    SessionInfo:
      type: object
      required: [session_id, created_at, expires_at, ip_address, is_current]
      properties:
        session_id:
          type: string
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        ip_address:
          type: string
        user_agent:
          type: string
          nullable: true
        is_current:
          type: boolean
    CreditsStatusResponse:
      type: object
      required:
        - balance_usdc_raw
        - balance_usdc
        - total_deposited_usdc_raw
        - total_spent_usdc_raw
        - usdc_mint
        - quota
        - pending_fees_pipe_raw
        - pending_fees_pipe
        - pending_fees_usdc_raw
        - product_mode
        - eligible_for_activation
        - bundled_public_delivery
        - portal_url
        - storage_usdc_raw_per_gb_month
        - storage_usdc_per_gb_month
        - bandwidth_usdc_raw_per_gb
        - bandwidth_usdc_per_gb
        - wordpress_site_count
      properties:
        balance_usdc_raw:
          type: integer
          format: int64
        balance_usdc:
          type: number
          format: double
        total_deposited_usdc_raw:
          type: integer
          format: int64
        total_spent_usdc_raw:
          type: integer
          format: int64
        usdc_mint:
          type: string
        last_topup_at:
          type: string
          nullable: true
        quota:
          $ref: "#/components/schemas/CreditsQuota"
        intent:
          $ref: "#/components/schemas/CreditsIntentStatus"
          nullable: true
        pending_fees_pipe_raw:
          type: integer
          format: int64
        pending_fees_pipe:
          type: number
          format: double
        pending_fees_usdc_raw:
          type: integer
          format: int64
        product_mode:
          $ref: "#/components/schemas/BillingProductMode"
        eligible_for_activation:
          type: boolean
        eligibility_error:
          type: string
          nullable: true
        bundled_public_delivery:
          type: boolean
        portal_url:
          type: string
          format: uri
        storage_usdc_raw_per_gb_month:
          type: integer
          format: int64
        storage_usdc_per_gb_month:
          type: number
          format: double
        bandwidth_usdc_raw_per_gb:
          type: integer
          format: int64
        bandwidth_usdc_per_gb:
          type: number
          format: double
        wordpress_site_count:
          type: integer
          format: int64
    CreditsQuota:
      type: object
      required: [tier_estimates]
      properties:
        tier_estimates:
          type: array
          items:
            $ref: "#/components/schemas/CreditsTierEstimate"
    CreditsTierEstimate:
      type: object
      properties:
        tier_name:
          type: string
        cost_per_gb_usdc:
          type: number
          format: double
        available_gb:
          type: number
          format: double
    CreditsIntentStatus:
      type: object
      required: [intent_id, status, requested_usdc_raw, detected_usdc_raw, credited_usdc_raw, usdc_mint, treasury_owner_pubkey, treasury_usdc_ata, reference_pubkey]
      properties:
        intent_id:
          type: string
        status:
          type: string
        requested_usdc_raw:
          type: integer
          format: int64
        detected_usdc_raw:
          type: integer
          format: int64
        credited_usdc_raw:
          type: integer
          format: int64
        usdc_mint:
          type: string
        treasury_owner_pubkey:
          type: string
        treasury_usdc_ata:
          type: string
        reference_pubkey:
          type: string
        payment_tx_sig:
          type: string
          nullable: true
        last_checked_at:
          type: string
          format: date-time
          nullable: true
        credited_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true
    PipePriceResponse:
      type: object
      required: [price_usd, source, timestamp]
      properties:
        price_usd:
          type: number
          format: double
        source:
          type: string
        timestamp:
          type: string
          format: date-time
    CreateCreditsIntentRequest:
      type: object
      required: [amount_usdc_raw]
      properties:
        amount_usdc_raw:
          type: integer
          format: int64
    CreditsIntentResponse:
      type: object
      required: [intent_id, status, requested_usdc_raw, requested_usdc, detected_usdc_raw, credited_usdc_raw, usdc_mint, treasury_owner_pubkey, treasury_usdc_ata, reference_pubkey]
      properties:
        intent_id:
          type: string
        status:
          type: string
        requested_usdc_raw:
          type: integer
          format: int64
        requested_usdc:
          type: number
          format: double
        detected_usdc_raw:
          type: integer
          format: int64
        credited_usdc_raw:
          type: integer
          format: int64
        usdc_mint:
          type: string
        treasury_owner_pubkey:
          type: string
        treasury_usdc_ata:
          type: string
        reference_pubkey:
          type: string
        payment_tx_sig:
          type: string
          nullable: true
        last_checked_at:
          type: string
          format: date-time
          nullable: true
        credited_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true
    CreditsIntentConflictResponse:
      type: object
      required: [error, intent]
      properties:
        error:
          type: string
        intent:
          $ref: "#/components/schemas/CreditsIntentStatus"
    SubmitCreditsPaymentRequest:
      type: object
      required: [intent_id, tx_sig]
      properties:
        intent_id:
          type: string
        tx_sig:
          type: string
    SubmitCreditsPaymentResponse:
      type: object
      required: [intent_id, status, requested_usdc_raw, detected_usdc_raw, credited_usdc_raw, balance_usdc_raw]
      properties:
        intent_id:
          type: string
        status:
          type: string
        requested_usdc_raw:
          type: integer
          format: int64
        detected_usdc_raw:
          type: integer
          format: int64
        credited_usdc_raw:
          type: integer
          format: int64
        balance_usdc_raw:
          type: integer
          format: int64
        payment_tx_sig:
          type: string
          nullable: true
        last_checked_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true
    CreditsX402ChallengeResponse:
      type: object
      required: [error, intent_id, reference_pubkey, treasury_usdc_ata, usdc_mint, requested_usdc_raw]
      properties:
        error:
          type: string
          example: payment required
        intent_id:
          type: string
        reference_pubkey:
          type: string
        treasury_usdc_ata:
          type: string
        usdc_mint:
          type: string
        requested_usdc_raw:
          type: integer
          format: int64
    X402PaymentRequired:
      type: object
      required: [x402Version, resource, accepts]
      properties:
        x402Version:
          type: integer
          format: int32
          example: 2
        resource:
          type: string
          example: /api/credits/x402
        accepts:
          type: array
          items:
            $ref: "#/components/schemas/X402PaymentAccept"
    X402PaymentAccept:
      type: object
      required: [scheme, network, amount, asset, payTo]
      properties:
        scheme:
          type: string
          example: exact
        network:
          type: string
          example: solana:mainnet
        amount:
          type: string
          example: "1000000"
        asset:
          type: string
        payTo:
          type: string
        maxTimeoutSeconds:
          type: integer
          format: int64
          nullable: true
        extra:
          type: object
          nullable: true
          additionalProperties: true
          description: Contains the credits `intent_id` and `reference_pubkey`.
    X402PaymentResponse:
      type: object
      required: [success, transaction, network]
      properties:
        success:
          type: boolean
        transaction:
          type: string
        network:
          type: string
    CancelCreditsIntentRequest:
      type: object
      required: [intent_id]
      properties:
        intent_id:
          type: string
    CreditsCancelResponse:
      type: object
      required: [intent_id, status]
      properties:
        intent_id:
          type: string
        status:
          type: string
    LifetimeStatusResponse:
      type: object
      required: [lifetime_active]
      properties:
        lifetime_active:
          type: boolean
        lifetime_activated_at:
          type: string
          format: date-time
          nullable: true
        intent:
          $ref: "#/components/schemas/LifetimeIntentStatus"
          nullable: true
        lifetime_promo_title:
          type: string
          nullable: true
        lifetime_promo_body:
          type: string
          nullable: true
        lifetime_terms_url:
          type: string
          nullable: true
    LifetimeIntentStatus:
      type: object
      required: [intent_id, status, required_usdc_raw, detected_usdc_raw, remaining_usdc_raw, usdc_mint, treasury_usdc_ata, reference_pubkey]
      properties:
        intent_id:
          type: string
        status:
          type: string
        required_usdc_raw:
          type: integer
          format: int64
        detected_usdc_raw:
          type: integer
          format: int64
        remaining_usdc_raw:
          type: integer
          format: int64
        usdc_mint:
          type: string
        treasury_usdc_ata:
          type: string
        reference_pubkey:
          type: string
        payment_tx_sig:
          type: string
          nullable: true
        last_checked_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true
    LifetimeIntentResponse:
      allOf:
        - $ref: "#/components/schemas/LifetimeIntentStatus"
        - type: object
          properties:
            lifetime_promo_title:
              type: string
              nullable: true
            lifetime_promo_body:
              type: string
              nullable: true
            lifetime_terms_url:
              type: string
              nullable: true
    SubmitLifetimePaymentRequest:
      type: object
      required: [intent_id, tx_sig]
      properties:
        intent_id:
          type: string
        tx_sig:
          type: string
    SubmitLifetimePaymentResponse:
      type: object
      required: [intent_id, status, required_usdc_raw, detected_usdc_raw, remaining_usdc_raw, lifetime_active]
      properties:
        intent_id:
          type: string
        status:
          type: string
        required_usdc_raw:
          type: integer
          format: int64
        detected_usdc_raw:
          type: integer
          format: int64
        remaining_usdc_raw:
          type: integer
          format: int64
        lifetime_active:
          type: boolean
        payment_tx_sig:
          type: string
          nullable: true
        last_checked_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true
    TokenUsageResponse:
      type: object
      required: [user_id, period, breakdown]
      properties:
        user_id:
          type: string
        period:
          type: string
        breakdown:
          $ref: "#/components/schemas/TokenUsageBreakdownDetail"
    EnhancedTokenUsageResponse:
      type: object
      required: [user_id, period, breakdown]
      properties:
        user_id:
          type: string
        period:
          type: string
        breakdown:
          $ref: "#/components/schemas/EnhancedTokenUsageBreakdownDetail"
    TokenUsageBreakdownDetail:
      type: object
      description: |
        Storage uses GB-month derived from daily snapshots when available.
        Bandwidth uses GB of egress transfers during the selected period.
      required: [storage, bandwidth, total]
      properties:
        storage:
          $ref: "#/components/schemas/TokenUsageBreakdown"
        bandwidth:
          $ref: "#/components/schemas/TokenUsageBreakdown"
        total:
          $ref: "#/components/schemas/TokenUsageBreakdown"
    EnhancedTokenUsageBreakdownDetail:
      type: object
      required: [storage, bandwidth, total]
      properties:
        storage:
          $ref: "#/components/schemas/EnhancedTokenUsageBreakdown"
        bandwidth:
          $ref: "#/components/schemas/EnhancedTokenUsageBreakdown"
        total:
          $ref: "#/components/schemas/TokenUsageBreakdown"
    TokenUsageBreakdown:
      type: object
      description: |
        `gb_transferred` represents GB-month for storage and GB for egress.
        `usdc_charged` represents USDC charged for the selected period.
      required: [gb_transferred, tokens_spent, tokens_burned, tokens_to_treasury, usdc_charged]
      properties:
        gb_transferred:
          type: number
          format: double
          description: GB for egress; GB-month for storage
        tokens_spent:
          type: number
          format: double
        tokens_burned:
          type: number
          format: double
        tokens_to_treasury:
          type: number
          format: double
        usdc_charged:
          type: number
          format: double
          description: USDC charged for the period
    EnhancedTokenUsageBreakdown:
      type: object
      required: [gb_transferred, tokens_spent, tokens_burned, tokens_to_treasury, transfer_count, tier_details, usdc_charged]
      properties:
        gb_transferred:
          type: number
          format: double
          description: GB for egress; GB-month for storage
        tokens_spent:
          type: number
          format: double
        tokens_burned:
          type: number
          format: double
        tokens_to_treasury:
          type: number
          format: double
        transfer_count:
          type: integer
          format: int64
        tier_details:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/TierBreakdown"
        usdc_charged:
          type: number
          format: double
          description: USDC charged for the period
    TierBreakdown:
      type: object
      required: [tier_name, transfer_count, gb_transferred, base_cost, final_cost, avg_multiplier, tokens_burned, tokens_to_treasury]
      properties:
        tier_name:
          type: string
        transfer_count:
          type: integer
          format: int64
        gb_transferred:
          type: number
          format: double
        base_cost:
          type: number
          format: double
        final_cost:
          type: number
          format: double
        avg_multiplier:
          type: number
          format: double
        tokens_burned:
          type: number
          format: double
        tokens_to_treasury:
          type: number
          format: double


    UploadStatus:
      type: object
      required:
        [
          operation_id,
          file_name,
          status,
          finished,
          parts_completed,
          total_parts,
          bytes_total,
          bytes_uploaded,
          created_at,
          updated_at
        ]
      properties:
        operation_id:
          type: string
        file_name:
          type: string
        status:
          type: string
          enum: [queued, running, completed, failed]
        finished:
          type: boolean
        parts_completed:
          type: integer
          format: int64
        total_parts:
          type: integer
          format: int64
        error:
          type: string
          nullable: true
        content_hash:
          type: string
          nullable: true
        deterministic_url:
          type: string
          nullable: true
        bytes_total:
          type: integer
          format: int64
        bytes_uploaded:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    UdpTransferMode:
      type: string
      enum: [upload, download]
    UdpTransferStatus:
      type: string
      enum: [initialized, committed, aborted, expired]
    UdpFecScheme:
      type: string
      enum: [reed-solomon-systematic, raptorq-source-only]
      description: raptorq-source-only also accepts request alias `raptorq`; in this mode server normalizes repair_symbols to 0.
    UdpRepairCodec:
      type: string
      enum: [reed-solomon, none]
      description: Codec used for server-side repair-symbol generation/ingest for the selected FEC scheme. Can be forced to `none` globally via UDP_TRANSPORT_DISABLE_REPAIR_SYMBOLS.
    UdpRaptorQConfig:
      type: object
      required: [symbol_size, source_symbols, repair_symbols, mtu]
      properties:
        symbol_size:
          type: integer
          format: int32
        source_symbols:
          type: integer
          format: int32
        repair_symbols:
          type: integer
          format: int32
        mtu:
          type: integer
          format: int32
    UdpFecSchemeCapability:
      type: object
      required: [name, aliases, repair_codec, supports_repair_ingest, supports_repair_egress]
      properties:
        name:
          $ref: "#/components/schemas/UdpFecScheme"
        aliases:
          type: array
          items:
            type: string
        repair_codec:
          $ref: "#/components/schemas/UdpRepairCodec"
        supports_repair_ingest:
          type: boolean
        supports_repair_egress:
          type: boolean
    UdpCapabilitiesResponse:
      type: object
      required:
        [transport_enabled, socket_enabled, download_egress_enabled, packet_auth_required, default_fec_scheme, supported_fec_schemes, default_raptorq]
      properties:
        transport_enabled:
          type: boolean
        socket_enabled:
          type: boolean
        download_egress_enabled:
          type: boolean
        packet_auth_required:
          type: boolean
        default_fec_scheme:
          $ref: "#/components/schemas/UdpFecScheme"
          description: Server default used when init requests omit fec_scheme (configured by UDP_TRANSPORT_DEFAULT_FEC_SCHEME).
        supported_fec_schemes:
          type: array
          items:
            $ref: "#/components/schemas/UdpFecSchemeCapability"
        default_raptorq:
          $ref: "#/components/schemas/UdpRaptorQConfig"
    UdpUploadInitRequest:
      type: object
      required: [file_name]
      properties:
        file_name:
          type: string
        file_size:
          type: integer
          format: int64
          nullable: true
        fec_scheme:
          $ref: "#/components/schemas/UdpFecScheme"
        repair_codec:
          allOf:
            - $ref: "#/components/schemas/UdpRepairCodec"
          nullable: true
          description: Optional explicit codec override for the session (`none` forces source-only by normalizing repair_symbols to 0).
        symbol_size:
          type: integer
          format: int32
          nullable: true
          description: Repair symbol size. Combined with MTU and packet-auth overhead, must fit UDP payload geometry.
        source_symbols:
          type: integer
          format: int32
          nullable: true
        repair_symbols:
          type: integer
          format: int32
          nullable: true
    UdpUploadInitResponse:
      type: object
      required:
        [session_id, operation_id, storage_key, upload_status_uri, commit_endpoint, abort_endpoint, fallback_http_upload_url, expires_at, fec_scheme, repair_codec, raptorq, packet_auth_required]
      properties:
        session_id:
          type: string
        operation_id:
          type: string
        storage_key:
          type: string
        upload_status_uri:
          type: string
        commit_endpoint:
          type: string
        abort_endpoint:
          type: string
        fallback_http_upload_url:
          type: string
        expires_at:
          type: string
          format: date-time
        fec_scheme:
          $ref: "#/components/schemas/UdpFecScheme"
        repair_codec:
          $ref: "#/components/schemas/UdpRepairCodec"
        raptorq:
          $ref: "#/components/schemas/UdpRaptorQConfig"
        packet_auth_required:
          type: boolean
        packet_auth_algorithm:
          type: string
          nullable: true
        packet_auth_key:
          type: string
          nullable: true
    UdpUploadCommitRequest:
      type: object
      required: [session_id]
      properties:
        session_id:
          type: string
        content_hash:
          type: string
          nullable: true
        deterministic_url:
          type: string
          nullable: true
    UdpUploadCommitResponse:
      type: object
      required: [session_id, status, storage_key, object_size, committed_at]
      properties:
        session_id:
          type: string
        operation_id:
          type: string
          nullable: true
        status:
          $ref: "#/components/schemas/UdpTransferStatus"
        storage_key:
          type: string
        object_size:
          type: integer
          format: int64
        etag:
          type: string
          nullable: true
        content_hash:
          type: string
          nullable: true
        deterministic_url:
          type: string
          nullable: true
        committed_at:
          type: string
          format: date-time
    UdpUploadAbortRequest:
      type: object
      required: [session_id]
      properties:
        session_id:
          type: string
        purge_object:
          type: boolean
    UdpUploadAbortResponse:
      type: object
      required: [session_id, status, purge_object]
      properties:
        session_id:
          type: string
        operation_id:
          type: string
          nullable: true
        status:
          $ref: "#/components/schemas/UdpTransferStatus"
        purge_object:
          type: boolean
    UdpDownloadInitRequest:
      type: object
      required: [file_name]
      properties:
        file_name:
          type: string
        fec_scheme:
          $ref: "#/components/schemas/UdpFecScheme"
        repair_codec:
          allOf:
            - $ref: "#/components/schemas/UdpRepairCodec"
          nullable: true
          description: Optional explicit codec override for the session (`none` forces source-only by normalizing repair_symbols to 0).
        symbol_size:
          type: integer
          format: int32
          nullable: true
          description: Repair symbol size. Combined with MTU and packet-auth overhead, must fit UDP payload geometry.
        source_symbols:
          type: integer
          format: int32
          nullable: true
        repair_symbols:
          type: integer
          format: int32
          nullable: true
    UdpDownloadInitResponse:
      type: object
      required: [session_id, storage_key, file_size, expires_at, fallback_http_download_url, fec_scheme, repair_codec, raptorq]
      properties:
        session_id:
          type: string
        storage_key:
          type: string
        file_size:
          type: integer
          format: int64
        etag:
          type: string
          nullable: true
        expires_at:
          type: string
          format: date-time
        fallback_http_download_url:
          type: string
        fec_scheme:
          $ref: "#/components/schemas/UdpFecScheme"
        repair_codec:
          $ref: "#/components/schemas/UdpRepairCodec"
        raptorq:
          $ref: "#/components/schemas/UdpRaptorQConfig"
        packet_auth_algorithm:
          type: string
          nullable: true
        packet_auth_key:
          type: string
          nullable: true
    UdpDownloadSendRequest:
      type: object
      required: [session_id, target_addr]
      properties:
        session_id:
          type: string
        target_addr:
          type: string
          description: UDP destination as IP:port
        offset:
          type: integer
          format: int64
          nullable: true
        max_bytes:
          type: integer
          format: int64
          nullable: true
          description: Forward burst bytes to send; use 0 for ACK-only update when nack_offsets is empty.
        ack_next_offset:
          type: integer
          format: int64
          nullable: true
          description: Client-acknowledged contiguous receive point; server will advance tracked resume state.
        nack_offsets:
          type: array
          items:
            type: integer
            format: int64
          description: Offsets to retransmit first within the current requested window.
        nack_max_payload_bytes:
          type: integer
          format: int32
          nullable: true
        include_repair_symbols:
          type: boolean
          description: When true, send repair-symbol datagrams (if session repair_symbols > 0) in addition to source packets.
    UdpDownloadSendResponse:
      type: object
      required:
        [session_id, target_addr, offset, offset_source, requested_bytes, bytes_sent, packets_sent, next_offset, done, nack_requested, nack_resent, nack_ignored, resent_bytes, include_repair_symbols, repair_packets_sent, repair_bytes_sent, charged_usdc_raw]
      properties:
        session_id:
          type: string
        target_addr:
          type: string
        offset:
          type: integer
          format: int64
        offset_source:
          type: string
          enum: [request, tracked_next_offset, default_zero]
        ack_next_offset_requested:
          type: integer
          format: int64
          nullable: true
        ack_next_offset_applied:
          type: integer
          format: int64
          nullable: true
        requested_bytes:
          type: integer
          format: int64
        bytes_sent:
          type: integer
          format: int64
        packets_sent:
          type: integer
          format: int64
        next_offset:
          type: integer
          format: int64
        done:
          type: boolean
        packet_auth_algorithm:
          type: string
          nullable: true
        nack_requested:
          type: integer
          format: int64
        nack_resent:
          type: integer
          format: int64
        nack_ignored:
          type: integer
          format: int64
        resent_bytes:
          type: integer
          format: int64
        include_repair_symbols:
          type: boolean
        fec_scheme:
          $ref: "#/components/schemas/UdpFecScheme"
          nullable: true
        repair_codec:
          $ref: "#/components/schemas/UdpRepairCodec"
          nullable: true
        charged_usdc_raw:
          type: integer
          format: int64
        repair_packets_sent:
          type: integer
          format: int64
        repair_bytes_sent:
          type: integer
          format: int64
        truncated_reason:
          type: string
          nullable: true
    UdpSessionStatusResponse:
      type: object
      required:
        [session_id, mode, status, file_name, storage_key, created_at, expires_at, fec_scheme, repair_codec, raptorq]
      properties:
        session_id:
          type: string
        operation_id:
          type: string
          nullable: true
        mode:
          $ref: "#/components/schemas/UdpTransferMode"
        status:
          $ref: "#/components/schemas/UdpTransferStatus"
        file_name:
          type: string
        storage_key:
          type: string
        expected_size:
          type: integer
          format: int64
          nullable: true
        object_size:
          type: integer
          format: int64
          nullable: true
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        committed_at:
          type: string
          format: date-time
          nullable: true
        fec_scheme:
          $ref: "#/components/schemas/UdpFecScheme"
        repair_codec:
          $ref: "#/components/schemas/UdpRepairCodec"
        raptorq:
          $ref: "#/components/schemas/UdpRaptorQConfig"
        ingest:
          $ref: "#/components/schemas/UdpIngestProgress"
          nullable: true
        egress:
          $ref: "#/components/schemas/UdpEgressProgress"
          nullable: true
    UdpIngestProgress:
      type: object
      required:
        [bytes_received, repair_packets_received, reconstructed_bytes, reconstruction_events, repair_blocks_tracked, max_offset_written, covered_bytes, unique_packets_tracked, replay_drops, last_packet_at]
      properties:
        bytes_received:
          type: integer
          format: int64
        repair_packets_received:
          type: integer
          format: int64
        reconstructed_bytes:
          type: integer
          format: int64
        reconstruction_events:
          type: integer
          format: int64
        repair_blocks_tracked:
          type: integer
          format: int64
        max_offset_written:
          type: integer
          format: int64
        declared_total_size:
          type: integer
          format: int64
          nullable: true
        covered_bytes:
          type: integer
          format: int64
        expected_size_complete:
          type: boolean
          nullable: true
        unique_packets_tracked:
          type: integer
          format: int64
        replay_drops:
          type: integer
          format: int64
        last_packet_at:
          type: string
          format: date-time
    UdpEgressProgress:
      type: object
      required:
        [bytes_sent, packets_sent, charged_usdc_raw, bursts_sent, repair_packets_sent, repair_bytes_sent, acked_next_offset, nack_requests, nack_resends, resent_bytes, last_offset, next_offset, last_target_addr, last_send_at]
      properties:
        bytes_sent:
          type: integer
          format: int64
        packets_sent:
          type: integer
          format: int64
        charged_usdc_raw:
          type: integer
          format: int64
        bursts_sent:
          type: integer
          format: int64
        repair_packets_sent:
          type: integer
          format: int64
        repair_bytes_sent:
          type: integer
          format: int64
        acked_next_offset:
          type: integer
          format: int64
        nack_requests:
          type: integer
          format: int64
        nack_resends:
          type: integer
          format: int64
        resent_bytes:
          type: integer
          format: int64
        last_offset:
          type: integer
          format: int64
        next_offset:
          type: integer
          format: int64
        last_target_addr:
          type: string
        last_send_at:
          type: string
          format: date-time
    UdpSessionTouchRequest:
      type: object
      properties:
        extend_secs:
          type: integer
          format: int64
          nullable: true
    UdpSessionTouchResponse:
      type: object
      required: [session_id, status, expires_at]
      properties:
        session_id:
          type: string
        status:
          $ref: "#/components/schemas/UdpTransferStatus"
        expires_at:
          type: string
          format: date-time
    UploadTier:
      type: string
      enum: [normal, priority, premium, ultra, enterprise]
      description: Upload tier.
    DownloadRequest:
      type: object
      required: [file_name]
      properties:
        file_name:
          type: string
          description: File name or path relative to your root.
    DeleteFileRequest:
      type: object
      required: [file_name]
      properties:
        file_name:
          type: string
          description: File name or path relative to your root.
    DeleteFileResponse:
      type: object
      required: [message]
      properties:
        message:
          type: string
    FileListItem:
      type: object
      required: [name, size, mime]
      properties:
        name:
          type: string
        size:
          type: integer
          format: int64
        mime:
          type: string
        created_at:
          type: string
          format: date-time
          nullable: true
        etag:
          type: string
          nullable: true
    FileListResponse:
      type: object
      required: [files]
      properties:
        files:
          type: array
          items:
            $ref: "#/components/schemas/FileListItem"
        next_token:
          type: string
          nullable: true
          description: Continuation token for pagination.
    PortalFileListItem:
      type: object
      required: [file_name, size]
      properties:
        file_name:
          type: string
        size:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
          nullable: true
        etag:
          type: string
          nullable: true
    PortalFilesResponse:
      type: object
      required: [files, total_count, total_size, page_count, has_more]
      properties:
        files:
          type: array
          items:
            $ref: "#/components/schemas/PortalFileListItem"
        total_count:
          type: integer
          format: int64
        total_size:
          type: integer
          format: int64
        page_count:
          type: integer
          format: int64
        has_more:
          type: boolean
    ReceiptItem:
      type: object
      required: [timestamp, applied, bytes, tokens_pipe, usd_amount, is_usd_estimated, operation]
      properties:
        timestamp:
          type: string
          format: date-time
        applied:
          type: string
          enum: [storage, bandwidth]
        bytes:
          type: integer
          format: int64
        tier:
          type: string
          nullable: true
        tokens_pipe:
          type: number
          format: double
        usd_amount:
          type: number
          format: double
        is_usd_estimated:
          type: boolean
        operation:
          type: string
          enum: [upload, download]
    ReceiptsResponse:
      type: object
      required: [items, sum_pipe, sum_usd, count]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ReceiptItem"
        sum_pipe:
          type: string
        sum_usd:
          type: string
        count:
          type: integer
          format: int64
    CreatePublicLinkRequest:
      type: object
      required: [file_name]
      properties:
        file_name:
          type: string
          description: File name or path relative to your root.
        custom_title:
          type: string
        custom_description:
          type: string
    CreatePublicLinkResponse:
      type: object
      required: [link_hash]
      properties:
        link_hash:
          type: string
    DeletePublicLinkRequest:
      type: object
      required: [link_hash]
      properties:
        link_hash:
          type: string
    DeletePublicLinkResponse:
      type: object
      required: [message, link_hash]
      properties:
        message:
          type: string
        link_hash:
          type: string
    StorageBudgetError:
      type: object
      required: [error, required_usdc, available_usdc, overage_allowed, overage_cap_usdc, message]
      properties:
        error:
          type: string
        required_usdc:
          type: number
          format: double
        available_usdc:
          type: number
          format: double
        overage_allowed:
          type: boolean
        overage_cap_usdc:
          type: number
          format: double
        message:
          type: string
    InsufficientCreditsError:
      type: object
      required: [error, required_usdc, balance_usdc, message]
      properties:
        error:
          type: string
        required_usdc:
          type: number
          format: double
        balance_usdc:
          type: number
          format: double
        message:
          type: string
    CreateS3KeyRequest:
      type: object
      properties:
        key_name:
          type: string
          description: Optional display name for the key
    CreateS3KeyResponse:
      type: object
      required: [access_key_id, secret_access_key, endpoint, region, bucket_name, name_prefix]
      properties:
        access_key_id:
          type: string
        secret_access_key:
          type: string
        endpoint:
          type: string
          description: S3 gateway endpoint (path-style)
        region:
          type: string
        bucket_name:
          type: string
        name_prefix:
          type: string
          description: Opaque prefix for advanced clients; treat as an implementation detail
    S3KeyListItem:
      type: object
      required: [key_name, access_key_id, bucket_id, bucket_name, name_prefix, capabilities, created_at]
      properties:
        key_name:
          type: string
        access_key_id:
          type: string
        bucket_id:
          type: string
        bucket_name:
          type: string
        name_prefix:
          type: string
          description: Opaque prefix for advanced clients; treat as an implementation detail
        capabilities:
          type: string
        created_at:
          type: string
          format: date-time
    S3KeyListResponse:
      type: object
      required: [keys]
      properties:
        keys:
          type: array
          items:
            $ref: "#/components/schemas/S3KeyListItem"
    S3KeyRevokeResponse:
      type: object
      required: [status, access_key_id]
      properties:
        status:
          type: string
        access_key_id:
          type: string
    S3InfoResponse:
      type: object
      required: [endpoint, region]
      properties:
        endpoint:
          type: string
        region:
          type: string
    BucketSettingsResponse:
      type: object
      required: [bucket_name, public_read, cors_allowed_origins]
      properties:
        bucket_name:
          type: string
        public_read:
          type: boolean
        cors_allowed_origins:
          type: array
          items:
            type: string
    PatchBucketSettingsRequest:
      type: object
      required: [public_read, cors_allowed_origins]
      properties:
        public_read:
          type: boolean
        cors_allowed_origins:
          type: array
          items:
            type: string
    BillingProductMode:
      type: string
      enum: [standard, wordpress]
    WordPressSiteIntegrationRequest:
      type: object
      required: [install_id, site_url]
      properties:
        install_id:
          type: string
          format: uuid
        site_url:
          type: string
          format: uri
        site_name:
          type: string
          nullable: true
        wp_version:
          type: string
          nullable: true
        plugin_version:
          type: string
          nullable: true
    WordPressSiteConnectionState:
      type: string
      enum: [connected, read_only]
    WordPressAccountStatusResponse:
      type: object
      required:
        - product_mode
        - eligible_for_activation
        - bundled_public_delivery
        - portal_url
        - storage_usdc_raw_per_gb_month
        - storage_usdc_per_gb_month
        - bandwidth_usdc_raw_per_gb
        - bandwidth_usdc_per_gb
        - site_count
      properties:
        product_mode:
          $ref: "#/components/schemas/BillingProductMode"
        eligible_for_activation:
          type: boolean
        eligibility_error:
          type: string
          nullable: true
        bundled_public_delivery:
          type: boolean
        portal_url:
          type: string
          format: uri
        storage_usdc_raw_per_gb_month:
          type: integer
          format: int64
        storage_usdc_per_gb_month:
          type: number
          format: double
        bandwidth_usdc_raw_per_gb:
          type: integer
          format: int64
        bandwidth_usdc_per_gb:
          type: number
          format: double
        site_count:
          type: integer
          format: int64
    WordPressSiteSummaryResponse:
      type: object
      required:
        - install_id
        - bucket_name
        - bucket_scope
        - public_base_url
        - connection_state
        - site_url
        - storage_bytes
        - created_at
      properties:
        install_id:
          type: string
          format: uuid
        bucket_name:
          type: string
        bucket_scope:
          type: string
        public_base_url:
          type: string
          format: uri
        connection_state:
          $ref: "#/components/schemas/WordPressSiteConnectionState"
        site_url:
          type: string
          format: uri
        site_name:
          type: string
          nullable: true
        wp_version:
          type: string
          nullable: true
        plugin_version:
          type: string
          nullable: true
        storage_bytes:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
    WordPressSiteListResponse:
      type: object
      required: [sites]
      properties:
        sites:
          type: array
          items:
            $ref: "#/components/schemas/WordPressSiteSummaryResponse"
    WordPressSiteIntegrationStatusResponse:
      type: object
      required:
        - install_id
        - bucket_name
        - bucket_scope
        - s3_endpoint
        - region
        - bucket_url
        - public_base_url
        - cors_allowed_origins
        - connection_state
        - site_url
        - created_at
      properties:
        install_id:
          type: string
          format: uuid
        bucket_name:
          type: string
        bucket_scope:
          type: string
        s3_endpoint:
          type: string
          format: uri
        region:
          type: string
        bucket_url:
          type: string
          format: uri
        public_base_url:
          type: string
          format: uri
        cors_allowed_origins:
          type: array
          items:
            type: string
        connection_state:
          $ref: "#/components/schemas/WordPressSiteConnectionState"
        site_url:
          type: string
          format: uri
        site_name:
          type: string
          nullable: true
        wp_version:
          type: string
          nullable: true
        plugin_version:
          type: string
          nullable: true
        active_access_key_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    WordPressSiteIntegrationConnectResponse:
      allOf:
        - $ref: "#/components/schemas/WordPressSiteIntegrationStatusResponse"
        - type: object
          required: [access_key_id, secret_access_key]
          properties:
            access_key_id:
              type: string
            secret_access_key:
              type: string
    WordPressSiteIntegrationDisconnectResponse:
      type: object
      required:
        - install_id
        - bucket_name
        - bucket_scope
        - bucket_url
        - public_base_url
        - connection_state
        - revoked_access_key_ids
        - site_url
        - created_at
      properties:
        install_id:
          type: string
          format: uuid
        bucket_name:
          type: string
        bucket_scope:
          type: string
        bucket_url:
          type: string
          format: uri
        public_base_url:
          type: string
          format: uri
        connection_state:
          $ref: "#/components/schemas/WordPressSiteConnectionState"
        revoked_access_key_ids:
          type: array
          items:
            type: string
        active_access_key_id:
          type: string
          nullable: true
        site_url:
          type: string
          format: uri
        site_name:
          type: string
          nullable: true
        wp_version:
          type: string
          nullable: true
        plugin_version:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    S3XmlResponse:
      type: object
      description: XML response body
    BinaryRequest:
      type: string
      format: binary
    BinaryResponse:
      type: string
      format: binary
    MessageResponse:
      type: object
      required: [message]
      properties:
        message:
          type: string
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
        message:
          type: string
        migration_endpoint:
          type: string
