openapi: 3.1.0
info:
  title: CashLink Public Image Space API
  version: 2.0.0
  description: Anonymous image hosting backed only by add-on storage. `quota_balance_bytes` means storage; paid browser image traffic (`quota_type=traffic`) is not an API product and is rejected. Terms version 2026-08-08.
servers:
  - url: /api/v2/image-space
tags:
  - name: Accounts
  - name: Payments
  - name: Images
  - name: Upload sessions
paths:
  /accounts:
    post:
      tags: [Accounts]
      operationId: createImageAccount
      security: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': {$ref: '#/components/responses/Envelope'}
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /accounts/recover:
    post:
      tags: [Accounts]
      operationId: recoverImageAccount
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [recovery_code]
              properties:
                recovery_code: {type: string, pattern: '^climgr_[A-Za-z0-9_-]{43}$'}
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /account:
    get:
      tags: [Accounts]
      operationId: getImageAccount
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /account/token-rotations:
    post:
      tags: [Accounts]
      operationId: rotateImageAccountToken
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /quota/quote:
    get:
      tags: [Payments]
      operationId: quoteImageCapacity
      description: Quotes 100 MiB add-on-storage units. This endpoint is storage-only and rejects `quota_type=traffic`.
      security: []
      parameters:
        - name: units
          in: query
          required: true
          schema: {type: integer, minimum: 1, maximum: 100}
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders:
    get:
      tags: [Payments]
      operationId: listImagePaymentOrders
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
    post:
      tags: [Payments]
      operationId: createImagePaymentOrder
      description: Creates an add-on-storage order. Traffic orders are unavailable through Image Space API.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/PaymentOrderInput'}
      responses:
        '201': {$ref: '#/components/responses/Envelope'}
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Payments]
      operationId: getImagePaymentOrder
      security: [{OrderToken: []}]
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}/activate:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    post:
      tags: [Payments]
      operationId: activateImagePaymentOrder
      security: [{OrderToken: []}]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [saved_order_token]
              properties:
                saved_order_token: {type: boolean, const: true}
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}/events:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Payments]
      operationId: streamImagePaymentOrderEvents
      security: [{OrderToken: []}]
      parameters:
        - name: Last-Event-ID
          in: header
          schema: {type: string}
      responses:
        '200':
          description: Server-sent event stream
          content:
            text/event-stream:
              schema: {type: string}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}/qr:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Payments]
      operationId: getImagePaymentOrderQr
      security: [{OrderToken: []}]
      responses:
        '200':
          description: Payment QR code
          content:
            image/svg+xml:
              schema: {type: string}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}/refunds:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Payments]
      operationId: listImagePaymentOrderRefunds
      security: [{OrderToken: []}]
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /images:
    get:
      tags: [Images]
      operationId: listImages
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
    post:
      tags: [Images]
      operationId: uploadImage
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [image]
              properties:
                image: {type: string, format: binary}
                title: {type: string, maxLength: 120}
                tags: {type: string, maxLength: 300}
                is_nsfw: {type: boolean}
                is_public_gallery: {type: boolean}
                keep_original: {type: boolean}
      responses:
        '202': {$ref: '#/components/responses/Envelope'}
        '402': {$ref: '#/components/responses/Problem'}
        default: {$ref: '#/components/responses/Problem'}
  /images/{publicCode}:
    parameters:
      - $ref: '#/components/parameters/PublicCode'
    get:
      tags: [Images]
      operationId: getImage
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
    patch:
      tags: [Images]
      operationId: updateImage
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: {type: string, maxLength: 120}
                tags:
                  type: array
                  maxItems: 10
                  items: {type: string, maxLength: 30}
                is_nsfw: {type: boolean}
                is_public_gallery: {type: boolean}
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
    delete:
      tags: [Images]
      operationId: deleteImage
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204': {description: Image deleted and paid capacity released}
        default: {$ref: '#/components/responses/Problem'}
  /images/{publicCode}/rescans:
    parameters:
      - $ref: '#/components/parameters/PublicCode'
    post:
      tags: [Images]
      operationId: rescanImage
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /upload-sessions:
    post:
      tags: [Upload sessions]
      operationId: createImageUploadSession
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/UploadSessionInput'}
      responses:
        '201': {$ref: '#/components/responses/Envelope'}
        '402': {$ref: '#/components/responses/Problem'}
        default: {$ref: '#/components/responses/Problem'}
  /upload-sessions/{session}:
    parameters:
      - $ref: '#/components/parameters/SessionId'
      - $ref: '#/components/parameters/UploadToken'
    get:
      tags: [Upload sessions]
      operationId: getImageUploadSession
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
    patch:
      tags: [Upload sessions]
      operationId: appendImageUploadChunk
      parameters:
        - name: Upload-Offset
          in: header
          required: true
          schema: {type: integer, minimum: 0}
        - name: Upload-Chunk-SHA256
          in: header
          required: true
          schema: {type: string, pattern: '^[a-f0-9]{64}$'}
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: {type: string, format: binary}
      responses:
        '200': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
    delete:
      tags: [Upload sessions]
      operationId: cancelImageUploadSession
      responses:
        '204': {description: Session cancelled and reservation released}
        default: {$ref: '#/components/responses/Problem'}
  /upload-sessions/{session}/complete:
    parameters:
      - $ref: '#/components/parameters/SessionId'
      - $ref: '#/components/parameters/UploadToken'
    post:
      tags: [Upload sessions]
      operationId: completeImageUploadSession
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202': {$ref: '#/components/responses/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
security:
  - AccountToken: []
