openapi: 3.1.0
info:
  title: FlipLetters API
  version: 1.0.0
  summary: "Cloud render, teams, workspaces, and API keys for FlipLetters."
  description: "FlipLetters makes split-flap / flip-dot board videos.\n\nThis specification documents the **real** public HTTP surface.\nIt is small on purpose.\n\n- **Drafting boards needs no REST API.** Use flipcode (npm `@flipletters/flipcode`)\n  or MCP: hosted Streamable HTTP at https://app.flipletters.com/api/mcp\n  (alias /mcp/v1) or local stdio `npx -y flipletters-mcp-server`.\n- **HTTP APIs run on https://app.flipletters.com.** flipletters.com publishes\n  this spec, docs, and agent files; it does not execute render/team calls.\n- **Auth:** `Authorization: Bearer <firebase-id-token>` (Studio session) or\n  `Authorization: Bearer flk_…` (key minted by a human in Studio → Account →\n  API keys & agents). Scopes: `render`, `team`, `postiz`.\n- **No public webhooks** are documented. Stripe / TikTok / billing hooks are\n  internal Studio integrations, not a partner webhook API.\n- Other `/api/*` routes exist for the Studio UI (analytics, catalog checkout,\n  admin, cron). They are session-only and are omitted here."
  contact:
    name: FlipLetters
    email: "dev@flipletters.com"
    url: "https://flipletters.com/docs/api"
  license:
    name: Proprietary Studio API; flipcode parser is MIT
    url: "https://www.npmjs.com/package/@flipletters/flipcode"
servers:
  -     url: "https://app.flipletters.com"
        description: FlipLetters Studio (live HTTP API)
tags:
  -     name: Discovery
        description: Spec and docs on flipletters.com
  -     name: Render
        description: "Cloud MP4 export (scope: render)"
  -     name: Teams
        description: "Shared template library (scope: team)"
  -     name: Keys
        description: Mint/revoke flk_ keys (Firebase session only)
  -     name: Workspaces
        description: Agency projects and Postiz destinations
  -     name: Postiz
        description: Schedule/draft/post via a connected Postiz
