openapi: 3.1.0
info:
  title: SceneVela Developer API
  version: 2026-07-31.1
  description: |
    Server-side generation and webhook API. Bearer keys must never be embedded
    in browsers or mobile applications. The `/keys` and `/webhooks` operations
    are same-origin browser-management routes used by the Developer settings UI.
servers:
  - url: /api/v1/developer
tags:
  - { name: Generation }
  - { name: Webhook API }
  - { name: Browser key management }
  - { name: Browser webhook management }
paths:
  /quotes:
    post:
      tags: [Generation]
      summary: Create a trusted generation quote
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/QuoteRequest" }
      responses:
        "200":
          description: Quote created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QuoteResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /generations:
    get:
      tags: [Generation]
      summary: List generations owned by this API key
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Stable keyset page
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/GenerationPage" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      tags: [Generation]
      summary: Submit a generation
      security: [{ bearerAuth: [] }]
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string, minLength: 8, maxLength: 120 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/GenerationRequest" }
      responses:
        "201":
          description: Created or idempotently returned
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GenerationResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402":
          description: Insufficient credits
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /generations/{id}:
    get:
      tags: [Generation]
      summary: Retrieve one API-key-owned generation
      description: Successful jobs contain freshly signed, short-lived private output URLs.
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/GenerationId"
      responses:
        "200":
          description: Generation
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GenerationResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /endpoints:
    get:
      tags: [Webhook API]
      summary: List endpoints in the Bearer key project scope
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: Endpoint list without signing secrets
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookEndpoint" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      tags: [Webhook API]
      summary: Create an endpoint
      description: The signing secret is returned once.
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BearerWebhookCreate" }
      responses:
        "201":
          description: Endpoint created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookSecretResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /keys:
    get:
      tags: [Browser key management]
      summary: List workspace API keys
      description: Same-origin browser session; owner/admin only.
      security: [{ cookieAuth: [] }]
      responses:
        "200":
          description: Key metadata without secrets
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ApiKeyMetadata" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Browser key management]
      summary: Create an API key
      description: Same-origin browser session; plaintext key is returned once.
      security: [{ cookieAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApiKeyCreate" }
      responses:
        "201":
          description: Key created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiKeySecretResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /keys/{id}:
    delete:
      tags: [Browser key management]
      summary: Revoke an API key
      security: [{ cookieAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200": { description: Revoked }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /keys/{id}/rotate:
    post:
      tags: [Browser key management]
      summary: Rotate an API key
      description: Revokes the old key and returns the replacement plaintext once.
      security: [{ cookieAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Rotated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiKeySecretResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /webhooks:
    get:
      tags: [Browser webhook management]
      summary: List workspace webhook endpoints
      description: Same-origin browser session; owner/admin only.
      security: [{ cookieAuth: [] }]
      responses:
        "200":
          description: Endpoint metadata
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookEndpoint" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Browser webhook management]
      summary: Create a webhook endpoint
      description: Same-origin browser session; signing secret is returned once.
      security: [{ cookieAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BrowserWebhookCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookSecretResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /webhooks/{id}:
    delete:
      tags: [Browser webhook management]
      summary: Disable an endpoint and terminate pending or leased deliveries
      security: [{ cookieAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200": { description: Disabled }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /webhooks/{id}/rotate:
    post:
      tags: [Browser webhook management]
      summary: Rotate an endpoint signing secret
      description: Returns the new secret once. Active delivery leases produce 409.
      security: [{ cookieAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Rotated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookSecretResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sv_live
    cookieAuth:
      type: apiKey
      in: cookie
      name: sb-access-token
      description: SceneVela same-origin authenticated browser session.
  parameters:
    Limit:
      in: query
      name: limit
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    Cursor:
      in: query
      name: cursor
      schema: { type: string, maxLength: 512 }
      description: Opaque cursor returned as `nextCursor`.
    GenerationId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
    ResourceId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
  headers:
    RetryAfter:
      description: Seconds until the current rate-limit window may be retried.
      schema: { type: integer, minimum: 1 }
    WebhookEventId:
      description: Stable event UUID, unchanged across retries.
      schema: { type: string, format: uuid }
    WebhookTimestamp:
      description: Unix timestamp used in the signature input.
      schema: { type: integer }
    WebhookSignature:
      description: "`v1=<hex HMAC-SHA256(secret, timestamp + '.' + raw_body)>`"
      schema: { type: string, pattern: "^v1=[a-f0-9]{64}$" }
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing, invalid, expired or revoked credential
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Missing scope, role, workspace or project access
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found in credential scope
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: Idempotency, state or active-delivery conflict
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unavailable:
      description: Dependency unavailable; request failed closed
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string, examples: [INVALID_API_KEY] }
        errorId: { type: string, format: uuid }
    QuoteRequest:
      type: object
      additionalProperties: false
      required: [kind, mode, modelId, aspectRatio, resolution]
      properties:
        kind: { enum: [video, image] }
        mode: { type: string }
        modelId: { type: string }
        aspectRatio: { type: string }
        resolution: { type: string, examples: [1K] }
        duration: { type: [integer, "null"] }
        audio: { type: boolean }
        outputCount: { type: integer, minimum: 1 }
    Quote:
      type: object
      required: [credits, expiresAt]
      properties:
        credits: { type: integer, minimum: 0 }
        expiresAt: { type: string, format: date-time }
    QuoteResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [quote, quoteToken]
          properties:
            quote: { $ref: "#/components/schemas/Quote" }
            quoteToken: { type: string }
    GenerationRequest:
      type: object
      additionalProperties: false
      required: [quoteToken, prompt]
      properties:
        quoteToken: { type: string }
        prompt: { type: string, minLength: 1, maxLength: 2000 }
        inputAssetId: { type: [string, "null"], format: uuid }
    Generation:
      type: object
      required: [id, kind, mode, status, progress, creditsQuoted, createdAt]
      properties:
        id: { type: string, format: uuid }
        kind: { type: string }
        mode: { type: string }
        modelId: { type: string }
        status: { enum: [reserved, queued, processing, succeeded, failed, canceled, refunded] }
        progress: { type: integer, minimum: 0, maximum: 100 }
        creditsQuoted: { type: integer }
        creditsCharged: { type: integer }
        outputUrls:
          type: array
          items: { type: string, format: uri }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    GenerationResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/Generation" }
    GenerationPage:
      type: object
      required: [items, nextCursor]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/Generation" }
        nextCursor: { type: [string, "null"] }
    ApiKeyCreate:
      type: object
      additionalProperties: false
      required: [name, scopes]
      allOf:
        - if:
            properties:
              scopes:
                contains:
                  enum: [generation:create, generation:read]
          then:
            required: [projectId]
            properties:
              projectId: { type: string, format: uuid }
      properties:
        name: { type: string, minLength: 1, maxLength: 80 }
        scopes:
          type: array
          minItems: 1
          uniqueItems: true
          items: { enum: [generation:create, generation:read, webhook:manage] }
        projectId:
          type: [string, "null"]
          format: uuid
          description: Required for generation:create or generation:read; omitted/null only for webhook-only keys.
        expiresAt: { type: [string, "null"], format: date-time }
    ApiKeyMetadata:
      type: object
      required: [id, name, public_prefix, scopes, status, created_at]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        public_prefix: { type: string }
        project_id: { type: [string, "null"], format: uuid }
        scopes:
          type: array
          items: { type: string }
        status: { enum: [active, revoked, rotated] }
        expires_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
    ApiKeySecretResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [id, key]
          properties:
            id: { type: string, format: uuid }
            key: { type: string, pattern: "^sv_live_[A-Za-z0-9_-]{16}_[A-Za-z0-9_-]{43}$" }
    WebhookEventType:
      enum: [generation.updated, generation.succeeded, generation.failed]
    BearerWebhookCreate:
      type: object
      additionalProperties: false
      required: [url, eventTypes]
      properties:
        url: { type: string, format: uri, maxLength: 2048 }
        eventTypes:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items: { $ref: "#/components/schemas/WebhookEventType" }
    BrowserWebhookCreate:
      allOf:
        - $ref: "#/components/schemas/BearerWebhookCreate"
        - type: object
          properties:
            projectId: { type: [string, "null"], format: uuid }
    WebhookEndpoint:
      type: object
      required: [id, url, event_types, status, failure_count, created_at]
      properties:
        id: { type: string, format: uuid }
        project_id: { type: [string, "null"], format: uuid }
        url: { type: string, format: uri }
        event_types:
          type: array
          items: { $ref: "#/components/schemas/WebhookEventType" }
        status: { enum: [active, disabled] }
        failure_count: { type: integer, minimum: 0 }
        created_at: { type: string, format: date-time }
    WebhookSecretResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [secret]
          properties:
            id: { type: string, format: uuid }
            secret: { type: string, pattern: "^whsec_[A-Za-z0-9_-]{43}$" }
    WebhookDeliveryHeaders:
      type: object
      description: Headers sent with each signed webhook delivery.
      properties:
        X-SceneVela-Event-Id:
          type: string
          format: uuid
          description: Stable event UUID, unchanged across retries.
        X-SceneVela-Timestamp:
          type: integer
          description: Unix timestamp used in the signature input.
        X-SceneVela-Signature:
          type: string
          pattern: "^v1=[a-f0-9]{64}$"
          description: HMAC-SHA256 over `timestamp + "." + raw_body`.
