openapi: 3.1.0
info:
  title: Slidestorm API v1
  version: "1.0.0"
  summary: Build and post TikTok slideshows from a single prompt.
  description: |
    The Slidestorm API is in beta — endpoints and schemas may evolve as we refine things.

    Designed for both AI agents and human developers: discover your available
    inputs, generate a slideshow from a text prompt, poll until it's ready,
    and post the result to TikTok.
  contact:
    name: Slidestorm Support
    url: https://slidestorm.ai
servers:
  - url: https://slidestorm.ai/api/v1
    description: Production
tags:
  - name: Discovery
  - name: Usage
  - name: Slideshows
  - name: Posts
security:
  - BearerToken: []
paths:
  /accounts:
    get:
      tags: [Discovery]
      summary: List your connected TikTok accounts
      operationId: listAccounts
      x-required-abilities: [slidestorm-api:read]
      security:
        - BearerToken: []
      responses:
        "200":
          description: Accounts retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountsListSuccess"
              examples:
                default:
                  value:
                    ok: true
                    code: 200
                    message: Accounts retrieved
                    data:
                      accounts:
                        - id: 01JT5KACXYZABCDEF01234567
                          platform: tiktok
                          username: creator_123
                          display_name: Creator 123
                          avatar_url: https://example.com/avatar.jpg
                          status: active
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
  /image-styles:
    get:
      tags: [Discovery]
      summary: Browse available image styles
      operationId: listImageStyles
      x-required-abilities: [slidestorm-api:read]
      security:
        - BearerToken: []
      responses:
        "200":
          description: Image styles retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImageStylesListSuccess"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
  /collections:
    get:
      tags: [Discovery]
      summary: Browse available image collections
      operationId: listCollections
      x-required-abilities: [slidestorm-api:read]
      security:
        - BearerToken: []
      responses:
        "200":
          description: Collections retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CollectionsListSuccess"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
  /usage:
    get:
      tags: [Usage]
      summary: Check your credit balance and costs
      operationId: getUsage
      x-required-abilities: [slidestorm-api:read]
      security:
        - BearerToken: []
      responses:
        "200":
          description: Usage retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageSuccess"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
  /slideshows:
    post:
      tags: [Slideshows]
      summary: Generate a new slideshow
      operationId: createSlideshow
      x-required-abilities: [slidestorm-api:write]
      security:
        - BearerToken: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSlideshowRequest"
      responses:
        "201":
          description: Slideshow generation queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SlideshowCreateSuccess"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "409":
          description: Idempotency conflict or in-progress duplicate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                reused_with_different_payload:
                  value:
                    ok: false
                    code: 409
                    message: Idempotency key has already been used with a different payload
                    error_code: idempotency_key_reused_with_different_payload
                request_in_progress:
                  value:
                    ok: false
                    code: 409
                    message: An identical request is currently in progress
                    error_code: idempotency_request_in_progress
        "422":
          description: Invalid payload or idempotency key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                validation:
                  value:
                    ok: false
                    code: 422
                    message: Invalid payload
                    error_code: invalid_payload
                    issues:
                      field_errors:
                        background_option:
                          - The selected background option is invalid.
                idempotency_key_required:
                  value:
                    ok: false
                    code: 422
                    message: Idempotency-Key header is required
                    error_code: idempotency_key_required
                invalid_idempotency_key:
                  value:
                    ok: false
                    code: 422
                    message: Idempotency-Key header is too long
                    error_code: invalid_idempotency_key
                slideshow_create_failed:
                  value:
                    ok: false
                    code: 422
                    message: Unable to create slideshow
                    error_code: slideshow_create_failed
        "429":
          description: Too many requests or insufficient credits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                rate_limited:
                  value:
                    ok: false
                    code: 429
                    message: Too many requests
                    error_code: rate_limited
                insufficient_credits:
                  value:
                    ok: false
                    code: 429
                    message: Daily image generation limit reached.
                    error_code: insufficient_credits
                    issues:
                      usage_stats:
                        remaining: 0
        "500":
          description: Internal server error during slideshow queueing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                create_error:
                  value:
                    ok: false
                    code: 500
                    message: Failed to queue slideshow generation
                    error_code: slideshow_create_error
  /slideshows/{id}:
    get:
      tags: [Slideshows]
      summary: Get slideshow status and slides
      description: |
        After creating a slideshow, poll this endpoint until `data.slideshow.status` reaches
        a terminal state: `completed`, `failed`, or `timed_out`. While the status is
        `generating`, the slideshow is still being built.

        Polling tips:
        - Check every 2–5 seconds, backing off gradually.
        - Stop as soon as you reach a terminal status.
        - Prefer `data.slideshow.should_poll` and `data.slideshow.next_action` as the source of truth
          for client behavior.
        - If `timed_out`, inspect `data.slideshow.diagnostics` for details on what happened.
      operationId: getSlideshow
      x-required-abilities: [slidestorm-api:read]
      security:
        - BearerToken: []
      parameters:
        - $ref: "#/components/parameters/PublicIdPath"
      responses:
        "200":
          description: Slideshow retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SlideshowSingleSuccess"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
  /posts:
    get:
      tags: [Posts]
      summary: List your posts
      operationId: listPosts
      x-required-abilities: [slidestorm-api:read]
      security:
        - BearerToken: []
      parameters:
        - name: status
          in: query
          required: false
          description: |
            Filter by aggregate lifecycle status.
            - `pending` — due-now pending posts (`scheduled_at <= now`)
            - `scheduled` — future pending posts (`scheduled_at > now`)
            - `uploaded` — all deliveries sent to TikTok inbox, awaiting manual publish
            - `posted` — all deliveries published
            - `failed` — all deliveries failed
            - `cancelled` — all deliveries cancelled
            - `partial` — mixed outcomes (at least one failed + at least one non-failed delivery)
          schema:
            type: string
            enum: [pending, scheduled, uploaded, posted, failed, cancelled, partial]
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: per_page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 20
      responses:
        "200":
          description: Posts retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostsListSuccess"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
    post:
      tags: [Posts]
      summary: Post a completed slideshow to TikTok
      operationId: createPost
      x-required-abilities: [slidestorm-api:write]
      security:
        - BearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePostRequest"
      responses:
        "200":
          description: Duplicate post returned (idempotent by external_id)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostSingleSuccess"
              examples:
                duplicate:
                  value:
                    ok: true
                    code: 200
                    message: Post already exists
                    data:
                      duplicate: true
                      post:
                        id: 01JT5KAXXYZABCDEF01234567
                        type: slideshow
                        platform: tiktok
                        status: pending
                        caption: Post this slideshow
                        scheduled_at: 2026-04-24T16:00:00Z
                        created_at: 2026-04-24T15:00:00Z
                        external_id: external-123
                        accounts:
                          total: 1
                          states:
                            - account_id: 01JT5KACXYZABCDEF01234567
                              status: pending
                              published_url: null
                              error_message: null
                              last_attempted_at: null
        "201":
          description: Post queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostSingleSuccess"
              examples:
                created:
                  value:
                    ok: true
                    code: 201
                    message: Post queued
                    data:
                      duplicate: false
                      post:
                        id: 01JT5KAXXYZABCDEF01234567
                        type: slideshow
                        platform: tiktok
                        status: pending
                        caption: Post this slideshow
                        scheduled_at: 2026-04-24T16:00:00Z
                        created_at: 2026-04-24T15:00:00Z
                        external_id: null
                        accounts:
                          total: 1
                          states:
                            - account_id: 01JT5KACXYZABCDEF01234567
                              status: pending
                              published_url: null
                              error_message: null
                              last_attempted_at: null
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "409":
          description: Slideshow not ready or external id payload conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                slideshow_not_ready:
                  value:
                    ok: false
                    code: 409
                    message: Slideshow is not ready for posting
                    error_code: slideshow_not_ready
                    issues:
                      slideshow:
                        id: 01JT5KA8XYZABCDEF01234567
                        status: generating
                external_id_reused_with_different_payload:
                  value:
                    ok: false
                    code: 409
                    message: external_id has already been used with a different payload
                    error_code: external_id_reused_with_different_payload
                    issues:
                      external_id:
                        value: external-123
                        existing_post_id: 01JT5KAXXYZABCDEF01234567
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
  /posts/{id}:
    get:
      tags: [Posts]
      summary: Get a post and its delivery status
      description: |
        Returns the post's current aggregate status and per-account delivery states.
        This endpoint returns persisted data only — it does not contact TikTok.
        To refresh the latest status from TikTok, call `POST /posts/{id}/check-status`.
      operationId: getPost
      x-required-abilities: [slidestorm-api:read]
      security:
        - BearerToken: []
      parameters:
        - $ref: "#/components/parameters/PublicIdPath"
      responses:
        "200":
          description: Post retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostSingleSuccessWithoutDuplicate"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
    patch:
      tags: [Posts]
      summary: Update a post before delivery
      operationId: updatePost
      x-required-abilities: [slidestorm-api:write]
      security:
        - BearerToken: []
      parameters:
        - $ref: "#/components/parameters/PublicIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePostRequest"
      responses:
        "200":
          description: Post updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostSingleSuccessWithoutDuplicate"
        "400":
          description: Post lifecycle conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                not_mutable:
                  value:
                    ok: false
                    code: 400
                    message: Post is not mutable
                    error_code: post_not_mutable
                accounts_locked:
                  value:
                    ok: false
                    code: 400
                    message: Accounts cannot be changed after successful delivery
                    error_code: accounts_locked
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
    delete:
      tags: [Posts]
      summary: Delete a pending or scheduled post
      operationId: deletePost
      x-required-abilities: [slidestorm-api:write]
      security:
        - BearerToken: []
      parameters:
        - $ref: "#/components/parameters/PublicIdPath"
      responses:
        "200":
          description: Post deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptySuccess"
        "400":
          description: Post cannot be deleted in current status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                not_deletable:
                  value:
                    ok: false
                    code: 400
                    message: Only pending or scheduled posts can be deleted
                    error_code: post_not_deletable
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
  /posts/{id}/check-status:
    post:
      tags: [Posts]
      summary: Refresh delivery status from TikTok
      description: |
        Contacts TikTok using the post's `platform_publish_id` and syncs the delivery
        status back into Slidestorm.

        A few things to keep in mind:
        - `uploaded` means the content reached TikTok's inbox for manual review — it may not
          be publicly visible yet.
        - `posted` means published and live.
        - `failed` may still be recoverable. Check `data.status.is_recoverable` to decide
          whether calling `POST /posts/{id}/retry` makes sense.
      operationId: checkPostStatus
      x-required-abilities: [slidestorm-api:write]
      security:
        - BearerToken: []
      parameters:
        - $ref: "#/components/parameters/PublicIdPath"
      responses:
        "200":
          description: Post status synced
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostStatusCheckSuccess"
        "400":
          description: Post cannot be checked right now
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                not_checkable:
                  value:
                    ok: false
                    code: 400
                    message: No platform publish ID available for status check
                    error_code: post_status_not_checkable
        "401":
          description: Provider token issue while checking status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                token_expired:
                  value:
                    ok: false
                    code: 401
                    message: "Unable to check post status: Token expired and refresh failed. Please reconnect your TikTok account."
                    error_code: token_expired
                token_invalid:
                  value:
                    ok: false
                    code: 401
                    message: "Unable to check post status: Access token is invalid. Please reconnect your TikTok account."
                    error_code: token_invalid
        "429":
          $ref: "#/components/responses/RateLimitedError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "500":
          description: Provider/API error while checking status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                platform_api_error:
                  value:
                    ok: false
                    code: 500
                    message: Failed to check post status. Please try again.
                    error_code: platform_api_error
                post_status_check_error:
                  value:
                    ok: false
                    code: 500
                    message: Failed to check post status. Please try again.
                    error_code: post_status_check_error
  /posts/{id}/retry:
    post:
      tags: [Posts]
      summary: Retry failed or cancelled deliveries
      description: |
        Re-queues deliveries that are in `failed` or `cancelled` status, resetting them to `pending`.
        Omit `account_ids` to retry all eligible deliveries at once.
        If nothing is retryable, you'll get a `400` with `error_code=nothing_to_retry`.
      operationId: retryPost
      x-required-abilities: [slidestorm-api:write]
      security:
        - BearerToken: []
      parameters:
        - $ref: "#/components/parameters/PublicIdPath"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RetryPostRequest"
      responses:
        "200":
          description: Post queued for retry
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostSingleSuccessWithoutDuplicate"
        "400":
          description: No retryable deliveries
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                nothing_to_retry:
                  value:
                    ok: false
                    code: 400
                    message: No retryable deliveries found
                    error_code: nothing_to_retry
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimitedError"
components:
  securitySchemes:
    BearerToken:
      type: http
      scheme: bearer
      bearerFormat: Sanctum token
      description: |
        Include a bearer token with your requests. Create one from your account settings — it works with all endpoints.
  parameters:
    PublicIdPath:
      name: id
      in: path
      required: true
      description: 26-character ULID public identifier.
      schema:
        $ref: "#/components/schemas/PublicId"
  responses:
    UnauthorizedError:
      description: Bearer token is missing or invalid
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            invalid_token:
              value:
                ok: false
                code: 401
                message: Authentication token is invalid or missing
                error_code: invalid_token
    ForbiddenError:
      description: Your token doesn't have permission for this action
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            forbidden:
              value:
                ok: false
                code: 403
                message: You do not have permission to perform this action
                error_code: forbidden
    NotFoundError:
      description: Resource not found or not owned by your account
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            not_found:
              value:
                ok: false
                code: 404
                message: Resource not found
                error_code: not_found
    ValidationError:
      description: Request body failed validation
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            invalid_payload:
              value:
                ok: false
                code: 422
                message: Invalid payload
                error_code: invalid_payload
                issues:
                  field_errors:
                    caption:
                      - The caption field is required.
    RateLimitedError:
      description: Too many requests — back off and try again
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            rate_limited:
              value:
                ok: false
                code: 429
                message: Too many requests
                error_code: rate_limited
    SlideshowNotReadyError:
      description: Slideshow is still generating and not ready to post
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            not_ready:
              value:
                ok: false
                code: 409
                message: Slideshow is not ready for posting
                error_code: slideshow_not_ready
                issues:
                  slideshow:
                    id: 01JT5KA8XYZABCDEF01234567
                    status: generating
    InsufficientCreditsError:
      description: Not enough credits remaining for this operation
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            insufficient_credits:
              value:
                ok: false
                code: 429
                message: Daily image generation limit reached.
                error_code: insufficient_credits
                issues:
                  usage_stats:
                    remaining: 0
  schemas:
    PublicId:
      type: string
      minLength: 26
      maxLength: 26
      pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
      example: 01JT5KA8XYZABCDEF01234567
    IsoDateTime:
      type: string
      format: date-time
      example: "2026-04-24T15:00:00Z"
    SuccessEnvelope:
      type: object
      required: [ok, code, message, data]
      properties:
        ok:
          type: boolean
          const: true
        code:
          type: integer
          minimum: 100
          maximum: 599
        message:
          type: string
        data:
          type: object
          additionalProperties: true
    ErrorEnvelope:
      type: object
      required: [ok, code, message, error_code]
      properties:
        ok:
          type: boolean
          const: false
        code:
          type: integer
          minimum: 100
          maximum: 599
        message:
          type: string
        error_code:
          type: string
        issues:
          type: object
          additionalProperties: true
    Account:
      type: object
      required: [id, platform, username, display_name, avatar_url, status]
      properties:
        id:
          $ref: "#/components/schemas/PublicId"
        platform:
          type: string
          enum: [tiktok]
        username:
          type: string
        display_name:
          type: string
        avatar_url:
          type: [string, "null"]
          format: uri
        status:
          type: string
          enum: [active]
    ImageStyle:
      type: object
      required: [id, name, display_name, style_prompt]
      properties:
        id:
          $ref: "#/components/schemas/PublicId"
        name:
          type: string
        display_name:
          type: string
        style_prompt:
          type: string
    ImageCollection:
      type: object
      required: [id, name, display_name, type, image_count, style_prompt, subjects]
      properties:
        id:
          $ref: "#/components/schemas/PublicId"
        name:
          type: string
        display_name:
          type: string
        type:
          type: string
          enum: [stock, user]
        image_count:
          type: integer
          minimum: 0
        style_prompt:
          type: [string, "null"]
        subjects:
          type: array
          items:
            type: string
    Credits:
      type: object
      required: [remaining, subscription_remaining, pack_remaining, has_active_subscription]
      properties:
        remaining:
          type: integer
        subscription_remaining:
          type: integer
        pack_remaining:
          type: integer
        has_active_subscription:
          type: boolean
    CreditCosts:
      type: object
      required: [slideshow_generation, image_generation]
      properties:
        slideshow_generation:
          type: integer
          minimum: 0
        image_generation:
          type: integer
          minimum: 0
    SlideshowGenerationSummary:
      type: object
      required: [total_slides, completed_images, failed_images, pending_images, missing_images]
      properties:
        total_slides:
          type: integer
          minimum: 0
        completed_images:
          type: integer
          minimum: 0
        failed_images:
          type: integer
          minimum: 0
        pending_images:
          type: integer
          minimum: 0
        missing_images:
          type: integer
          minimum: 0
    SlideshowDiagnostics:
      type: object
      required: [failure_code, timed_out_at, last_progress_at, timeout_threshold_minutes]
      properties:
        failure_code:
          type: [string, "null"]
          example: generation_timeout
        timed_out_at:
          anyOf:
            - $ref: "#/components/schemas/IsoDateTime"
            - type: "null"
        last_progress_at:
          anyOf:
            - $ref: "#/components/schemas/IsoDateTime"
            - type: "null"
        timeout_threshold_minutes:
          type: integer
          minimum: 1
    SlideshowSlide:
      type: object
      required: [id, order, signed_image_url, caption, captions, error_message]
      properties:
        id:
          $ref: "#/components/schemas/PublicId"
        order:
          type: integer
          minimum: 0
        signed_image_url:
          type: [string, "null"]
          format: uri
        caption:
          type: [string, "null"]
        captions:
          type: array
          items:
            $ref: "#/components/schemas/SlideCaption"
        error_message:
          type: [string, "null"]
    SlideCaption:
      type: object
      required:
        - id
        - text
        - position
        - fontSize
        - fontFamily
        - fontWeight
        - textColor
        - backgroundColor
        - outline
        - italic
        - underline
        - textStyle
        - textAlign
        - boxWidth
      properties:
        id:
          type: string
        text:
          type: string
        position:
          type: object
          required: [x, y]
          properties:
            x:
              type: number
            y:
              type: number
        fontSize:
          type: [number, "null"]
        fontFamily:
          type: [string, "null"]
        fontWeight:
          type: string
        textColor:
          type: string
        backgroundColor:
          type: [string, "null"]
        outline:
          type: boolean
        italic:
          type: boolean
        underline:
          type: boolean
        textStyle:
          type: [string, "null"]
        textAlign:
          type: [string, "null"]
        boxWidth:
          type: [number, "null"]
    Slideshow:
      type: object
      description: |
        A slideshow and its generation status. The status field tells you where it is in its lifecycle:
        - `generating` — still being built (keep polling).
        - `completed` — ready to post.
        - `failed` — generation finished with unrecoverable failures.
        - `timed_out` — generation took too long; check `diagnostics` for details.
      required:
        - id
        - title
        - status
        - created_at
        - generation_summary
        - diagnostics
        - is_postable
        - postability_reasons
        - is_terminal
        - should_poll
        - next_action
        - slides
      properties:
        id:
          $ref: "#/components/schemas/PublicId"
        title:
          type: string
        status:
          type: string
          description: Where the slideshow is in its generation lifecycle.
          enum: [generating, completed, failed, timed_out]
        created_at:
          anyOf:
            - $ref: "#/components/schemas/IsoDateTime"
            - type: "null"
        generation_summary:
          $ref: "#/components/schemas/SlideshowGenerationSummary"
        diagnostics:
          $ref: "#/components/schemas/SlideshowDiagnostics"
        is_postable:
          type: boolean
        postability_reasons:
          type: array
          description: |
            Machine-readable reasons why `is_postable` is false.
            `no_slides_generated` appears only for terminal no-output outcomes.
          items:
            type: string
        is_terminal:
          type: boolean
          description: |
            True when slideshow generation has reached a final state.
            When true, clients should stop polling this lifecycle.
        should_poll:
          type: boolean
          description: |
            True when the slideshow is still being generated and clients should keep polling.
            Equivalent to `!is_terminal`.
        next_action:
          type: string
          description: |
            Machine-friendly guidance for the next client action:
            - `poll` — continue polling `GET /slideshows/{id}`
            - `create_post` — slideshow is ready for `POST /posts`
            - `create_new_slideshow` — terminal non-postable result; submit a new slideshow request
          enum: [poll, create_post, create_new_slideshow]
        slides:
          type: array
          items:
            $ref: "#/components/schemas/SlideshowSlide"
    PostAccountState:
      type: object
      description: Delivery status for a single target account. The aggregate post status is derived from these.
      required: [account_id, status, published_url, error_message, last_attempted_at]
      properties:
        account_id:
          anyOf:
            - $ref: "#/components/schemas/PublicId"
            - type: "null"
        status:
          type: string
          description: |
            Delivery status for this account.
            - `uploaded` — sent to TikTok inbox for manual publish (not necessarily public yet)
            - `posted` — published and live
          enum: [pending, uploaded, posted, failed, cancelled]
        published_url:
          type: [string, "null"]
          format: uri
        error_message:
          type: [string, "null"]
        last_attempted_at:
          anyOf:
            - $ref: "#/components/schemas/IsoDateTime"
            - type: "null"
    PostAccounts:
      type: object
      required: [total, states]
      properties:
        total:
          type: integer
          minimum: 0
        states:
          type: array
          items:
            $ref: "#/components/schemas/PostAccountState"
    SocialAccountSummary:
      type: object
      required: [id, platform, username, display_name, avatar_url]
      properties:
        id:
          anyOf:
            - $ref: "#/components/schemas/PublicId"
            - type: "null"
        platform:
          type: string
          enum: [tiktok]
        username:
          type: [string, "null"]
        display_name:
          type: [string, "null"]
        avatar_url:
          type: [string, "null"]
          format: uri
    PostableSummary:
      type: object
      required: [id, type, title, status, preview_image_url]
      properties:
        id:
          type: [string, "null"]
        type:
          type: string
        title:
          type: [string, "null"]
        status:
          type: [string, "null"]
        preview_image_url:
          type: [string, "null"]
          format: uri
    Post:
      type: object
      description: |
        A post and its overall delivery status. The top-level `status` is derived from per-account
        delivery outcomes and scheduling. See `accounts.states` for individual account results.
      required:
        - id
        - type
        - platform
        - status
        - title
        - description
        - caption
        - scheduled_at
        - posted_at
        - created_at
        - updated_at
        - external_id
        - platform_publish_id
        - platform_post_id
        - error_message
        - is_immediate
        - can_check_status
        - can_link_to_post
        - tiktok_url
        - social_account
        - postable
        - accounts
      properties:
        id:
          $ref: "#/components/schemas/PublicId"
        type:
          type: string
          enum: [slideshow, video, talking_head_video]
        platform:
          type: string
          enum: [tiktok]
        status:
          type: string
          description: |
            Overall post status, derived from all delivery outcomes.
            - `pending` — queued or processing
            - `scheduled` — scheduled for the future
            - `uploaded` — all deliveries sent to TikTok inbox for manual publish
            - `posted` — all deliveries published and live
            - `failed` — all deliveries failed
            - `cancelled` — all deliveries cancelled
            - `partial` — mixed outcomes across accounts
          enum: [pending, scheduled, uploaded, posted, failed, cancelled, partial]
        title:
          type: string
          maxLength: 90
        description:
          type: string
          maxLength: 4000
        caption:
          type: string
          maxLength: 4000
        scheduled_at:
          anyOf:
            - $ref: "#/components/schemas/IsoDateTime"
            - type: "null"
        posted_at:
          anyOf:
            - $ref: "#/components/schemas/IsoDateTime"
            - type: "null"
        created_at:
          $ref: "#/components/schemas/IsoDateTime"
        updated_at:
          anyOf:
            - $ref: "#/components/schemas/IsoDateTime"
            - type: "null"
        external_id:
          type: [string, "null"]
        platform_publish_id:
          type: [string, "null"]
        platform_post_id:
          type: [string, "null"]
        error_message:
          type: [string, "null"]
        is_immediate:
          type: boolean
        can_check_status:
          type: boolean
          description: True when you can call `POST /posts/{id}/check-status` to refresh delivery status.
        can_link_to_post:
          type: boolean
          description: True when a public link to the TikTok post is available (see `tiktok_url`).
        tiktok_url:
          type: [string, "null"]
          format: uri
        social_account:
          anyOf:
            - $ref: "#/components/schemas/SocialAccountSummary"
            - type: "null"
        postable:
          anyOf:
            - $ref: "#/components/schemas/PostableSummary"
            - type: "null"
        accounts:
          $ref: "#/components/schemas/PostAccounts"
    AccountsListSuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [accounts]
              properties:
                accounts:
                  type: array
                  items:
                    $ref: "#/components/schemas/Account"
    ImageStylesListSuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [styles]
              properties:
                styles:
                  type: array
                  items:
                    $ref: "#/components/schemas/ImageStyle"
    CollectionsListSuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [collections]
              properties:
                collections:
                  type: array
                  items:
                    $ref: "#/components/schemas/ImageCollection"
    UsageSuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [credits, costs]
              properties:
                credits:
                  $ref: "#/components/schemas/Credits"
                costs:
                  $ref: "#/components/schemas/CreditCosts"
    SlideshowCreateSuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [slideshow, polling]
              properties:
                slideshow:
                  type: object
                  required: [id, title, status]
                  properties:
                    id:
                      $ref: "#/components/schemas/PublicId"
                    title:
                      type: string
                    status:
                      type: string
                      enum: [generating, completed, failed, timed_out]
                polling:
                  type: object
                  required: [endpoint, status]
                  properties:
                    endpoint:
                      type: string
                      example: /api/v1/slideshows/01JT5KA8XYZABCDEF01234567
                    status:
                      type: string
                      enum: [generating, completed, failed, timed_out]
    SlideshowSingleSuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [slideshow]
              properties:
                slideshow:
                  $ref: "#/components/schemas/Slideshow"
    PostSingleSuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [post]
              properties:
                post:
                  $ref: "#/components/schemas/Post"
                duplicate:
                  type: boolean
    PostSingleSuccessWithoutDuplicate:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [post]
              properties:
                post:
                  $ref: "#/components/schemas/Post"
    PostStatusSnapshot:
      type: object
      description: |
        The latest delivery status, returned by `POST /posts/{id}/check-status`.
        Combines what TikTok reports with Slidestorm's own lifecycle interpretation.
      required:
        - post_status
        - provider_status
        - is_terminal
        - is_recoverable
        - platform_post_id
        - error_code
        - error_message
        - progress
      properties:
        post_status:
          type: string
          description: The post's overall status after syncing with TikTok.
          enum: [pending, scheduled, uploaded, posted, failed, cancelled, partial]
        provider_status:
          type: [string, "null"]
          description: The raw status value from TikTok, when available.
        is_terminal:
          type: boolean
          description: True when the delivery has reached a final state — no more polling needed.
        is_recoverable:
          type: boolean
          description: True when you can retry this delivery (failed or cancelled deliveries exist that are eligible).
        platform_post_id:
          type: [string, "null"]
        error_code:
          type: [string, "null"]
        error_message:
          type: [string, "null"]
        progress:
          type: object
          description: Progress details for UI display (may include stage, title, message, and percentage when available).
          additionalProperties: true
    PostStatusCheckSuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [status, post]
              properties:
                status:
                  $ref: "#/components/schemas/PostStatusSnapshot"
                post:
                  $ref: "#/components/schemas/Post"
    PostsListSuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              required: [posts, pagination]
              properties:
                posts:
                  type: array
                  items:
                    $ref: "#/components/schemas/Post"
                pagination:
                  type: object
                  required: [current_page, last_page, per_page, total, from, to]
                  properties:
                    current_page:
                      type: integer
                      minimum: 1
                    last_page:
                      type: integer
                      minimum: 1
                    per_page:
                      type: integer
                      minimum: 1
                    total:
                      type: integer
                      minimum: 0
                    from:
                      type: [integer, "null"]
                      minimum: 1
                    to:
                      type: [integer, "null"]
                      minimum: 1
    EmptySuccess:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              additionalProperties: false
    PlatformConfigurations:
      type: object
      additionalProperties: false
      properties:
        tiktok:
          type: object
          additionalProperties: false
          description: TikTok-specific posting configuration.
          properties:
            post_mode:
              type: string
              enum: [MEDIA_UPLOAD]
              description: |
                Upload content to TikTok inbox. After MEDIA_UPLOAD succeeds, the user must open the TikTok app and publish manually.
            privacy_level:
              type: string
              enum:
                - PUBLIC_TO_EVERYONE
                - MUTUAL_FOLLOW_FRIENDS
                - FOLLOWER_OF_CREATOR
                - SELF_ONLY
    AccountInput:
      type: object
      required: [id]
      properties:
        id:
          $ref: "#/components/schemas/PublicId"
    CreateSlideshowRequest:
      type: object
      additionalProperties: false
      required: [prompt, slide_count, background_option]
      properties:
        prompt:
          type: string
          maxLength: 10000
        slide_count:
          type: integer
          minimum: 1
          maximum: 12
        background_option:
          type: string
          enum: [generate, library, single]
        quantity:
          type: integer
          enum: [1]
        image_style_id:
          $ref: "#/components/schemas/PublicId"
        custom_image_style:
          type: string
          maxLength: 1000
        model:
          type: string
          maxLength: 255
        collection_id:
          $ref: "#/components/schemas/PublicId"
        single_image_id:
          $ref: "#/components/schemas/PublicId"
        reference_image_id:
          $ref: "#/components/schemas/PublicId"
        influencer_id:
          $ref: "#/components/schemas/PublicId"
      description: |
        Which optional fields are required depends on your `background_option`:
        - `generate` — provide either `image_style_id` or `custom_image_style`
        - `library` — provide `collection_id`
        - `single` — provide `single_image_id`
    CreatePostRequest:
      type: object
      additionalProperties: false
      required: [caption, slideshow_id, accounts]
      properties:
        caption:
          type: string
          minLength: 1
          maxLength: 4000
          description: |
            Caption text used for TikTok photo posting.
            TikTok text limits are strict: title up to 90 characters (from the start of caption)
            and description up to 4000 characters. Over-limit payloads are rejected.
        slideshow_id:
          $ref: "#/components/schemas/PublicId"
        accounts:
          type: array
          minItems: 1
          maxItems: 10
          items:
            $ref: "#/components/schemas/AccountInput"
        scheduled_at:
          oneOf:
            - $ref: "#/components/schemas/IsoDateTime"
            - type: "null"
        external_id:
          type: string
          maxLength: 191
        platform_configurations:
          $ref: "#/components/schemas/PlatformConfigurations"
      description: |
        `slideshow_id` is required and must point to a completed slideshow you own.
        You can optionally include an `external_id` for idempotency — if the same
        `external_id` is sent with a different payload, you'll get a `409`.
    UpdatePostRequest:
      type: object
      additionalProperties: false
      properties:
        caption:
          type: string
          minLength: 1
          maxLength: 4000
          description: |
            Caption text used for TikTok photo posting.
            TikTok text limits are strict: title up to 90 characters (from the start of caption)
            and description up to 4000 characters. Over-limit payloads are rejected.
        accounts:
          type: array
          minItems: 1
          maxItems: 10
          items:
            $ref: "#/components/schemas/AccountInput"
        scheduled_at:
          oneOf:
            - $ref: "#/components/schemas/IsoDateTime"
            - type: "null"
        platform_configurations:
          $ref: "#/components/schemas/PlatformConfigurations"
      description: Include at least one field you'd like to change.
    RetryPostRequest:
      type: object
      additionalProperties: false
      description: |
        Optionally specify which accounts to retry.
        If omitted, all eligible deliveries for the post are retried.
      properties:
        account_ids:
          type: array
          minItems: 1
          maxItems: 10
          items:
            $ref: "#/components/schemas/PublicId"
