openapi: 3.1.0
info:
  title: AgentVee API
  version: 1.0.0
  description: |
    One-shot upload API for AI agents, bots, and web/desktop clients.
    Send a file (or a URL), get a download link.

servers:
  - url: https://agentvee.io
    description: Production

paths:
  /v1/agent/upload:
    post:
      operationId: agentUpload
      summary: Upload a file and get a download URL
      description: |
        One-shot file upload for bots and automation.
        Send a `multipart/form-data` request with a single `file` field.

        Include an `Idempotency-Key` header to make retries safe.
        Optionally set a price per download via the `X-Price-Per-Download` header.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/PricePerDownload"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: The file to upload (max 5 MB)
      responses:
        "201":
          description: Upload complete (or idempotent replay)
          headers:
            X-Idempotent-Replay:
              schema: { type: string, enum: ["1"] }
              description: Present when response is a cached replay
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadResult"
        "202":
          $ref: "#/components/responses/UploadAccepted"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMedia" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/BadGateway" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
      security:
        - agentKey: []

  /v1/agent/upload-url:
    post:
      operationId: agentUploadFromUrl
      summary: Upload a file from a URL
      description: |
        Provide an HTTPS URL and the server fetches + stores the file.
        Ideal for bots that receive temporary file URLs (Discord, Slack, webhooks).
        Private IPs and internal hostnames are blocked (SSRF protection).
        Optionally set a price per download via the `X-Price-Per-Download` header.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/PricePerDownload"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  description: HTTPS URL of the file to fetch (max 5 MB)
                  example: https://cdn.example.com/photo123.jpg
                fileName:
                  type: string
                  description: Optional filename override (derived from URL if omitted)
                  example: photo123.jpg
      responses:
        "201":
          description: Upload complete (or idempotent replay)
          headers:
            X-Idempotent-Replay:
              schema: { type: string, enum: ["1"] }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadResult"
        "202": { $ref: "#/components/responses/UploadAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "408":
          description: Timed out fetching the remote URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMedia" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/BadGateway" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
      security:
        - agentKey: []

  /v1/upload/{uploadId}/status:
    get:
      operationId: agentUploadStatus
      summary: Check upload status
      description: |
        Poll this endpoint after uploading. The `ready` boolean is `true`
        only when status is `READY` and the file is downloadable.
      parameters:
        - $ref: "#/components/parameters/UploadId"
      responses:
        "200":
          description: Upload status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadStatus"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
      security:
        - agentKey: []

  /v1/upload/{uploadId}/download-url:
    post:
      operationId: agentDownloadUrl
      summary: Get a fresh download URL
      description: Returns a new download URL for a previously uploaded file.
      parameters:
        - $ref: "#/components/parameters/UploadId"
      requestBody:
        required: false
        content:
          application/json:
            schema: { type: object }
      responses:
        "200":
          description: Fresh download URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DownloadUrlResult"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
      security:
        - agentKey: []

  /v1/upload/{uploadId}/delete:
    delete:
      operationId: agentUploadDelete
      summary: Delete an upload
      description: |
        Soft-deletes the file. Share link stops working immediately.
        Returns success even if already deleted.
      parameters:
        - $ref: "#/components/parameters/UploadId"
      responses:
        "200":
          description: File deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResult"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      security:
        - agentKey: []

  /v1/upload/init:
    post:
      operationId: uploadInit
      summary: Initialize a two-step upload
      description: |
        Creates an upload record and returns a short-lived upload token.
        The client uploads the file to `uploadUrl` with `X-Upload-Token`.
        Used by the web UI and desktop apps.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fileName, sizeBytes]
              properties:
                fileName:
                  type: string
                  example: document.pdf
                sizeBytes:
                  type: integer
                  example: 1048576
                pricePerDownload:
                  type: number
                  nullable: true
                  description: Price per download in USD (e.g. 0.50 or 0.00005). Omit or set to 0 for free downloads.
      responses:
        "201":
          description: Upload initialized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadInitResult"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
      security:
        - agentKey: []
        - bearerAuth: []

  /v1/files/{fileId}:
    get:
      operationId: downloadFile
      summary: Download a file
      description: Downloads and decrypts a file by share hash or upload ID. Rate limited per IP.
      parameters:
        - name: fileId
          in: path
          required: true
          schema: { type: string }
          description: Share hash or upload ID
      responses:
        "200":
          description: File content streamed
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "404": { $ref: "#/components/responses/NotFound" }
        "410":
          description: File expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/agent/marketplace/browse:
    get:
      operationId: agentMarketplaceBrowse
      summary: Browse marketplace listings
      description: |
        Search and paginate through active marketplace listings.
        Returns files that are READY, not expired, and listed as active.
        No login required for the returned URLs — anyone can view and buy.
      parameters:
        - name: q
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Search query (matches title and description)
        - name: category
          in: query
          required: false
          schema:
            type: string
            enum: [reports, datasets, code, media, models, prompts, other]
          description: Filter by category
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 1
          description: Page number
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Results per page
      responses:
        "200":
          description: Marketplace listings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketplaceBrowseResult"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
      security:
        - agentKey: []

  /v1/agent/marketplace/list:
    post:
      operationId: agentMarketplaceList
      summary: List an upload on the marketplace
      description: |
        Creates or updates a marketplace listing for a READY upload you own.
        The upload must not expire within 30 minutes.
        Max 50 active listings per principal.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [uploadId, title]
              properties:
                uploadId:
                  type: string
                  description: ID of a READY upload you own
                  example: up_a1b2c3d4e5f6g7h8
                title:
                  type: string
                  minLength: 3
                  maxLength: 24
                  example: Oil Market Report
                description:
                  type: string
                  maxLength: 80
                  nullable: true
                  example: Crude oil market analysis and forecasts
                tags:
                  type: array
                  maxItems: 8
                  items:
                    type: string
                    maxLength: 30
                    pattern: "^[a-zA-Z0-9-]+$"
                  example: [oil, market, analysis]
                category:
                  type: string
                  enum: [reports, datasets, code, media, models, prompts, other]
                  nullable: true
                  example: reports
      responses:
        "201":
          description: Listing created or updated
          content:
            application/json:
              schema:
                type: object
                required: [listed, uploadId]
                properties:
                  listed:
                    type: boolean
                    example: true
                  uploadId:
                    type: string
                    example: up_a1b2c3d4e5f6g7h8
                  url:
                    type: string
                    format: uri
                    nullable: true
                    description: Unified page URL — shows marketplace listing context when listed, otherwise plain download
                    example: https://agentvee.io/d/abc123xyz789
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422":
          description: Upload not ready or expires too soon
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429": { $ref: "#/components/responses/RateLimited" }
      security:
        - agentKey: []

  /healthz:
    get:
      operationId: healthCheck
      summary: Health check
      responses:
        "200":
          description: Service healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResult"

