openapi: 3.1.0
info:
  title: TessaRun API
  version: 0.1.0
  description: >-
    Workspace-isolated managed content and GenAI infrastructure. This is an
    implementation-preview contract; compatibility guarantees begin when the
    first stable version is published.
servers:
  - url: https://api.tessarun.com/api/v1
  - url: http://localhost:4100/api/v1
security:
  - workspaceKey: []
tags:
  - name: Models
  - name: Generations
  - name: Assets
  - name: Webhooks
    description: >-
      Signed HTTPS event delivery. Deliveries include webhook-id,
      webhook-timestamp, and webhook-signature headers.
  - name: Usage
webhooks:
  generationEvent:
    post:
      operationId: receiveGenerationEvent
      tags: [Webhooks]
      summary: Receive a selected generation lifecycle event
      security: []
      description: >-
        Verify the timestamp and HMAC signature against the exact raw request
        body before parsing JSON.
      parameters:
        - name: webhook-id
          in: header
          required: true
          schema: {type: string}
        - name: webhook-timestamp
          in: header
          required: true
          schema: {type: string, pattern: "^[0-9]+$"}
        - name: webhook-signature
          in: header
          required: true
          schema: {type: string, pattern: "^v1,"}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
      responses:
        "200":
          description: Event accepted by the receiver