components:
  securitySchemes:
    AccountToken:
      type: http
      scheme: bearer
      bearerFormat: climg_
    OrderToken:
      type: http
      scheme: bearer
      bearerFormat: clapi_
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: {type: string, minLength: 16, maxLength: 200}
    OrderId:
      name: order
      in: path
      required: true
      schema: {type: string, format: uuid}
    PublicCode:
      name: publicCode
      in: path
      required: true
      schema: {type: string, pattern: '^[A-Za-z0-9_-]{10}$'}
    SessionId:
      name: session
      in: path
      required: true
      schema: {type: string, format: uuid}
    UploadToken:
      name: Upload-Token
      in: header
      required: true
      schema: {type: string}
  schemas:
    Envelope:
      type: object
      required: [data, links, meta]
      properties:
        data: {type: object, additionalProperties: true}
        links:
          type: object
          required: [self]
          properties:
            self: {type: string, format: uri}
        meta:
          type: object
          required: [contract_version]
          properties:
            contract_version: {type: integer, const: 2}
    Problem:
      type: object
      required: [type, title, status, detail, code]
      properties:
        type: {type: string, format: uri}
        title: {type: string}
        status: {type: integer}
        detail: {type: string}
        code: {type: string}
        instance: {type: string}
        request_id: {type: string}
        errors: {type: object, additionalProperties: true}
    PaymentOrderInput:
      type: object
      required: [units, quote_token, refund_address, refund_address_confirmation, terms_version, terms_accepted]
      properties:
        units: {type: integer, minimum: 1, maximum: 100}
        quote_token: {type: string}
        refund_address: {type: string}
        refund_address_confirmation: {type: string}
        terms_version: {type: string, const: '2026-08-08'}
        terms_accepted: {type: boolean, const: true}
        webhook_url: {type: string, format: uri, pattern: '^https://'}
    UploadSessionInput:
      type: object
      required: [name, size]
      properties:
        name: {type: string, maxLength: 255}
        mime: {type: string, maxLength: 150}
        size: {type: integer, minimum: 1, maximum: 20971520}
        title: {type: string, maxLength: 120}
        tags: {type: string, maxLength: 300}
        is_nsfw: {type: boolean}
        is_public_gallery: {type: boolean}
        keep_original: {type: boolean}
  responses:
    Envelope:
      description: Successful JSON response
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Envelope'}
    Problem:
      description: RFC 9457 problem details
      content:
        application/problem+json:
          schema: {$ref: '#/components/schemas/Problem'}
