openapi: 3.1.0
info:
  title: ChildrenBooks Public API
  version: "1.0.0"
  summary: Generate AI-illustrated children's storybooks programmatically.
  description: |
    The ChildrenBooks API lets Pro and Premium subscribers of
    [childrenbooks.online](https://www.childrenbooks.online) create storybooks
    from any backend, SDK, or tool such as Hugging Face Spaces or a CLI.

    **Usage model**
    - Create an API key from the [Dashboard](https://www.childrenbooks.online/dashboard).
      The key is shown once — copy it immediately.
    - Monthly generation quota is shared with the Dashboard (Autumn billing).
    - A daily cap and per-minute rate limit prevent runaway usage if the key
      leaks.
    - Jobs are asynchronous: `POST /v1/stories` returns immediately with a
      `taskId` + `bookId`; poll `GET /v1/stories/{id}` for progress.

    **Breaking change policy**
    The `/v1` prefix is stable. If a field's semantics change we'll publish a
    `/v2` endpoint; `/v1` fields will not be removed without a 12-month
    deprecation window announced in the changelog.

    **Pricing**
    Each page (including the cover) consumes 1 credit from your monthly
    quota. A 10-page book ≈ 10 credits.
  contact:
    name: ChildrenBooks Support
    url: https://www.childrenbooks.online/contact
    email: support@childrenbooks.online
  license:
    name: Proprietary
    url: https://www.childrenbooks.online/terms
servers:
  - url: https://api.childrenbooks.online
    description: Production
  - url: http://localhost:3100
    description: Local dev (apps/api)
security:
  - ApiKeyHeader: []
  - ApiKeyBearer: []
tags:
  - name: Stories
    description: Create and retrieve storybooks.
  - name: System
    description: Health and metadata endpoints.
paths:
  /v1/health:
    get:
      tags: [System]
      summary: Liveness probe
      description: Returns `ok` if the API is accepting traffic. No auth required.
      security: []
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthEnvelope"

  /v1/stories:
    post:
      tags: [Stories]
      summary: Create a storybook generation task
      description: |
        Starts an asynchronous generation job. The response is returned as
        soon as the job is queued (typically <300ms). Polling is done via
        `GET /v1/stories/{id}` using the returned `bookId`.

        **Rate limits**
        | Plan    | Per-minute | Per-day |
        | ------- | ---------- | ------- |
        | Pro     | 60         | 50      |
        | Premium | 200        | 200     |

        Responses include `x-ratelimit-remaining` and `x-ratelimit-reset`.
      operationId: createStory
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateStoryRequest"
            examples:
              watercolor_7_en:
                summary: A 7-page watercolor adventure (English)
                value:
                  storyIdea: "A little fox who learns to share her carrots with forest friends."
                  ageRange: "4-7"
                  style: "watercolor"
                  pageCount: 7
                  tone: "heartwarming"
                  language: "english"
      responses:
        "202":
          description: Task accepted.
          headers:
            x-ratelimit-limit:
              schema: { type: integer }
            x-ratelimit-remaining:
              schema: { type: integer }
            x-ratelimit-reset:
              schema: { type: integer, description: "Seconds until the window resets" }
            x-request-id:
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoryAcceptedEnvelope"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServerError" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /v1/stories/{id}:
    get:
      tags: [Stories]
      summary: Get storybook status + pages
      description: |
        Returns the current generation status, any pages already rendered, and
        metadata. Visibility is scoped to the authenticated user (API keys
        only see books owned by their user_id).
      operationId: getStory
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Book found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoryEnvelope"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/ServerError" }

components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        Primary header for API key auth. Value format: `cbk_live_<base62>`.
    ApiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: cbk_live
      description: |
        Alternative for SDKs that default to Bearer tokens. The token MUST
        start with `cbk_live_` otherwise the server returns 401.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Optional client-generated UUID. If the same key is reused within 24h
        the original task is returned instead of creating a duplicate.
      schema: { type: string, maxLength: 128 }
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      description: |
        If set, the server echoes this id back and includes it in all logs
        for easier debugging. Otherwise the server generates one.
      schema: { type: string, maxLength: 64 }

  schemas:
    HealthEnvelope:
      type: object
      required: [ok, data]
      properties:
        ok: { const: true }
        requestId: { type: string }
        data:
          type: object
          required: [status, stage]
          properties:
            status: { const: "ok" }
            stage: { type: string, enum: [development, preview, production] }
            version: { type: string }
            now: { type: string, format: date-time }

    CreateStoryRequest:
      type: object
      required: [storyIdea, ageRange, style, pageCount, tone, language]
      properties:
        storyIdea:
          type: string
          minLength: 10
          maxLength: 2000
          description: Plain-language prompt describing the story to generate.
        ageRange:
          type: string
          enum: ["0-3", "4-7", "8-12"]
        style:
          type: string
          enum: [watercolor, oil_painting, "3d", comic_style, crayon, colored_pencil, paper_cut]
        pageCount:
          type: integer
          minimum: 5
          maximum: 15
          description: Total pages including cover. Consumes 1 credit per page.
        tone:
          type: string
          enum: [playful_funny, fantasy_magic, animal, heartwarming, sci_fi, daily_life_basics, adventurous, educational]
        language:
          type: string
          enum: [english, spanish, french, german, chinese, japanese, korean, russian, portuguese, italian]

    StoryAccepted:
      type: object
      required: [taskId, bookId, status]
      properties:
        taskId: { type: string, format: uuid }
        bookId: { type: string, format: uuid }
        status:
          type: string
          enum: [pending, processing, completed, failed]

    StoryAcceptedEnvelope:
      type: object
      required: [ok, data]
      properties:
        ok: { const: true }
        requestId: { type: string }
        data: { $ref: "#/components/schemas/StoryAccepted" }

    StoryPage:
      type: object
      required: [index]
      properties:
        index: { type: integer }
        title: { type: ["string", "null"] }
        content: { type: ["string", "null"] }
        imageUrl: { type: ["string", "null"] }

    Story:
      type: object
      required: [id, status, pages]
      properties:
        id: { type: string, format: uuid }
        title: { type: ["string", "null"] }
        slug: { type: ["string", "null"] }
        coverImage: { type: ["string", "null"] }
        summary: { type: ["string", "null"] }
        storyIdea: { type: ["string", "null"] }
        ageRange: { type: ["string", "null"] }
        style: { type: ["string", "null"] }
        pageCount: { type: ["integer", "null"] }
        tone: { type: ["string", "null"] }
        language: { type: ["string", "null"] }
        source: { type: ["string", "null"], enum: [web, api, hf, null] }
        storageProvider: { type: ["string", "null"] }
        createdAt: { type: ["string", "null"], format: date-time }
        updatedAt: { type: ["string", "null"], format: date-time }
        status: { type: string, enum: [pending, processing, completed, failed, unknown] }
        progress: { type: integer, minimum: 0, maximum: 100 }
        errorMessage: { type: ["string", "null"] }
        completedAt: { type: ["string", "null"], format: date-time }
        pages:
          type: array
          items: { $ref: "#/components/schemas/StoryPage" }

    StoryEnvelope:
      type: object
      required: [ok, data]
      properties:
        ok: { const: true }
        requestId: { type: string }
        data: { $ref: "#/components/schemas/Story" }

    ApiError:
      type: object
      required: [ok, error]
      properties:
        ok: { const: false }
        requestId: { type: string }
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - UNAUTHORIZED
                - INVALID_API_KEY
                - PLAN_REQUIRED
                - FORBIDDEN
                - VALIDATION_ERROR
                - NOT_FOUND
                - ALREADY_EXISTS
                - RATE_LIMIT_EXCEEDED
                - DAILY_CAP_EXCEEDED
                - INSUFFICIENT_CREDITS
                - EXTERNAL_SERVICE_ERROR
                - INTERNAL_ERROR
                - BAD_REQUEST
                - METHOD_NOT_ALLOWED
            message: { type: string }
            fields:
              type: object
              additionalProperties: { type: string }
            meta:
              type: object
              additionalProperties: true

  responses:
    BadRequest:
      description: Malformed body or invalid arguments.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    Unauthorized:
      description: Missing / malformed / unknown API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    PaymentRequired:
      description: Monthly credit balance exhausted.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    Forbidden:
      description: Key valid but the underlying plan is not Pro/Premium.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    NotFound:
      description: Resource does not exist or is not visible to this key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    RateLimited:
      description: Per-minute or daily rate limit exceeded.
      headers:
        retry-after:
          schema: { type: integer, description: "Seconds until you may retry" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    UpstreamError:
      description: A dependency (e.g. Autumn, QStash) is temporarily unavailable.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    ServerError:
      description: Unexpected server error.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
