openapi: 3.1.0
info:
  title: Xavira AI API
  version: "1.0.0"
  summary: NSFW image + video generation with cached FaceID identity.
  description: |
    Xavira AI exposes a small, plug-and-play REST surface for NSFW image and
    video generation. The signature primitive is a **persistent character**:
    create once via `POST /v1/characters`, then drop it into any prompt via
    `POST /v1/images:generate` and the face stays consistent across gens.

    See [xavira.ai/docs](https://xavira.ai/docs) for the full reference.
  contact:
    name: Xavira AI Support
    email: business@sria-digital.com
    url: https://xavira.ai/docs
  termsOfService: https://xavira.ai/terms
  license:
    name: Proprietary

servers:
  - url: https://api.xavira.ai
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Characters
    description: Persistent identity primitives. Created once, reused across gens.
  - name: Images
    description: Synchronous image generation.
  - name: Videos
    description: Asynchronous video generation (5s clips via webhook + poll).
  - name: Generations
    description: Status polling and history.

paths:
  /v1/characters:
    post:
      tags: [Characters]
      summary: Create a persistent character
      description: |
        Two input modes: traits (closed enums) or reference_image_url. Returns
        a `character_id` you reuse in `/v1/images:generate` and
        `/v1/videos:generate`.
      operationId: createCharacter
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/CreateCharacterTraits"
                - $ref: "#/components/schemas/CreateCharacterReference"
            examples:
              traits:
                summary: Traits mode
                value:
                  model_id: realistic-sharp-v1
                  traits:
                    gender: female
                    ethnicity: east-asian
                    ageRange: 23-29
                    hairLength: long
                    hairColor: black
                    build: slim
                    breastSize: medium
                  scene: "blue eyes, light freckles, soft window light"
                  name: Sora
              reference:
                summary: Reference image mode
                value:
                  model_id: realistic-sharp-v1
                  reference_image_url: https://example.com/avatar.jpg
                  name: Imported character
      responses:
        "201":
          description: Character created.
          headers:
            X-RateLimit-Limit:    { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset:    { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CharacterCreated" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/ModerationBlocked" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /v1/images:generate:
    post:
      tags: [Images]
      summary: Generate an image against an existing character
      description: |
        Synchronous. Typical warm-worker response in 5-7s. Cost: 1 credit.
        FaceID embedding cached in worker volume on first call, ~700ms hit on
        subsequent calls for the same character.
      operationId: generateImage
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string, maxLength: 100 }
          required: false
          description: Repeat with the same key returns the prior response (no duplicate charge, no duplicate gen).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/GenerateImageBody" }
            examples:
              minimal:
                summary: Minimal call
                value:
                  character_id: 151fccbd-4667-43fc-9cc0-fe2be6b7d1b8
                  prompt: "sitting in a sunlit kitchen, casual sweater, candid photographic portrait"
              with_seed:
                summary: With seed + negative
                value:
                  character_id: 151fccbd-4667-43fc-9cc0-fe2be6b7d1b8
                  prompt: "moody low-key studio lighting, leather jacket, three-quarter view"
                  negative_prompt_append: "warm tones, daylight"
                  seed: 12345
      responses:
        "201":
          description: Image generated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GenerationCompleted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "404": { $ref: "#/components/responses/CharacterNotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/ModerationBlocked" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /v1/videos:generate:
    post:
      tags: [Videos]
      summary: Generate an I2V video from a character avatar (async)
      description: |
        Returns 202 immediately with a `generation_id`. Customer polls
        `GET /v1/generations/{id}` or registers a `callback_url` to receive
        an HMAC-signed webhook on completion. Cost: 5 credits.
      operationId: generateVideo
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string, maxLength: 100 }
          required: false
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/GenerateVideoBody" }
            examples:
              with_webhook:
                summary: With webhook callback
                value:
                  character_id: 151fccbd-4667-43fc-9cc0-fe2be6b7d1b8
                  prompt: "slow head turn, gentle smile spreading, hair flowing"
                  callback_url: https://api.yourapp.com/xavira-webhook
      responses:
        "202":
          description: Video submitted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GenerationSubmitted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "404": { $ref: "#/components/responses/CharacterNotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/ModerationBlocked" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /v1/generations/{id}:
    get:
      tags: [Generations]
      summary: Get a generation's status + output
      description: |
        Lazy completion: if status is `pending` we check RunPod and mutate the
        DB on terminal state; idempotent. Safe to poll concurrently.
      operationId: getGeneration
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/GenerationCompleted"
                  - $ref: "#/components/schemas/GenerationSubmitted"
                  - $ref: "#/components/schemas/GenerationFailed"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404":
          description: Generation not found.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /v1/status:
    get:
      tags: [Status]
      summary: System status (public, no auth)
      operationId: getStatus
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StatusReport" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key in the format `xav_live_<key>`. Create one at https://xavira.ai/profile.

  headers:
    RateLimitLimit:
      schema: { type: integer }
      description: Per-minute cap for this endpoint, this API key.
    RateLimitRemaining:
      schema: { type: integer }
      description: Calls left in the current minute window.
    RateLimitReset:
      schema: { type: integer }
      description: Epoch seconds when the window resets.
    RetryAfter:
      schema: { type: integer }
      description: Seconds to wait before retrying (only on 429).

  schemas:
    CreateCharacterTraits:
      type: object
      required: [model_id, traits]
      properties:
        model_id:
          type: string
          enum: [realistic-sharp-v1, anime-v1]
        traits:
          $ref: "#/components/schemas/Traits"
        scene:
          type: string
          maxLength: 1000
          description: Free-text scene/outfit/pose/lighting. See scene vocabulary in the docs.
        name:
          type: string
          maxLength: 100
        metadata:
          type: object
          additionalProperties: true
    CreateCharacterReference:
      type: object
      required: [model_id, reference_image_url]
      properties:
        model_id:
          type: string
          enum: [realistic-sharp-v1, anime-v1]
        reference_image_url:
          type: string
          format: uri
          description: HTTPS URL, content-type image/*, max 20MB.
        name:
          type: string
          maxLength: 100
        metadata:
          type: object
          additionalProperties: true
    Traits:
      type: object
      required: [gender, ethnicity, ageRange, hairLength, hairColor, build]
      properties:
        gender:
          type: string
          enum: [female, male]
        ethnicity:
          type: string
          enum: [white, black, hispanic, middle-eastern, indian, east-asian, south-east-asian]
        ageRange:
          type: string
          enum: ["18-22", "23-29", "30-39", "40-plus"]
        hairLength:
          type: string
          enum: [short, medium, long]
        hairColor:
          type: string
          enum: [black, brown, blonde, red, auburn, grey, white]
        build:
          type: string
          enum: [petite, slim, athletic, curvy, voluptuous]
        breastSize:
          type: string
          enum: [small, medium, large]
          description: Only applied when gender=female.
    CharacterCreated:
      type: object
      properties:
        character_id: { type: string, format: uuid }
        avatar_url:   { type: string, format: uri }
        face_cache_key: { type: string }

    GenerateImageBody:
      type: object
      required: [character_id, prompt]
      properties:
        character_id: { type: string, format: uuid }
        prompt: { type: string, maxLength: 2000 }
        negative_prompt_append: { type: string, maxLength: 1000 }
        seed: { type: integer, minimum: 0 }

    GenerateVideoBody:
      type: object
      required: [character_id, prompt]
      properties:
        character_id: { type: string, format: uuid }
        prompt: { type: string, maxLength: 2000 }
        negative_prompt_append: { type: string, maxLength: 1000 }
        seed: { type: integer, minimum: 0 }
        callback_url: { type: string, format: uri, description: HTTPS URL we POST the completion webhook to. }

    GenerationCompleted:
      type: object
      properties:
        generation_id: { type: string, format: uuid }
        character_id: { type: string, format: uuid, nullable: true }
        kind: { type: string, enum: [image, video] }
        status: { type: string, enum: [completed] }
        output_url: { type: string, format: uri }
        gen_time_ms: { type: integer }
        wall_time_ms: { type: integer }
        cost_credits: { type: integer }

    GenerationSubmitted:
      type: object
      properties:
        generation_id: { type: string, format: uuid }
        kind: { type: string, enum: [image, video] }
        status: { type: string, enum: [pending, processing] }
        cost_credits: { type: integer }
        poll_url: { type: string }

    GenerationFailed:
      type: object
      properties:
        generation_id: { type: string, format: uuid }
        status: { type: string, enum: [failed, cancelled, timed_out] }
        error_message: { type: string }
        cost_credits: { type: integer, description: "0; credits are refunded on terminal failure." }

    StatusReport:
      type: object
      properties:
        overall: { type: string, enum: [operational, degraded, outage] }
        checked_at: { type: string, format: date-time }
        components:
          type: array
          items:
            type: object
            properties:
              id:    { type: string }
              label: { type: string }
              status: { type: string, enum: [operational, degraded, outage] }
              latency_ms: { type: integer, nullable: true }
              note: { type: string, nullable: true }
        recent_window:
          type: object
          properties:
            hours:           { type: integer }
            total_completed: { type: integer }
            total_failed:    { type: integer }
            success_rate:    { type: number, nullable: true, minimum: 0, maximum: 1 }

    Error:
      type: object
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:    { type: string }
            message: { type: string }
            details: { type: object, additionalProperties: true }

  responses:
    BadRequest:
      description: Validation failure.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PaymentRequired:
      description: Insufficient credits.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    CharacterNotFound:
      description: Unknown character_id, or belongs to another customer.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    IdempotencyConflict:
      description: Idempotency-Key reused while prior request still pending.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ModerationBlocked:
      description: Prompt or output blocked by moderation. No credit charged.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Per-key per-minute cap exceeded.
      headers:
        X-RateLimit-Limit:     { $ref: "#/components/headers/RateLimitLimit" }
        X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
        X-RateLimit-Reset:     { $ref: "#/components/headers/RateLimitReset" }
        Retry-After:           { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    InternalError:
      description: Internal error. No credit charged. Safe to retry.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    UpstreamError:
      description: Upstream dependency error (RunPod, R2, OpenRouter). No credit charged.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