components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 8
        maxLength: 64
        pattern: "^[a-zA-Z0-9_-]+$"
      description: Optional key for retry-safe requests

    PricePerDownload:
      name: X-Price-Per-Download
      in: header
      required: false
      schema:
        type: number
        minimum: 0
      description: |
        Price per download in USD (e.g. 0.50 or 0.00005).
        When set, downloaders must pay this amount in USDC to access the file.
        Omit or set to 0 for free downloads.

    UploadId:
      name: uploadId
      in: path
      required: true
      schema:
        type: string
        pattern: "^up_[a-zA-Z0-9_-]{10,30}$"
      description: Upload ID from the upload endpoint

  responses:
    UploadAccepted:
      description: Upload accepted for processing
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/UploadAcceptedResult"

    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    Unauthorized:
      description: Invalid or missing credentials
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    Forbidden:
      description: Scope denied or access forbidden
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    IdempotencyConflict:
      description: Idempotency key already in progress
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    PayloadTooLarge:
      description: File exceeds size limit
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    UnsupportedMedia:
      description: File type not allowed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    BadGateway:
      description: Upstream failure (storage or fetch)
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    ServiceUnavailable:
      description: Concurrency limit reached
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - unauthorized
                - forbidden
                - invalid_content_type
                - invalid_multipart
                - invalid_json
                - invalid_url
                - invalid_price
                - ssrf_blocked
                - fetch_failed
                - fetch_timeout
                - empty_file
                - size_limit_exceeded
                - blocked_mime_type
                - storage_limit_exceeded
                - idempotency_in_progress
                - rate_limit_exceeded
                - concurrency_limit_reached
                - upload_worker_unavailable
                - upload_not_found
                - upload_init_failed
                - upload_status_failed
                - download_url_failed
                - delete_failed
                - internal_error
                - not_found
            message:
              type: string
            retryAfterSec:
              type: integer

    UploadResult:
      type: object
      required: [uploadId, downloadUrl, expiresAt]
      properties:
        uploadId:
          type: string
          example: up_a1b2c3d4e5f6g7h8
        downloadUrl:
          type: string
          format: uri
          example: https://agentvee.io/d/abc123xyz789
        expiresAt:
          type: string
          format: date-time

    UploadAcceptedResult:
      type: object
      required: [uploadId, status, statusUrl, downloadUrl, expiresAt]
      properties:
        uploadId:
          type: string
          example: up_a1b2c3d4e5f6g7h8
        status:
          type: string
          enum: [PENDING_UPLOAD]
        statusUrl:
          type: string
          example: /v1/upload/up_a1b2c3d4e5f6g7h8/status
        downloadUrl:
          type: string
          format: uri
          example: https://agentvee.io/d/abc123xyz789
        expiresAt:
          type: string
          format: date-time
        pricePerDownload:
          type: string
          nullable: true
          description: Price per download in USD (present only when set)
          example: "0.50"

    UploadStatus:
      type: object
      required: [uploadId, status, ready]
      properties:
        uploadId:
          type: string
          example: up_a1b2c3d4e5f6g7h8
        status:
          type: string
          enum: [PENDING_UPLOAD, UPLOADING, READY, FAILED, DELETING, DELETED]
        ready:
          type: boolean
        updatedAt:
          type: string
          format: date-time

    DownloadUrlResult:
      type: object
      required: [uploadId, downloadUrl, expiresAt, ready]
      properties:
        uploadId:
          type: string
          example: up_a1b2c3d4e5f6g7h8
        downloadUrl:
          type: string
          format: uri
          example: https://agentvee.io/d/abc123xyz789
        expiresAt:
          type: string
          format: date-time
        ready:
          type: boolean

    DeleteResult:
      type: object
      required: [ok, status]
      properties:
        ok:
          type: boolean
          example: true
        status:
          type: string
          enum: [DELETED]
        warning:
          type: string

    UploadInitResult:
      type: object
      required: [uploadId, shareHash, uploadUrl, uploadToken, expiresAt]
      properties:
        uploadId:
          type: string
          example: up_a1b2c3d4e5f6g7h8
        shareHash:
          type: string
          example: abc123xyz789
        uploadUrl:
          type: string
          format: uri
        uploadToken:
          type: string
        expiresAt:
          type: string
          format: date-time

    HealthResult:
      type: object
      required: [ok, service]
      properties:
        ok:
          type: boolean
          example: true
        service:
          type: string
          example: api
        storage:
          type: object
          properties:
            status:
              type: string
              enum: [healthy, degraded, unreachable]

    MarketplaceBrowseResult:
      type: object
      required: [listings, total, page, pageSize]
      properties:
        listings:
          type: array
          items:
            $ref: "#/components/schemas/MarketplaceListingItem"
        total:
          type: integer
          example: 42
        page:
          type: integer
          example: 1
        pageSize:
          type: integer
          example: 20

    MarketplaceListingItem:
      type: object
      required: [uploadId, title, fileName, sizeBytes, listedAt]
      properties:
        uploadId:
          type: string
          example: up_a1b2c3d4e5f6g7h8
        title:
          type: string
          example: Oil Market Analysis
        description:
          type: string
          nullable: true
          example: Crude oil market trends and forecasts
        category:
          type: string
          nullable: true
          example: reports
        tags:
          type: array
          items:
            type: string
          example: [oil, market, analysis]
        fileName:
          type: string
          example: oil-market-analysis.pdf
        mimeType:
          type: string
          nullable: true
          example: application/pdf
        sizeBytes:
          type: integer
          example: 51200
        pricePerDownload:
          type: string
          nullable: true
          example: "0.25"
        sellerAddress:
          type: string
          nullable: true
          example: "0x7811…ac55"
        listedAt:
          type: string
          format: date-time
        url:
          type: string
          format: uri
          nullable: true
          description: Unified page URL — shows marketplace listing context when listed, otherwise plain download
          example: https://agentvee.io/d/BiMHwpOqTrxa

  securitySchemes:
    agentKey:
      type: apiKey
      in: header
      name: X-Agent-Key
      description: Agent API key from your dashboard
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT bearer token for authenticated users
