openapi: 3.1.0
info:
  title: FooPixel API
  version: 1.0.0-alpha
  summary: Agent-native image hosting with account-managed delivery settings.
  description: |
    Alpha contract. FooPixel accepts image bytes and returns a public URL for the
    original. Anonymous assets expire. Some ownership and delivery settings are
    available only through the signed-in account interface. Image variants are not
    implemented.
servers:
  - url: https://api.foopixel.com
    description: Alpha API (availability not guaranteed)
tags:
  - name: Uploads
  - name: Assets
  - name: Delivery
paths:
  /v1/uploads:
    post:
      tags: [Uploads]
      operationId: createUpload
      summary: Create a direct-upload session
      description: Anonymous callers are limited to 5 MiB input and 2000px output width. Authenticated accounts may have a larger policy. Uploaded images are processed and may be moderated before delivery.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UploadRequest" }
      responses:
        "201":
          description: Upload session created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateUploadResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
  /v1/uploads/{id}/complete:
    post:
      tags: [Uploads]
      operationId: completeUpload
      summary: Complete an upload after bytes are stored
      description: The body must contain the completionToken returned by createUpload. The token is one-time and is never returned in asset metadata.
      parameters: [{ $ref: "#/components/parameters/AssetId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [uploadToken]
              properties:
                uploadToken: { type: string, pattern: "^upl_[0-9a-f-]{72}$" }
      responses:
        "200":
          description: Upload accepted; scanning may still be pending.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompleteUploadResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "410": { $ref: "#/components/responses/Gone" }
  /v1/assets/{id}:
    get:
      tags: [Assets]
      operationId: getAsset
      summary: Get asset metadata for an account
      security: [{ ApiKeyAuth: [] }, { AccountAccessToken: [] }]
      parameters: [{ $ref: "#/components/parameters/AssetId" }]
      responses:
        "200":
          description: Asset metadata.
          content:
            {
              application/json:
                { schema: { $ref: "#/components/schemas/AssetEnvelope" } },
            }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/assets:
    get:
      tags: [Assets]
      operationId: listAssets
      summary: List assets visible to an authenticated account
      security: [{ ApiKeyAuth: [] }, { AccountAccessToken: [] }]
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        "200":
          description: Account assets.
          content:
            {
              application/json:
                { schema: { $ref: "#/components/schemas/AssetListEnvelope" } },
            }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /a/{publicId}:
    get:
      tags: [Delivery]
      operationId: getOriginal
      summary: Stream an original image by public ID
      description: Public IDs are opaque and have no required prefix. Anonymous images expire. Caching and variant availability depend on account settings; variants are not implemented in v1.
      security: []
      parameters: [{ $ref: "#/components/parameters/PublicId" }]
      responses:
        "200":
          description: Original image bytes.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
          content: { image/*: { schema: { type: string, format: binary } } }
        "206":
          description: Requested byte range of the original.
          headers:
            Content-Range:
              {
                description: Delivered byte range and complete object size.,
                schema: { type: string },
              }
        "304": { description: Not modified when If-None-Match matches. }
        "403": { description: Variant request while account CDN is disabled. }
        "404": { $ref: "#/components/responses/NotFound" }
        "410": { $ref: "#/components/responses/Gone" }
        "416":
          { description: Requested byte range is malformed or unsatisfiable. }
        "501": { description: Variant generation is not implemented in alpha. }
    head:
      tags: [Delivery]
      operationId: headOriginal
      summary: Inspect original headers without downloading bytes
      security: []
      parameters: [{ $ref: "#/components/parameters/PublicId" }]
      responses:
        "200": { description: Original exists; response has no body. }
        "404": { $ref: "#/components/responses/NotFound" }
        "410": { $ref: "#/components/responses/Gone" }
components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: FooPixelApiKey
      description: API key for authenticated account asset-management requests.
    AccountAccessToken:
      type: http
      scheme: bearer
      description: Access token for authenticated account requests.
  parameters:
    AssetId:
      name: id
      in: path
      required: true
      schema: { type: string, pattern: "^ast_[A-Za-z0-9_-]+$" }
    PublicId:
      name: publicId
      in: path
      required: true
      schema: { type: string, minLength: 1, pattern: "^[A-Za-z0-9_-]+$" }
  headers:
    ETag:
      description: Entity tag for conditional requests.
      schema: { type: string }
    CacheControl:
      description: Cache policy for the delivered image.
      schema: { type: string }
  schemas:
    UploadRequest:
      type: object
      required: [contentType, byteSize, checksumSha256]
      properties:
        filename: { type: string, minLength: 1, maxLength: 255 }
        contentType:
          {
            type: string,
            enum: [image/jpeg, image/png, image/webp, image/heic, image/heif],
          }
        byteSize:
          {
            type: integer,
            minimum: 1,
            maximum: 20971520,
            description: Checked against the policy returned by the server (5 MiB for anonymous uploads today).,
          }
        checksumSha256:
          {
            type: string,
            pattern: "^[A-Fa-f0-9]{64}$",
            description: Required SHA-256 of the exact upload bytes,
            verified before normalization.,
          }
        processing:
          type: object
          description: Optional declaration that the browser already normalized this image. Omit for server normalization.
          required: [mode, maxWidth, optimized]
          properties:
            mode: { type: string, enum: [client] }
            maxWidth: { type: integer, const: 2000 }
            optimized: { type: boolean, const: true }
    SignedUpload:
      type: object
      required: [method, url, headers, expiresAt]
      properties:
        method: { type: string, enum: [PUT] }
        url: { type: string, format: uri }
        headers: { type: object, additionalProperties: { type: string } }
        expiresAt: { type: string, format: date-time }
    CreateUploadResponse:
      type: object
      required:
        [
          assetId,
          publicUrl,
          claimUrl,
          expiresAt,
          policy,
          upload,
          completionToken,
        ]
      properties:
        assetId: { type: string, example: ast_01JX8W9W7K9A }
        publicUrl:
          {
            type: string,
            format: uri,
            example: "https://media.foopixel.com/a/01JX8W9W7K9A",
          }
        claimUrl:
          {
            type: string,
            format: uri,
            description: Account claim URL when available.,
          }
        expiresAt: { type: string, format: date-time }
        policy: { $ref: "#/components/schemas/UploadPolicy" }
        upload: { $ref: "#/components/schemas/SignedUpload" }
        completionToken:
          { type: string, pattern: "^upl_[0-9a-f-]{72}$", writeOnly: true }
    Asset:
      type: object
      required:
        [
          id,
          publicId,
          contentType,
          byteSize,
          processingStatus,
          createdAt,
          publicUrl,
          ownership,
          cdn,
        ]
      properties:
        id: { type: string, example: ast_01JX8W9W7K9A }
        publicId: { type: string, example: 01JX8W9W8W4B }
        filename: { type: string }
        contentType:
          {
            type: string,
            enum: [image/jpeg, image/png, image/webp, image/heic, image/heif],
          }
        byteSize: { type: integer }
        processingStatus:
          {
            type: string,
            enum:
              [pending_upload, pending_scan, ready, rejected, deleted, expired],
          }
        createdAt: { type: string, format: date-time }
        uploadedAt: { type: string, format: date-time }
        expiresAt: { type: string, format: date-time }
        claimedAt: { type: string, format: date-time }
        publicUrl: { type: string, format: uri }
        claimUrl: { type: string, format: uri }
        ownership: { type: string, enum: [anonymous, claimed] }
        cdn: { $ref: "#/components/schemas/CdnStatus" }
    CdnStatus:
      type: object
      required: [enabled]
      properties:
        enabled: { type: boolean }
    UploadPolicy:
      type: object
      required: [maxUploadBytes, maxWidth, outputContentType]
      properties:
        maxUploadBytes: { type: integer, minimum: 1, maximum: 20971520 }
        maxWidth: { type: integer, minimum: 1 }
        outputContentType: { type: string, const: image/webp }
    AssetEnvelope:
      type: object
      required: [asset]
      properties: { asset: { $ref: "#/components/schemas/Asset" } }
    AssetListEnvelope:
      type: object
      required: [assets]
      properties:
        {
          assets:
            { type: array, items: { $ref: "#/components/schemas/Asset" } },
        }
    CompleteUploadResponse:
      type: object
      required: [asset, processingStatus]
      properties:
        asset: { $ref: "#/components/schemas/Asset" }
        processingStatus: { type: string, enum: [pending_scan, ready] }
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, requestId]
          properties:
            code: { type: string, example: invalid_request }
            message: { type: string }
            requestId: { type: string }
  responses:
    BadRequest:
      description: Request validation failed.
      content:
        {
          application/json:
            { schema: { $ref: "#/components/schemas/ErrorEnvelope" } },
        }
    Unauthorized:
      description: Authentication is missing or invalid.
      content:
        {
          application/json:
            { schema: { $ref: "#/components/schemas/ErrorEnvelope" } },
        }
    Forbidden:
      description: Completion token is invalid or operation is forbidden.
      content:
        {
          application/json:
            { schema: { $ref: "#/components/schemas/ErrorEnvelope" } },
        }
    NotFound:
      description: Resource does not exist or is not visible.
      content:
        {
          application/json:
            { schema: { $ref: "#/components/schemas/ErrorEnvelope" } },
        }
    Conflict:
      description: Resource is not in a state that permits this operation.
      content:
        {
          application/json:
            { schema: { $ref: "#/components/schemas/ErrorEnvelope" } },
        }
    Gone:
      description: Asset or upload session has expired or was deleted.
      content:
        {
          application/json:
            { schema: { $ref: "#/components/schemas/ErrorEnvelope" } },
        }
