openapi: 3.1.0
info:
  title: DeckMorph API
  version: "1.0.0"
  description: |
    Turn a slide image into native, editable Google Slides objects.

    ## Auth
    Mint an API key on your [account page](/account) (you'll need to sign in
    with Google). Send it as a bearer token:

        Authorization: Bearer dm_live_xxxxxxxxxxxx

    A key is shown once at creation — store it somewhere safe. Calls made with
    a key draw down the same monthly quota as your plan (Free 15/mo, Pro
    300/mo). Revoke a key anytime from your account.

    ## How a job runs
    `POST /decompose` is async. It returns `202` with a `jobId` immediately;
    the pipeline runs in the background. Poll `GET /jobs/{id}` (or stream
    `GET /jobs/{id}/stream`) until `status` is `done`, then fetch the result
    from `GET /runs/{id}/layout.json`.

    ## Limits & errors
    - `402 QUOTA_EXCEEDED` — you're out of slides for the month; upgrade or wait.
    - `429 RATE_LIMITED` — daily allowance hit (anonymous / BYOK tiers).
    - `413 IMAGE_TOO_LARGE` — image over 16 MB decoded.
    Every error body is `{ code, message, context? }`.
servers:
  - url: https://deckmorph.com
    description: Production
security:
  - apiKey: []
paths:
  /decompose:
    post:
      summary: Submit a slide image for decomposition
      description: |
        Accepts a base64-encoded PNG and enqueues a decompose job. Returns
        `202` with the job handles. Counts one slide against your quota.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [image_b64]
              properties:
                image_b64:
                  type: string
                  description: Base64-encoded PNG (raw, or a `data:image/png;base64,` URL).
                presentationId:
                  type: string
                  description: Optional — your Google Slides presentation id, for your own correlation.
                slideId:
                  type: string
                  description: Optional — the slide id.
      responses:
        "202":
          description: Job accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobId: { type: string, example: "5f3c1b9a-..." }
                  statusUrl: { type: string, example: "/jobs/5f3c1b9a-..." }
                  streamUrl: { type: string, example: "/jobs/5f3c1b9a-.../stream" }
                  kind: { type: string, enum: [async] }
        "400": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "413": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
  /jobs/{id}:
    get:
      summary: Get job status
      description: Poll until `status` is `done` or `failed`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Current job state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobId: { type: string }
                  status: { type: string, enum: [queued, running, done, failed] }
                  currentStage: { type: string, nullable: true }
                  stagesComplete: { type: array, items: { type: string } }
                  startedAt: { type: string, format: date-time }
                  completedAt: { type: string, format: date-time, nullable: true }
                  elapsedMs: { type: integer, nullable: true }
        "404": { $ref: "#/components/responses/Error" }
  /jobs/{id}/stream:
    get:
      summary: Stream job status (SSE)
      description: |
        Server-Sent Events stream of the job state. Each event payload is the
        same shape as `GET /jobs/{id}`. The stream closes when the job reaches
        a terminal state. Handy if you'd rather not poll.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: text/event-stream of job-state events.
          content:
            text/event-stream:
              schema: { type: string }
  /runs/{id}/layout.json:
    get:
      summary: Fetch the decomposed layout
      description: |
        Once a job is `done`, this returns the LayoutJSON — the canvas,
        background, and every decomposed element (text with font/size/color,
        shapes, and asset crops). This is what the extension turns into a
        Google Slides `batchUpdate`. Available for ~24h, then auto-deleted.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: LayoutJSON for the run.
          content:
            application/json:
              schema:
                type: object
                properties:
                  canvas:
                    type: object
                    properties: { w: { type: integer }, h: { type: integer } }
                  background: { type: object }
                  elements:
                    type: array
                    items: { type: object, description: "TEXT / SHAPE / ASSET element" }
                  assets:
                    type: array
                    items: { type: object }
        "404": { $ref: "#/components/responses/Error" }
  /runs/{id}/preview.png:
    get:
      summary: Rendered preview PNG
      description: A server-side render of the decomposed layout, for eyeballing the result.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: PNG image.
          content:
            image/png:
              schema: { type: string, format: binary }
        "404": { $ref: "#/components/responses/Error" }
  /usage:
    get:
      summary: Check your remaining quota
      description: |
        Read-only. Returns how many slides you've used and your limit. Send your
        API key to see your plan's monthly quota.
      security:
        - apiKey: []
      responses:
        "200":
          description: Usage snapshot.
          content:
            application/json:
              schema:
                type: object
                properties:
                  scope: { type: string, example: user }
                  plan: { type: string, example: free }
                  used: { type: integer, example: 7 }
                  limit: { type: integer, example: 15 }
                  remaining: { type: integer, nullable: true, example: 8 }
                  period: { type: string, example: month }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: dm_live_*
      description: A DeckMorph API key minted on your account page.
  responses:
    Error:
      description: Error envelope.
      content:
        application/json:
          schema:
            type: object
            properties:
              code: { type: string, example: QUOTA_EXCEEDED }
              message: { type: string }
              context: { type: object, additionalProperties: true }