paths:
  /models:
    get:
      operationId: listModels
      tags: [Models]
      summary: List discoverable logical models
      responses:
        "200":
          description: Model catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelList"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /models/{model_id}:
    get:
      operationId: getModel
      tags: [Models]
      summary: Get one logical model
      parameters:
        - $ref: "#/components/parameters/ModelId"
      responses:
        "200":
          description: Model
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Model"
        "404":
          $ref: "#/components/responses/NotFound"
  /generations:
    get:
      operationId: listGenerations
      tags: [Generations]
      summary: List Workspace generations
      parameters:
        - $ref: "#/components/parameters/TenantFilter"
      responses:
        "200":
          description: Generation list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenerationList"
    post:
      operationId: createGeneration
      tags: [Generations]
      summary: Submit fixed-price managed generation work
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateGeneration"
      responses:
        "202":
          description: Newly accepted generation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Generation"
        "402":
          description: Insufficient prepaid funds or Account past due
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          $ref: "#/components/responses/ValidationError"
  /generations/{generation_id}:
    get:
      operationId: getGeneration
      tags: [Generations]
      summary: Get one Workspace generation
      parameters:
        - $ref: "#/components/parameters/GenerationId"
      responses:
        "200":
          description: Generation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Generation"
        "404":
          $ref: "#/components/responses/NotFound"
  /assets:
    get:
      operationId: listAssets
      tags: [Assets]
      summary: List Workspace assets
      parameters:
        - $ref: "#/components/parameters/TenantFilter"
      responses:
        "200":
          description: Asset list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetList"
    post:
      operationId: createAssetUpload
      tags: [Assets]
      summary: Create an Asset and direct upload URL
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAsset"
      responses:
        "201":
          description: Pending Asset and presigned upload request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetUpload"
        "422":
          $ref: "#/components/responses/ValidationError"
  /assets/{asset_id}:
    get:
      operationId: getAsset
      tags: [Assets]
      summary: Get one Workspace Asset
      parameters:
        - $ref: "#/components/parameters/AssetId"
      responses:
        "200":
          description: Asset
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Asset"
        "404":
          $ref: "#/components/responses/NotFound"
  /assets/{asset_id}/complete:
    post:
      operationId: completeAssetUpload
      tags: [Assets]
      summary: Verify the uploaded R2 object and mark the Asset ready
      parameters:
        - $ref: "#/components/parameters/AssetId"
      responses:
        "200":
          description: Ready Asset
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Asset"
        "404":
          $ref: "#/components/responses/NotFound"
  /assets/{asset_id}/download:
    get:
      operationId: createAssetDownload
      tags: [Assets]
      summary: Create a short-lived direct download URL
      parameters:
        - $ref: "#/components/parameters/AssetId"
      responses:
        "200":
          description: Presigned download URL
          content:
            application/json:
              schema:
                type: object
                required: [url, expires_in]
                properties:
                  url: {type: string, format: uri}
                  expires_in: {type: integer, example: 900}
        "422":
          $ref: "#/components/responses/ValidationError"
  /webhook_event_types:
    get:
      operationId: listWebhookEventTypes
      tags: [Webhooks]
      summary: List event types available for webhook subscriptions
      responses:
        "200":
          description: Webhook event type catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEventTypeList"
  /webhook_endpoints:
    get:
      operationId: listWebhookEndpoints
      tags: [Webhooks]
      summary: List Workspace webhook endpoints
      responses:
        "200":
          description: Webhook endpoint list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpointList"
    post:
      operationId: createWebhookEndpoint
      tags: [Webhooks]
      summary: Register an HTTPS webhook destination
      description: The signing secret is returned only by this response and secret rotation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookEndpoint"
      responses:
        "201":
          description: Webhook endpoint with its initial signing secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpointWithSecret"
        "422":
          $ref: "#/components/responses/ValidationError"
  /webhook_endpoints/{webhook_endpoint_id}:
    get:
      operationId: getWebhookEndpoint
      tags: [Webhooks]
      summary: Get one Workspace webhook endpoint
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      responses:
        "200":
          description: Webhook endpoint
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      operationId: updateWebhookEndpoint
      tags: [Webhooks]
      summary: Update a webhook URL, event selection, description, or status
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWebhookEndpoint"
      responses:
        "200":
          description: Updated webhook endpoint
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      operationId: deleteWebhookEndpoint
      tags: [Webhooks]
      summary: Delete a webhook endpoint
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      responses:
        "204":
          description: Webhook endpoint deleted
        "404":
          $ref: "#/components/responses/NotFound"
  /webhook_endpoints/{webhook_endpoint_id}/rotate_secret:
    post:
      operationId: rotateWebhookEndpointSecret
      tags: [Webhooks]
      summary: Replace a webhook endpoint signing secret
      description: The old secret stops signing new deliveries immediately.
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      responses:
        "200":
          description: Webhook endpoint with its new signing secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpointWithSecret"
        "404":
          $ref: "#/components/responses/NotFound"
  /usage:
    get:
      operationId: listUsage
      tags: [Usage]
      summary: Get Workspace usage and the shared Account balance
      parameters:
        - $ref: "#/components/parameters/TenantFilter"
      responses:
        "200":
          description: Usage summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageSummary"
  /balance_transactions:
    get:
      operationId: listBalanceTransactions
      tags: [Usage]
      summary: List ledger entries attributed to this Workspace
      responses:
        "200":
          description: Workspace ledger list
          content:
            application/json:
              schema:
                type: object
                required: [object, data]
                properties:
                  object: {const: list}
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/BalanceTransaction"
components:
  securitySchemes:
    workspaceKey:
      type: http
      scheme: bearer
      bearerFormat: TessaRun Workspace API key
  parameters:
    TenantFilter:
      name: tenant
      in: query
      required: false
      description: Indexed customer-defined string scoped to the authenticated Workspace.
      schema: {type: string, maxLength: 255}
    ModelId:
      name: model_id
      in: path
      required: true
      schema: {type: string}
    GenerationId:
      name: generation_id
      in: path
      required: true
      schema: {type: string, format: uuid}
    AssetId:
      name: asset_id
      in: path
      required: true
      schema: {type: string, format: uuid}
    WebhookEndpointId:
      name: webhook_endpoint_id
      in: path
      required: true
      schema: {type: string, format: uuid}
  responses:
    Unauthorized:
      description: Missing or invalid Workspace API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    NotFound:
      description: Not found in the authenticated Workspace
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    ValidationError:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
  schemas:
    Money:
      type: object
      required: [value, currency, unit]
      properties:
        value: {type: integer, description: Nominal USD cents}
        currency: {const: usd}
        unit: {const: cent}
    Model:
      type: object
      required: [id, name, type, capabilities, availability, parameters]
      properties:
        id: {type: string}
        name: {type: string}
        maker: {type: string}
        type: {type: string, enum: [image, video, audio, file]}
        capabilities:
          type: array
          items: {type: string}
        providers:
          type: array
          items: {type: string}
        default_provider: {type: [string, "null"]}
        availability: {type: string, enum: [preview, inventory]}
        parameters: {type: object, additionalProperties: true}
    ModelList:
      type: object
      required: [object, data]
      properties:
        object: {const: list}
        data:
          type: array
          items: {$ref: "#/components/schemas/Model"}
    CreateGeneration:
      type: object
      required: [model, params]
      properties:
        model: {type: string}
        tenant: {type: string, maxLength: 255, description: Optional indexed downstream attribution string}
        params:
          type: object
          additionalProperties: true
          description: Model-specific parameters. Media inputs use the MediaInput shape.
        metadata:
          type: object
          additionalProperties: true
          description: Optional customer-defined identifiers and context stored and returned unchanged. At most 32,000 encoded bytes.
    MediaInput:
      description: One ready Workspace Asset reference or one temporary HTTPS URL.
      oneOf:
        - type: object
          required: [asset_id]
          additionalProperties: false
          properties:
            asset_id: {type: string, format: uuid}
        - type: object
          required: [url]
          additionalProperties: false
          properties:
            url: {type: string, format: uri, maxLength: 4096}
    Generation:
      type: object
      required: [id, object, status, model, params, metadata, price, assets, created_at]
      properties:
        id: {type: string, format: uuid}
        object: {const: generation}
        status: {type: string, enum: [pending, submitted, running, succeeded, failed]}
        model: {type: string}
        tenant: {type: [string, "null"]}
        params: {type: object, additionalProperties: true}
        metadata:
          type: object
          additionalProperties: true
          description: Customer-defined identifiers and context returned unchanged.
        price:
          type: object
          required: [amount, currency, unit]
          properties:
            amount: {type: integer}
            currency: {const: usd}
            unit: {const: cent}
        provider: {type: [string, "null"]}
        error: {type: [object, "null"], additionalProperties: true}
        assets:
          type: array
          items: {$ref: "#/components/schemas/Asset"}
        accepted_at: {type: [string, "null"], format: date-time}
        started_at: {type: [string, "null"], format: date-time}
        finished_at: {type: [string, "null"], format: date-time}
        created_at: {type: string, format: date-time}
    GenerationList:
      type: object
      required: [object, data]
      properties:
        object: {const: list}
        data:
          type: array
          items: {$ref: "#/components/schemas/Generation"}
    CreateAsset:
      type: object
      required: [media_kind, filename]
      properties:
        media_kind: {type: string, enum: [image, video, audio, file]}
        filename: {type: string}
        mime_type: {type: string}
        tenant: {type: string, maxLength: 255}
        metadata:
          type: object
          additionalProperties: true
          description: Optional customer-defined identifiers and context stored and returned unchanged. At most 32,000 encoded bytes.
    Asset:
      type: object
      required: [id, object, status, media_kind, metadata, created_at]
      properties:
        id: {type: string, format: uuid}
        object: {const: asset}
        status: {type: string, enum: [pending_upload, ready, failed, archived]}
        tenant: {type: [string, "null"]}
        media_kind: {type: string, enum: [image, video, audio, file]}
        mime_type: {type: [string, "null"]}
        filename: {type: [string, "null"]}
        size_bytes: {type: [integer, "null"]}
        width: {type: [integer, "null"]}
        height: {type: [integer, "null"]}
        duration_ms: {type: [integer, "null"]}
        metadata: {type: object, additionalProperties: true}
        created_at: {type: string, format: date-time}
    AssetUpload:
      type: object
      required: [asset, upload]
      properties:
        asset: {$ref: "#/components/schemas/Asset"}
        upload:
          type: object
          required: [method, url, expires_in]
          properties:
            method: {const: PUT}
            url: {type: string, format: uri}
            expires_in: {type: integer}
    AssetList:
      type: object
      required: [object, data]
      properties:
        object: {const: list}
        data:
          type: array
          items: {$ref: "#/components/schemas/Asset"}
    WebhookEvent:
      description: >-
        JSON body sent to a registered webhook endpoint. Verify the signature
        against the exact raw body before decoding JSON.
      type: object
      required: [id, object, type, data, metadata, occurred_at]
      properties:
        id: {type: string, format: uuid}
        object: {const: event}
        type: {type: string}
        operation_id: {type: [string, "null"], format: uuid}
        tenant: {type: [string, "null"]}
        data: {type: object, additionalProperties: true}
        metadata: {type: object, additionalProperties: true}
        occurred_at: {type: string, format: date-time}
    WebhookEventType:
      type: object
      required: [type, description]
      properties:
        type:
          type: string
          enum:
            - generation.accepted
            - generation.submitted
            - generation.running
            - generation.succeeded
            - generation.failed
        description: {type: string}
    WebhookEventTypeList:
      type: object
      required: [object, data]
      properties:
        object: {const: list}
        data:
          type: array
          items: {$ref: "#/components/schemas/WebhookEventType"}
    WebhookEndpointFields:
      type: object
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
          description: Public HTTPS destination. Private and loopback addresses are rejected.
        description: {type: [string, "null"], maxLength: 500}
        event_types:
          type: array
          uniqueItems: true
          description: Empty means all available event types.
          items:
            type: string
            enum:
              - generation.accepted
              - generation.submitted
              - generation.running
              - generation.succeeded
              - generation.failed
        status: {type: string, enum: [active, disabled]}
    CreateWebhookEndpoint:
      allOf:
        - $ref: "#/components/schemas/WebhookEndpointFields"
        - type: object
          required: [url]
    UpdateWebhookEndpoint:
      allOf:
        - $ref: "#/components/schemas/WebhookEndpointFields"
    WebhookEndpoint:
      allOf:
        - $ref: "#/components/schemas/WebhookEndpointFields"
        - type: object
          required: [id, object, url, event_types, status, created_at, updated_at]
          properties:
            id: {type: string, format: uuid}
            object: {const: webhook_endpoint}
            created_at: {type: string, format: date-time}
            updated_at: {type: string, format: date-time}
    WebhookEndpointWithSecret:
      allOf:
        - $ref: "#/components/schemas/WebhookEndpoint"
        - type: object
          required: [signing_secret]
          properties:
            signing_secret:
              type: string
              pattern: "^whsec_"
              description: Store securely. Returned only at creation and rotation.
    WebhookEndpointList:
      type: object
      required: [object, data]
      properties:
        object: {const: list}
        data:
          type: array
          items: {$ref: "#/components/schemas/WebhookEndpoint"}
    UsageRecord:
      type: object
      required: [id, object, category, quantity, unit, amount, metadata, occurred_at]
      properties:
        id: {type: string, format: uuid}
        object: {const: usage_record}
        category: {type: string, enum: [generation, storage, request]}
        operation_id: {type: [string, "null"], format: uuid}
        tenant: {type: [string, "null"]}
        quantity: {type: number}
        unit: {type: string}
        amount: {$ref: "#/components/schemas/Money"}
        metadata: {type: object, additionalProperties: true}
        occurred_at: {type: string, format: date-time}
    UsageSummary:
      type: object
      required: [object, prepaid_balance, data]
      properties:
        object: {const: usage_summary}
        prepaid_balance: {$ref: "#/components/schemas/Money"}
        data:
          type: array
          items: {$ref: "#/components/schemas/UsageRecord"}
    BalanceTransaction:
      type: object
      required: [id, object, type, amount, balance_after, metadata, occurred_at]
      properties:
        id: {type: string, format: uuid}
        object: {const: balance_transaction}
        type: {type: string, enum: [topup, charge, refund, adjustment]}
        operation_id: {type: [string, "null"], format: uuid}
        amount: {$ref: "#/components/schemas/Money"}
        balance_after: {$ref: "#/components/schemas/Money"}
        description: {type: [string, "null"]}
        metadata: {type: object, additionalProperties: true}
        occurred_at: {type: string, format: date-time}
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type]
          properties:
            type: {type: string}
            message: {type: string}
            details: {}