paths:
  /openapi.json:
    servers:
      -         url: "https://flipletters.com"

    get:
      tags:
        - Discovery
      summary: This OpenAPI document (published on flipletters.com)
      operationId: getOpenApiJson
      responses:
        200:
          description: OpenAPI 3 document
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /api/render:
    get:
      tags:
        - Render
      summary: "Render capability probe, or job status when jobId is set"
      operationId: getRender
      parameters:
        -           name: jobId
                    in: query
                    required: false
                    schema:
                      type: string
                    description: "When set, return that job's status. Requires render auth. Without jobId, returns { enabled, mode } with no auth."
      responses:
        200:
          description: "Capability { enabled, mode: local|cloud } or job { jobId, status, progress, url, fileName, error }"
        401:
          $ref: "#/components/responses/ApiError"
        403:
          $ref: "#/components/responses/ApiError"
        404:
          $ref: "#/components/responses/ApiError"
    post:
      tags:
        - Render
      summary: Start a cloud MP4 render under the caller's plan
      operationId: postRender
      security:
        -           bearerAuth: []

      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - project
              properties:
                project:
                  type: object
                  description: Studio project JSON (compiled from flipcode or the editor). Must include a non-empty slides array.
                  additionalProperties: true
                repeat:
                  type: integer
                  minimum: 1
                  maximum: 120
                  description: Loop count for ambience-style exports
                workspaceId:
                  type: string
                  description: Optional project id; required to match a workspace-scoped key
      responses:
        200:
          description: "{ jobId, statusUrl }"
        400:
          $ref: "#/components/responses/ApiError"
        401:
          $ref: "#/components/responses/ApiError"
        402:
          $ref: "#/components/responses/ApiError"
        403:
          $ref: "#/components/responses/ApiError"
        502:
          $ref: "#/components/responses/ApiError"
    delete:
      tags:
        - Render
      summary: Cancel a queued or in-flight cloud render
      operationId: deleteRender
      security:
        -           bearerAuth: []

      parameters:
        -           name: jobId
                    in: query
                    required: true
                    schema:
                      type: string
      responses:
        200:
          description: "{ ok, status: cancelled }"
        400:
          $ref: "#/components/responses/ApiError"
        401:
          $ref: "#/components/responses/ApiError"
        403:
          $ref: "#/components/responses/ApiError"
        404:
          $ref: "#/components/responses/ApiError"
        409:
          $ref: "#/components/responses/ApiError"
  /api/team:
    post:
      tags:
        - Teams
      summary: "Team library actions (create, join, list, share, …)"
      operationId: postTeam
      security:
        -           bearerAuth: []

      description: "One POST endpoint; the JSON `action` field selects the operation. Creating a team is a Premium perk; joining by invite is free."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
              properties:
                action:
                  type: string
                  enum:
                    - listTeams
                    - create
                    - join
                    - listTemplates
                    - leave
                    - share
                    - removeTemplate
                name:
                  type: string
                code:
                  type: string
                  description: Invite code for join
                teamId:
                  type: string
                project:
                  type: object
                  additionalProperties: true
                  description: "Required for action=share"
                templateId:
                  type: string
      responses:
        200:
          description: "Action-specific JSON (teams, teamId, templates, …)"
        400:
          $ref: "#/components/responses/ApiError"
        401:
          $ref: "#/components/responses/ApiError"
        402:
          $ref: "#/components/responses/ApiError"
        403:
          $ref: "#/components/responses/ApiError"
        404:
          $ref: "#/components/responses/ApiError"
        500:
          $ref: "#/components/responses/ApiError"
  /api/keys:
    get:
      tags:
        - Keys
      summary: List the signed-in user's API keys (metadata only)
      operationId: listKeys
      security:
        -           firebaseSession: []

      description: Firebase ID token only. Agents cannot mint keys. The plaintext secret is never listed.
      responses:
        200:
          description: "{ keys: ApiKeyPublic[] }"
        401:
          $ref: "#/components/responses/ApiError"
    post:
      tags:
        - Keys
      summary: Create an API key (plaintext shown once)
      operationId: createKey
      security:
        -           firebaseSession: []

      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                workspaceId:
                  type: string
                  description: "Optional: scope the key to one project"
      responses:
        200:
          description: "{ key, plaintext } — store plaintext immediately"
        400:
          $ref: "#/components/responses/ApiError"
        401:
          $ref: "#/components/responses/ApiError"
    delete:
      tags:
        - Keys
      summary: Revoke a key
      operationId: revokeKey
      security:
        -           firebaseSession: []

      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - keyId
              properties:
                keyId:
                  type: string
      responses:
        200:
          description: "{ ok: true }"
        400:
          $ref: "#/components/responses/ApiError"
        401:
          $ref: "#/components/responses/ApiError"
  /api/workspaces:
    get:
      tags:
        - Workspaces
      summary: List projects (ensures a Personal default)
      operationId: listWorkspaces
      security:
        -           bearerAuth: []

      responses:
        200:
          description: "{ workspaces, activeWorkspaceId, keyWorkspaceId }"
        401:
          $ref: "#/components/responses/ApiError"
    post:
      tags:
        - Workspaces
      summary: Create a project or set the active one (Studio session only)
      operationId: createOrSwitchWorkspace
      security:
        -           firebaseSession: []

      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                activeWorkspaceId:
                  type: string
      responses:
        200:
          description: "{ workspace } or { ok, activeWorkspaceId }"
        401:
          $ref: "#/components/responses/ApiError"
        403:
          $ref: "#/components/responses/ApiError"
  "/api/workspaces/{id}":
    parameters:
      -         name: id
                in: path
                required: true
                schema:
                  type: string
    get:
      tags:
        - Workspaces
      summary: Get one project and its (masked) share destinations
      operationId: getWorkspace
      security:
        -           bearerAuth: []

      responses:
        200:
          description: "{ workspaceId, destinations }"
        401:
          $ref: "#/components/responses/ApiError"
        403:
          $ref: "#/components/responses/ApiError"
    patch:
      tags:
        - Workspaces
      summary: Rename a project (Studio session)
      operationId: renameWorkspace
      security:
        -           firebaseSession: []

      responses:
        200:
          description: Updated workspace
        401:
          $ref: "#/components/responses/ApiError"
    delete:
      tags:
        - Workspaces
      summary: Delete a project (Studio session)
      operationId: deleteWorkspace
      security:
        -           firebaseSession: []

      responses:
        200:
          description: "{ ok: true }"
        401:
          $ref: "#/components/responses/ApiError"
  "/api/workspaces/{id}/destinations":
    parameters:
      -         name: id
                in: path
                required: true
                schema:
                  type: string
    get:
      tags:
        - Workspaces
      summary: List Postiz share accounts on a project
      operationId: listDestinations
      security:
        -           bearerAuth: []

      responses:
        200:
          description: "{ destinations }"
        401:
          $ref: "#/components/responses/ApiError"
    post:
      tags:
        - Workspaces
      summary: Add a Postiz destination
      operationId: addDestination
      security:
        -           firebaseSession: []

      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  enum:
                    - postiz
                name:
                  type: string
                baseUrl:
                  type: string
                apiKey:
                  type: string
      responses:
        200:
          description: Created destination (secret masked)
        401:
          $ref: "#/components/responses/ApiError"
  /api/postiz/integrations:
    get:
      tags:
        - Postiz
      summary: List channels on a Postiz destination
      operationId: listPostizIntegrations
      security:
        -           bearerAuth: []

      parameters:
        -           name: workspaceId
                    in: query
                    required: true
                    schema:
                      type: string
        -           name: destinationId
                    in: query
                    required: true
                    schema:
                      type: string
      responses:
        200:
          description: Postiz integrations list
        401:
          $ref: "#/components/responses/ApiError"
        403:
          $ref: "#/components/responses/ApiError"
  /api/postiz/schedule:
    post:
      tags:
        - Postiz
      summary: Upload a FlipLetters export to Postiz and schedule/draft/post
      operationId: schedulePostiz
      security:
        -           bearerAuth: []

      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                workspaceId:
                  type: string
                destinationId:
                  type: string
                integrationId:
                  type: string
                date:
                  type: string
                  format: date-time
                type:
                  type: string
                  enum:
                    - schedule
                    - draft
                    - now
                content:
                  type: string
                fileName:
                  type: string
                mediaUrl:
                  type: string
      responses:
        200:
          description: Postiz create-post result
        401:
          $ref: "#/components/responses/ApiError"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Firebase ID token or Studio API key `flk_…`. Keys are created by a signed-in human; they are never self-issued by agents."
    firebaseSession:
      type: http
      scheme: bearer
      description: Firebase ID token only (browser Studio session). API keys are rejected.
  schemas:
    ApiError:
      type: object
      required:
        - error
        - code
        - hint
      properties:
        error:
          type: string
          description: "Human-readable message (string, for existing Studio UI)"
        code:
          type: string
          description: "Stable machine code, e.g. NOT_FOUND"
        hint:
          type: string
          description: Resolution hint for agents and humans
  responses:
    ApiError:
      description: Structured FlipLetters API error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ApiError"
