openapi: 3.1.0
info:
  title: Slidestorm API v1
  version: "1.0.0"
  summary: Agent-first API for generating slideshows and posting to TikTok.
  description: |
    Beta notice: The Slidestorm API is currently in beta and subject to change.
    Endpoint behavior and schema details may evolve.

    Slidestorm API v1 is designed for agent workflows:
    discover valid inputs, create slideshow jobs, poll until terminal status,
    and create posts from completed slideshows.
  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 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"
  /image-styles:
    get:
      tags: [Discovery]
      summary: List stock 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"
  /collections:
    get:
      tags: [Discovery]
      summary: List available non-empty 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"
  /usage:
    get:
      tags: [Usage]
      summary: Get current 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"
  /slideshows:
    post:
      tags: [Slideshows]
      summary: Create a slideshow generation job
      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
        "429":
          $ref: "#/components/responses/InsufficientCreditsError"
        "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 details and generation status
      description: |
        Poll this endpoint after `POST /slideshows` until `data.slideshow.status` is terminal.
        Terminal statuses are `completed`, `failed`, and `timed_out`.
        Non-terminal status is `generating`.

        Recommended polling behavior:
        - Poll every 2-5 seconds with backoff.
        - Stop polling when terminal.
        - If `timed_out`, inspect `data.slideshow.diagnostics` for timeout context.
      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"
  /posts:
    get:
      tags: [Posts]
      summary: List posts
      operationId: listPosts
      x-required-abilities: [slidestorm-api:read]
      security:
        - BearerToken: []
      parameters:
        - name: status
          in: query
          required: false
          description: |
            Aggregate lifecycle status.
            - `pending`: due-now pending posts (`scheduled_at <= now`)
            - `scheduled`: future pending posts (`scheduled_at > now`)
            - `uploaded`: all deliveries uploaded to TikTok inbox, awaiting manual publish in TikTok
            - `posted`: all deliveries posted/published
            - `failed`: all deliveries failed
            - `cancelled`: all deliveries cancelled
            - `partial`: mixed delivery 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"
    post:
      tags: [Posts]
      summary: Create a post from a completed slideshow
      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"
  /posts/{id}:
    get:
      tags: [Posts]
      summary: Get a post by id
      description: |
        Returns the current persisted aggregate post state and per-account delivery states.
        This endpoint does not force a provider sync by itself.
        Call `POST /posts/{id}/check-status` to fetch latest provider state and reconcile lifecycle fields.
      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"
    patch:
      tags: [Posts]
      summary: Update mutable post fields
      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"
    delete:
      tags: [Posts]
      summary: Delete 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"
  /posts/{id}/check-status:
    post:
      tags: [Posts]
      summary: Fetch provider status and sync post lifecycle
      description: |
        Fetches provider status for the post's `platform_publish_id`, then syncs post and
        delivery lifecycle fields.

        Interpretation notes:
        - `uploaded` means content reached TikTok inbox for manual review/publish and is not
          guaranteed to be publicly visible.
        - `posted` means published/completed.
        - `failed` may be recoverable; use `data.status.is_recoverable` to determine whether
          `POST /posts/{id}/retry` is applicable.
      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
        "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
  /posts/{id}/retry:
    post:
      tags: [Posts]
      summary: Retry failed or cancelled deliveries
      description: |
        Resets retryable deliveries (`failed` or `cancelled`) back to `pending` and re-queues them.
        When `account_ids` is omitted, all retryable deliveries are retried.
        If no retryable deliveries exist, returns `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"
components:
  securitySchemes:
    BearerToken:
      type: http
      scheme: bearer
      bearerFormat: Sanctum token
      description: |
        Bearer token with API abilities:
        - `slidestorm-api:read` for GET endpoints
        - `slidestorm-api:write` for POST/PATCH/DELETE endpoints
  parameters:
    PublicIdPath:
      name: id
      in: path
      required: true
      description: 26-character ULID public identifier.
      schema:
        $ref: "#/components/schemas/PublicId"
  responses:
    UnauthorizedError:
      description: Missing or invalid bearer token
      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: Token does not include required ability
      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 in caller scope
      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: Payload validation failed
      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.
    SlideshowNotReadyError:
      description: Slideshow is not completed yet
      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: Credits exhausted for slideshow generation
      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: |
        Slideshow generation lifecycle object.
        Status semantics:
        - `generating`: non-terminal, still processing.
        - `completed`: terminal, eligible for post creation.
        - `failed`: terminal, generation completed with unrecoverable failures.
        - `timed_out`: terminal, generation exceeded timeout threshold; inspect `diagnostics`.
      required:
        - id
        - title
        - status
        - created_at
        - generation_summary
        - diagnostics
        - is_postable
        - postability_reasons
        - slides
      properties:
        id:
          $ref: "#/components/schemas/PublicId"
        title:
          type: string
        status:
          type: string
          description: Slideshow lifecycle status.
          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
          items:
            type: string
        slides:
          type: array
          items:
            $ref: "#/components/schemas/SlideshowSlide"
    PostAccountState:
      type: object
      description: Per-target-account delivery state used to derive aggregate post status.
      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 lifecycle state for one account.
            - `uploaded`: sent to TikTok inbox for manual publish (not necessarily public yet)
            - `posted`: published/completed
          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: |
        Aggregate post lifecycle object. `status` is derived from delivery states and scheduling.
        See `accounts.states` for per-account outcomes.
      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]
        platform:
          type: string
          enum: [tiktok]
        status:
          type: string
          description: |
            Aggregate lifecycle status.
            - `pending`: queued or processing
            - `scheduled`: future pending post
            - `uploaded`: all deliveries uploaded to TikTok inbox for manual publish
            - `posted`: all deliveries published/completed
            - `failed`: all deliveries failed
            - `cancelled`: all deliveries cancelled
            - `partial`: mixed delivery outcomes
          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 provider status sync (`POST /posts/{id}/check-status`) can be performed.
        can_link_to_post:
          type: boolean
          description: True when a public post link is available (for example via `platform_post_id`/`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: |
        Provider-synced status snapshot returned by `POST /posts/{id}/check-status`.
        This snapshot combines provider state with Slidestorm 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: Current aggregate Slidestorm post status after sync.
          enum: [pending, scheduled, uploaded, posted, failed, cancelled, partial]
        provider_status:
          type: [string, "null"]
          description: Raw provider status value when available (for example TikTok workflow status).
        is_terminal:
          type: boolean
          description: True when no further status polling is needed for this lifecycle.
        is_recoverable:
          type: boolean
          description: True when retry is possible (for example failed/cancelled retryable deliveries exist).
        platform_post_id:
          type: [string, "null"]
        error_code:
          type: [string, "null"]
        error_message:
          type: [string, "null"]
        progress:
          type: object
          description: Opaque progress payload for UX hints (stage/title/message/progress 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
          properties:
            post_mode:
              type: string
              enum: [DIRECT_POST, MEDIA_UPLOAD]
            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: |
        Conditional requirements:
        - if `background_option=generate`, provide one of `image_style_id` or `custom_image_style`
        - if `background_option=library`, provide `collection_id`
        - if `background_option=single`, provide `single_image_id`
    CreatePostRequest:
      type: object
      additionalProperties: false
      required: [caption, slideshow_id, accounts]
      properties:
        caption:
          type: string
          minLength: 1
          maxLength: 4000
        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 reference a completed slideshow in caller scope.
        If `external_id` is reused with a different payload, the API returns
        `409` with `error_code=external_id_reused_with_different_payload`.
    UpdatePostRequest:
      type: object
      additionalProperties: false
      properties:
        caption:
          type: string
          minLength: 1
          maxLength: 4000
        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: At least one editable field must be provided.
    RetryPostRequest:
      type: object
      additionalProperties: false
      description: |
        Optional account subset for retry.
        When omitted, the API retries all retryable deliveries for the post.
      properties:
        account_ids:
          type: array
          minItems: 1
          maxItems: 10
          items:
            $ref: "#/components/schemas/PublicId"
