openapi: 3.0.3
info:
  title: PublishQ API
  version: 1.0.0
  description: >-
    Schedule and publish content across your social platforms with a single API
    call.


    ## Quick start


    1. Go to **[Settings → API Keys](https://publishq.com/app/settings)** in the
    PublishQ dashboard and create a key with the scopes you need.

    2. Pass the key as a Bearer token: `Authorization: Bearer pq_live_...`

    3. List your connected accounts: `GET /accounts`

    4. Create a post: `POST /posts` with the account IDs and your content.


    ## Key concepts


    - A **Post** is a single object with an `accounts[]` array. Each entry
    represents the post on one connected social account.

    - **Media** is uploaded separately via `POST /media` and referenced by ID
    when creating posts.

    - **Workspaces** organize your connected accounts into groups.


    ## Rate limits


    Endpoints are rate-limited per API key as a safety net — for example, if a
    script accidentally sends the same request in a loop. If you get a `429`
    response, just wait for the time in the `Retry-After` header before
    retrying.
servers:
  - url: https://publishq.com/api/v1
tags:
  - name: Posts
    description: Create, update, list, retrieve, and delete posts across your
      connected social accounts.
  - name: Accounts
    description: Connected social accounts that can be used when creating posts.
  - name: Workspaces
    description: Workspaces used to organize connected social accounts.
  - name: Media
    description: Uploaded or generated media files that can be attached to posts.
components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: "PublishQ API key sent as `Authorization: Bearer pq_live_...`."
  schemas:
    ApiError:
      type: object
      properties:
        error:
          type: object
          properties:
            title:
              type: string
              example: Forbidden
            status:
              type: integer
              example: 403
            code:
              $ref: "#/components/schemas/ApiErrorCode"
            message:
              type: string
              example: "The API key does not have the required scope: posts:write"
            instance:
              type: string
              example: /api/v1/posts
            details:
              type: object
              additionalProperties:
                type: object
          required:
            - title
            - status
            - code
            - message
      required:
        - error
    ApiErrorCode:
      type: string
      enum:
        - missing_auth
        - invalid_auth_format
        - invalid_api_key
        - api_key_expired
        - insufficient_scope
        - validation_failed
        - route_not_found
        - internal_server_error
        - rate_limit_exceeded
        - post_not_found
        - post_forbidden
        - post_not_editable
        - media_not_found
        - media_in_use
        - storage_limit_exceeded
        - free_media_limit_exceeded
        - free_pending_media_limit_exceeded
        - file_required
        - file_too_large
        - unsupported_file_type
        - unsupported_media_type
        - invalid_file_extension
        - social_accounts_not_found
        - social_accounts_expired
        - platform_validation_failed
        - posting_limits_exceeded
        - scheduled_at_required
        - invalid_scheduled_at
        - scheduled_at_too_soon
        - reschedulable_posts_not_found
        - workspace_not_found
        - workspace_forbidden
        - workspace_owner_required
        - workspace_account_not_found
        - workspace_account_conflict
        - workspace_member_not_found
        - workspace_member_conflict
        - workspace_invitation_invalid
        - workspace_invitation_expired
      example: insufficient_scope
    Pagination:
      type: object
      properties:
        page:
          type: integer
          example: 1
          description: Current page number (starting from 1).
        limit:
          type: integer
          example: 20
          description: Maximum number of items returned per page.
        total:
          type: integer
          example: 100
          description: Total number of matching items.
        totalPages:
          type: integer
          example: 5
          description: Total number of pages for the current limit.
        hasNext:
          type: boolean
          example: true
          description: Whether another page exists after the current one.
        hasPrev:
          type: boolean
          example: false
          description: Whether another page exists before the current one.
      required:
        - page
        - limit
        - total
        - totalPages
        - hasNext
        - hasPrev
    BlueskyPlatformSpecificSettings:
      type: object
      properties: {}
      additionalProperties: false
      title: BlueskyPlatformSpecificSettings
      description: Bluesky-specific publishing settings. No settings are currently
        available.
    InstagramPlatformSpecificSettings:
      type: object
      properties:
        contentType:
          type: string
          enum:
            - story
            - reels
          description: Set to "story" for ephemeral content that disappears after 24
            hours. Set to "reels" to explicitly publish as a Reel. Omit for a
            regular feed post or carousel — single videos are automatically
            published as Reels.
        shareToFeed:
          type: boolean
          description: Reels only. When true (default), the reel can appear in both the
            Feed and Reels tabs. When false, it only appears in the Reels tab.
            Neither value guarantees placement — Instagram's algorithm decides
            based on eligibility and relevance.
        trialParams:
          type: object
          properties:
            graduationStrategy:
              type: string
              enum:
                - MANUAL
                - SS_PERFORMANCE
              description: "MANUAL: the trial reel can be graduated (converted to a public
                reel) manually in the Instagram app. SS_PERFORMANCE: Instagram
                automatically graduates the reel if it performs well with
                non-followers."
          required:
            - graduationStrategy
          title: InstagramTrialParams
          description: Reels only. Publish as a trial reel — shown only to non-followers
            first. Instagram requires a public account with at least 200
            followers and rejects the post otherwise.
        audioName:
          type: string
          description: Reels only. Custom name for the audio track of your reel. Can only
            be set once — either at creation time or later from the audio page
            in the Instagram app.
      additionalProperties: false
      title: InstagramPlatformSpecificSettings
      description: Instagram-specific post settings. All fields are optional — omit
        the entire object for a regular feed post or carousel.
    FacebookPlatformSpecificSettings:
      type: object
      properties:
        contentType:
          type: string
          enum:
            - story
          description: Set to "story" for ephemeral content that disappears after 24
            hours. Omit for a regular post — single videos are automatically
            published as Reels (Facebook no longer has a separate video format).
      additionalProperties: false
      title: FacebookPlatformSpecificSettings
      description: Facebook-specific post settings. All fields are optional — omit the
        entire object for a regular post.
    InstagramTrialParams:
      type: object
      properties:
        graduationStrategy:
          type: string
          enum:
            - MANUAL
            - SS_PERFORMANCE
          description: "MANUAL: the trial reel can be graduated (converted to a public
            reel) manually in the Instagram app. SS_PERFORMANCE: Instagram
            automatically graduates the reel if it performs well with
            non-followers."
      required:
        - graduationStrategy
      title: InstagramTrialParams
      description: Trial reel graduation settings. Trial reels are shown only to
        non-followers first. If the reel performs well (SS_PERFORMANCE) or you
        manually approve it (MANUAL), it gets promoted to a regular reel visible
        to followers.
    YouTubePlatformSpecificSettings:
      type: object
      properties:
        description:
          type: string
          maxLength: 5000
          description: Video description. Max 5000 characters. Shown below the video title
            on the watch page.
        privacyStatus:
          type: string
          enum:
            - public
            - unlisted
            - private
          description: Video privacy status. Defaults to public. Unverified API projects
            are restricted to private.
        madeForKids:
          type: boolean
          description: Declare the video as made for kids (COPPA). When true, personalized
            ads are disabled and some features (comments, notifications) are
            turned off.
        containsSyntheticMedia:
          type: boolean
          description: Disclose that the video contains realistic altered or synthetic
            (AI-generated) content.
        categoryId:
          type: string
          enum:
            - "1"
            - "2"
            - "10"
            - "15"
            - "17"
            - "19"
            - "20"
            - "22"
            - "23"
            - "24"
            - "25"
            - "26"
            - "27"
            - "28"
            - "29"
          description: 'YouTube video category. Defaults to "22" (People & Blogs). Valid
            IDs: 1 (Film & Animation), 2 (Autos & Vehicles), 10 (Music), 15
            (Pets & Animals), 17 (Sports), 19 (Travel & Events), 20 (Gaming), 22
            (People & Blogs), 23 (Comedy), 24 (Entertainment), 25 (News &
            Politics), 26 (Howto & Style), 27 (Education), 28 (Science &
            Technology), 29 (Nonprofits & Activism).'
        tags:
          type: array
          items:
            type: string
          description: Keyword tags for the video. Duplicates are removed. Total of all
            tags must be under 500 characters. Tags with spaces count as quoted
            (2 extra chars). Commas between tags also count.
        embeddable:
          type: boolean
          description: Whether the video can be embedded on other websites. Defaults to
            true.
        playlistId:
          type: string
          description: Playlist ID to add the video to after upload.
      additionalProperties: false
      title: YouTubePlatformSpecificSettings
      description: YouTube-specific upload settings, such as description and privacy.
    TikTokPlatformSpecificSettings:
      type: object
      properties:
        privacyLevel:
          type: string
          enum:
            - PUBLIC_TO_EVERYONE
            - FOLLOWER_OF_CREATOR
            - MUTUAL_FOLLOW_FRIENDS
            - SELF_ONLY
          description: Who can view this post. The value must match one of the options
            returned by the TikTok creator_info/query endpoint for the target
            account.
        disableComment:
          type: boolean
          description: If true, other TikTok users cannot comment on this post. Ignored if
            the creator has set comments to "No one" in their privacy settings.
        disableDuet:
          type: boolean
          description: If true, other TikTok users cannot create Duets with this post.
            Only applies to video posts. Ignored for photo posts and private
            accounts.
        disableStitch:
          type: boolean
          description: If true, other TikTok users cannot create Stitches with this post.
            Only applies to video posts. Ignored for photo posts and private
            accounts.
        brandContentToggle:
          type: boolean
          description: Set to true if this is a paid partnership promoting a third-party
            business. Required by TikTok for branded content disclosure.
        brandOrganicToggle:
          type: boolean
          description: Set to true if this post promotes the creator's own business.
        autoAddMusic:
          type: boolean
          description: Photo posts only. If true, TikTok automatically adds recommended
            background music to the photo carousel. Users can change the music
            later in the TikTok app.
        videoMadeWithAi:
          type: boolean
          description: Video posts only. Set to true if the video is AI-generated content.
            The video will be labeled with a "Creator labeled as AI-generated"
            tag.
        description:
          type: string
          maxLength: 4000
          description: Photo posts only. Additional description text (max 4000
            characters). Hashtags and mentions in the description are parsed by
            TikTok.
        photoCoverIndex:
          type: integer
          minimum: 0
          description: Photo posts only. Zero-based index of the image to use as the
            carousel cover. Defaults to 0 (first image).
          example: 0
      required:
        - privacyLevel
        - disableComment
        - disableDuet
        - disableStitch
        - brandContentToggle
        - brandOrganicToggle
      additionalProperties: false
      title: TikTokPlatformSpecificSettings
      description: TikTok-specific publishing settings. Privacy level and brand
        toggles are required for all posts. Duet/stitch settings only apply to
        video posts. autoAddMusic only applies to photo posts.
    LinkedInPlatformSpecificSettings:
      type: object
      properties:
        visibility:
          type: string
          enum:
            - CONNECTIONS
            - PUBLIC
          description: Who can see the post. PUBLIC (anyone on LinkedIn) or CONNECTIONS
            (only your connections). Defaults to PUBLIC. Only available for
            personal profiles — company pages are always public.
          example: PUBLIC
      additionalProperties: false
      title: LinkedInPlatformSpecificSettings
      description: LinkedIn-specific publishing settings, such as post visibility.
    ThreadsPlatformSpecificSettings:
      type: object
      properties:
        topicTag:
          type: string
          minLength: 1
          maxLength: 50
          description: "A topic tag for the post (without the # prefix). Max 50
            characters. Periods (.) and ampersands (&) are not allowed.
            Overrides any auto-detected topic from inline hashtags."
          example: photography
      additionalProperties: false
      title: ThreadsPlatformSpecificSettings
      description: Threads-specific publishing settings.
    TwitterPlatformSpecificSettings:
      type: object
      properties:
        communityId:
          type: string
          pattern: ^[0-9]{1,19}$
          description: Post to a specific X Community. The authenticated user must be a
            member of the community.
        shareWithFollowers:
          type: boolean
          description: When posting to a community, also share the post with your
            followers. Only applies when `communityId` is set.
        forSuperFollowersOnly:
          type: boolean
          description: Make this post exclusive to your subscribers (super followers).
        replySettings:
          type: string
          enum:
            - following
            - mentionedUsers
            - subscribers
            - verified
          description: "Control who can reply. `following`: only people you follow.
            `mentionedUsers`: only people mentioned in the post. `subscribers`:
            only your subscribers. `verified`: only verified users."
        quoteTweetId:
          type: string
          pattern: ^[0-9]{1,19}$
          description: ID of an existing tweet to quote. Mutually exclusive with poll and
            media.
        replyToTweetId:
          type: string
          pattern: ^[0-9]{1,19}$
          description: ID of an existing tweet to reply to. Your post will appear as a
            reply in that tweet's thread.
        poll:
          type: object
          properties:
            options:
              type: array
              items:
                type: string
                minLength: 1
                maxLength: 25
              minItems: 2
              maxItems: 4
              description: Poll choices. 2-4 options, max 25 characters each.
            durationMinutes:
              type: integer
              minimum: 5
              maximum: 10080
              description: How long the poll stays open, in minutes. Min 5 minutes, max 7 days
                (10080).
          required:
            - options
            - durationMinutes
          description: Create a poll. Mutually exclusive with media and `quoteTweetId`.
      additionalProperties: false
      title: TwitterPlatformSpecificSettings
      description: X (Twitter) specific post settings. All fields are optional.
    PostAccount:
      type: object
      properties:
        accountId:
          type: string
          format: uuid
          description: The connected social account identifier.
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        platform:
          $ref: "#/components/schemas/SocialPlatform"
        accountName:
          type: string
          description: Display name or username of the connected account.
          example: "@publishq"
        status:
          $ref: "#/components/schemas/PostStatus"
        externalUrl:
          type: string
          format: uri
          description: Link to the published post on the social platform. Present only
            after successful publishing.
          example: https://x.com/publishq/status/1234567890
        publishedAt:
          type: string
          format: date-time
          description: When this account's post was published.
          example: 2026-04-01T12:01:23Z
        errorMessage:
          type: string
          description: Error message from the platform if publishing failed for this
            account.
          example: Token expired. Please reconnect your Twitter account.
        postOverrides:
          $ref: "#/components/schemas/PostOverrides"
        platformSpecificSettings:
          $ref: "#/components/schemas/PlatformSpecificSettings"
      required:
        - accountId
        - platform
        - accountName
        - status
    SocialPlatform:
      type: string
      enum:
        - TWITTER
        - INSTAGRAM
        - FACEBOOK
        - LINKEDIN
        - YOUTUBE
        - TIKTOK
        - THREADS
        - BLUESKY
    PostStatus:
      type: string
      enum:
        - DRAFT
        - SCHEDULED
        - PUBLISHING
        - PUBLISHED
        - FAILED
    PostOverrides:
      type: object
      properties:
        content:
          type: string
          description: Custom text for this account, overriding the post-level content.
          example: Short version for Twitter — check the link in bio!
        thumbnailTimestamp:
          type: integer
          minimum: 0
          description: Video thumbnail timestamp in milliseconds. Applies to Instagram and
            TikTok.
          example: 5000
        mediaIds:
          type: array
          items:
            type: string
            format: uuid
          description: Replacement media for this account. Completely replaces the
            post-level media.
      additionalProperties: false
    PlatformSpecificSettings:
      anyOf:
        - type: object
          properties:
            description:
              type: string
              maxLength: 5000
              description: Video description. Max 5000 characters. Shown below the video title
                on the watch page.
            privacyStatus:
              type: string
              enum:
                - public
                - unlisted
                - private
              description: Video privacy status. Defaults to public. Unverified API projects
                are restricted to private.
            madeForKids:
              type: boolean
              description: Declare the video as made for kids (COPPA). When true, personalized
                ads are disabled and some features (comments, notifications) are
                turned off.
            containsSyntheticMedia:
              type: boolean
              description: Disclose that the video contains realistic altered or synthetic
                (AI-generated) content.
            categoryId:
              type: string
              enum:
                - "1"
                - "2"
                - "10"
                - "15"
                - "17"
                - "19"
                - "20"
                - "22"
                - "23"
                - "24"
                - "25"
                - "26"
                - "27"
                - "28"
                - "29"
              description: 'YouTube video category. Defaults to "22" (People & Blogs). Valid
                IDs: 1 (Film & Animation), 2 (Autos & Vehicles), 10 (Music), 15
                (Pets & Animals), 17 (Sports), 19 (Travel & Events), 20
                (Gaming), 22 (People & Blogs), 23 (Comedy), 24 (Entertainment),
                25 (News & Politics), 26 (Howto & Style), 27 (Education), 28
                (Science & Technology), 29 (Nonprofits & Activism).'
            tags:
              type: array
              items:
                type: string
              description: Keyword tags for the video. Duplicates are removed. Total of all
                tags must be under 500 characters. Tags with spaces count as
                quoted (2 extra chars). Commas between tags also count.
            embeddable:
              type: boolean
              description: Whether the video can be embedded on other websites. Defaults to
                true.
            playlistId:
              type: string
              description: Playlist ID to add the video to after upload.
          additionalProperties: false
          title: YouTubePlatformSpecificSettings
          description: YouTube-specific upload settings, such as description and privacy.
        - type: object
          properties:
            privacyLevel:
              type: string
              enum:
                - PUBLIC_TO_EVERYONE
                - FOLLOWER_OF_CREATOR
                - MUTUAL_FOLLOW_FRIENDS
                - SELF_ONLY
              description: Who can view this post. The value must match one of the options
                returned by the TikTok creator_info/query endpoint for the
                target account.
            disableComment:
              type: boolean
              description: If true, other TikTok users cannot comment on this post. Ignored if
                the creator has set comments to "No one" in their privacy
                settings.
            disableDuet:
              type: boolean
              description: If true, other TikTok users cannot create Duets with this post.
                Only applies to video posts. Ignored for photo posts and private
                accounts.
            disableStitch:
              type: boolean
              description: If true, other TikTok users cannot create Stitches with this post.
                Only applies to video posts. Ignored for photo posts and private
                accounts.
            brandContentToggle:
              type: boolean
              description: Set to true if this is a paid partnership promoting a third-party
                business. Required by TikTok for branded content disclosure.
            brandOrganicToggle:
              type: boolean
              description: Set to true if this post promotes the creator's own business.
            autoAddMusic:
              type: boolean
              description: Photo posts only. If true, TikTok automatically adds recommended
                background music to the photo carousel. Users can change the
                music later in the TikTok app.
            videoMadeWithAi:
              type: boolean
              description: Video posts only. Set to true if the video is AI-generated content.
                The video will be labeled with a "Creator labeled as
                AI-generated" tag.
            description:
              type: string
              maxLength: 4000
              description: Photo posts only. Additional description text (max 4000
                characters). Hashtags and mentions in the description are parsed
                by TikTok.
            photoCoverIndex:
              type: integer
              minimum: 0
              description: Photo posts only. Zero-based index of the image to use as the
                carousel cover. Defaults to 0 (first image).
              example: 0
          required:
            - privacyLevel
            - disableComment
            - disableDuet
            - disableStitch
            - brandContentToggle
            - brandOrganicToggle
          additionalProperties: false
          title: TikTokPlatformSpecificSettings
          description: TikTok-specific publishing settings. Privacy level and brand
            toggles are required for all posts. Duet/stitch settings only apply
            to video posts. autoAddMusic only applies to photo posts.
        - type: object
          properties:
            contentType:
              type: string
              enum:
                - story
                - reels
              description: Set to "story" for ephemeral content that disappears after 24
                hours. Set to "reels" to explicitly publish as a Reel. Omit for
                a regular feed post or carousel — single videos are
                automatically published as Reels.
            shareToFeed:
              type: boolean
              description: Reels only. When true (default), the reel can appear in both the
                Feed and Reels tabs. When false, it only appears in the Reels
                tab. Neither value guarantees placement — Instagram's algorithm
                decides based on eligibility and relevance.
            trialParams:
              type: object
              properties:
                graduationStrategy:
                  type: string
                  enum:
                    - MANUAL
                    - SS_PERFORMANCE
                  description: "MANUAL: the trial reel can be graduated (converted to a public
                    reel) manually in the Instagram app. SS_PERFORMANCE:
                    Instagram automatically graduates the reel if it performs
                    well with non-followers."
              required:
                - graduationStrategy
              title: InstagramTrialParams
              description: Reels only. Publish as a trial reel — shown only to non-followers
                first. Instagram requires a public account with at least 200
                followers and rejects the post otherwise.
            audioName:
              type: string
              description: Reels only. Custom name for the audio track of your reel. Can only
                be set once — either at creation time or later from the audio
                page in the Instagram app.
          additionalProperties: false
          title: InstagramPlatformSpecificSettings
          description: Instagram-specific post settings. All fields are optional — omit
            the entire object for a regular feed post or carousel.
        - type: object
          properties:
            contentType:
              type: string
              enum:
                - story
              description: Set to "story" for ephemeral content that disappears after 24
                hours. Omit for a regular post — single videos are automatically
                published as Reels (Facebook no longer has a separate video
                format).
          additionalProperties: false
          title: FacebookPlatformSpecificSettings
          description: Facebook-specific post settings. All fields are optional — omit the
            entire object for a regular post.
        - type: object
          properties:
            topicTag:
              type: string
              minLength: 1
              maxLength: 50
              description: "A topic tag for the post (without the # prefix). Max 50
                characters. Periods (.) and ampersands (&) are not allowed.
                Overrides any auto-detected topic from inline hashtags."
              example: photography
          additionalProperties: false
          title: ThreadsPlatformSpecificSettings
          description: Threads-specific publishing settings.
        - type: object
          properties:
            communityId:
              type: string
              pattern: ^[0-9]{1,19}$
              description: Post to a specific X Community. The authenticated user must be a
                member of the community.
            shareWithFollowers:
              type: boolean
              description: When posting to a community, also share the post with your
                followers. Only applies when `communityId` is set.
            forSuperFollowersOnly:
              type: boolean
              description: Make this post exclusive to your subscribers (super followers).
            replySettings:
              type: string
              enum:
                - following
                - mentionedUsers
                - subscribers
                - verified
              description: "Control who can reply. `following`: only people you follow.
                `mentionedUsers`: only people mentioned in the post.
                `subscribers`: only your subscribers. `verified`: only verified
                users."
            quoteTweetId:
              type: string
              pattern: ^[0-9]{1,19}$
              description: ID of an existing tweet to quote. Mutually exclusive with poll and
                media.
            replyToTweetId:
              type: string
              pattern: ^[0-9]{1,19}$
              description: ID of an existing tweet to reply to. Your post will appear as a
                reply in that tweet's thread.
            poll:
              type: object
              properties:
                options:
                  type: array
                  items:
                    type: string
                    minLength: 1
                    maxLength: 25
                  minItems: 2
                  maxItems: 4
                  description: Poll choices. 2-4 options, max 25 characters each.
                durationMinutes:
                  type: integer
                  minimum: 5
                  maximum: 10080
                  description: How long the poll stays open, in minutes. Min 5 minutes, max 7 days
                    (10080).
              required:
                - options
                - durationMinutes
              description: Create a poll. Mutually exclusive with media and `quoteTweetId`.
          additionalProperties: false
          title: TwitterPlatformSpecificSettings
          description: X (Twitter) specific post settings. All fields are optional.
        - type: object
          properties:
            visibility:
              type: string
              enum:
                - CONNECTIONS
                - PUBLIC
              description: Who can see the post. PUBLIC (anyone on LinkedIn) or CONNECTIONS
                (only your connections). Defaults to PUBLIC. Only available for
                personal profiles — company pages are always public.
              example: PUBLIC
          additionalProperties: false
          title: LinkedInPlatformSpecificSettings
          description: LinkedIn-specific publishing settings, such as post visibility.
        - type: object
          properties: {}
          additionalProperties: false
          title: BlueskyPlatformSpecificSettings
          description: Bluesky-specific publishing settings. No settings are currently
            available.
        - type: object
          properties: {}
          additionalProperties: true
      description: Platform-specific settings for this account. The accepted shape
        depends on the account platform.
    Post:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique post identifier.
          example: 9f8e7d6c-5b4a-3210-fedc-ba0987654321
        content:
          type: string
          description: The shared text content of the post.
          example: Just shipped our new API — schedule posts everywhere with one call 🚀
        status:
          $ref: "#/components/schemas/PostStatus"
        errorMessage:
          type: string
          description: Human-readable error summary when one or more accounts failed.
        mediaIds:
          type: array
          items:
            type: string
            format: uuid
          description: Media attached to this post.
        deletedMediaCount:
          type: integer
          minimum: 0
          description: How many media were attached to this post but have since been
            deleted (rendered as placeholders). Lets a client tell a post that
            HAD media apart from one posted with none.
        scheduledAt:
          type: string
          format: date-time
          description: When the post is scheduled to be published.
          example: 2026-04-01T12:00:00Z
        publishedAt:
          type: string
          format: date-time
          description: When the post was published. Present only when all accounts are
            published.
        createdAt:
          type: string
          format: date-time
          description: When the post was created.
          example: 2026-03-30T10:00:00Z
        accounts:
          type: array
          items:
            $ref: "#/components/schemas/PostAccount"
          description: Individual account entries. Each represents the post on one
            connected social account.
      required:
        - id
        - content
        - status
        - createdAt
        - accounts
    PostsResponse:
      type: object
      properties:
        posts:
          type: array
          items:
            $ref: "#/components/schemas/Post"
        pagination:
          $ref: "#/components/schemas/Pagination"
      required:
        - posts
        - pagination
    CreatePostRequest:
      type: object
      properties:
        content:
          type: string
          default: ""
          description: The text content of the post, shared across all accounts unless
            overridden. Optional when media is attached (platform-dependent).
          example: Just shipped our new API — schedule posts everywhere with one call 🚀
        mediaIds:
          type: array
          items:
            type: string
            format: uuid
          description: Media to attach to the post. Shared across all accounts unless an
            account provides its own mediaIds in postOverrides.
        thumbnailTimestamp:
          type: integer
          minimum: 0
          description: Default video thumbnail timestamp in milliseconds for Instagram and
            TikTok. Can be overridden per account.
          example: 0
        accounts:
          type: array
          items:
            $ref: "#/components/schemas/CreatePostAccount"
          minItems: 1
          description: Accounts to post to. Each entry can optionally include content
            overrides and platform-specific settings.
        scheduledAt:
          type: string
          format: date-time
          description: Schedule the post for a future date. If omitted and publishNow is
            false or absent, the post is saved as a draft.
          example: 2026-04-01T12:00:00Z
        publishNow:
          type: boolean
          description: Publish immediately. Cannot be combined with scheduledAt.
      required:
        - accounts
      additionalProperties: false
      description: Create a post across one or more connected social accounts.
    CreatePostAccount:
      type: object
      properties:
        accountId:
          type: string
          format: uuid
          description: ID of a connected social account to post to.
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        postOverrides:
          allOf:
            - $ref: "#/components/schemas/PostOverrides"
            - description: Override the post-level content or media for this specific account.
        platformSpecificSettings:
          allOf:
            - $ref: "#/components/schemas/PlatformSpecificSettings"
            - description: Platform-specific settings. The accepted shape depends on the
                account platform.
      required:
        - accountId
      additionalProperties: false
    UpdatePostRequest:
      type: object
      properties:
        content:
          type: string
          description: New shared text content for the post.
          example: Updated launch post copy with the final CTA and a cleaner hook.
        scheduledAt:
          type: string
          format: date-time
          description: New scheduled date for the post. Must be at least 1 minute in the
            future.
          example: 2026-04-15T14:00:00Z
        mediaIds:
          type: array
          items:
            type: string
            format: uuid
          description: Full replacement for the post media. If omitted, the existing media
            stays unchanged. If provided, it replaces the current media across
            all accounts in the post. Send an empty array to remove all media
            from the post.
        thumbnailTimestamp:
          type: integer
          minimum: 0
          description: New video thumbnail timestamp in milliseconds for Instagram and
            TikTok media. Does not change which media files are attached.
            Combine with mediaIds if you want to replace media and set the new
            thumbnail timestamp in the same request.
          example: 3500
        accounts:
          type: array
          items:
            $ref: "#/components/schemas/CreatePostAccount"
          minItems: 1
          description: Accounts whose content overrides or platform-specific settings
            should be updated, plus any additional connected accounts to add to
            the post. Existing accounts omitted from this array remain unchanged
            and must be removed through the account-specific delete endpoint.
        publishNow:
          type: boolean
          description: Publish an existing draft immediately. Cannot be combined with
            scheduledAt.
      description: "Update a draft or scheduled post. Published and publishing posts
        cannot be updated. Media behavior is explicit: omit mediaIds to keep
        existing media unchanged, provide mediaIds to replace all current media,
        or provide an empty mediaIds array to remove all media."
    SuccessMessage:
      type: object
      properties:
        deleted:
          type: boolean
          enum:
            - true
        id:
          type: string
          format: uuid
      required:
        - deleted
        - id
    Account:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique social account identifier in PublishQ.
          example: f6370ca1-1f47-4f68-be72-c1206913b326
        platform:
          allOf:
            - $ref: "#/components/schemas/SocialPlatform"
            - description: Social platform that this connected account belongs to.
              example: YOUTUBE
        platformName:
          type: string
          nullable: true
          description: Human-friendly account name returned by the social platform.
          example: PublishQ
        platformUsername:
          type: string
          description: Primary username or handle on the connected social platform.
          example: "@publishq"
        platformUserId:
          type: string
          description: Unique user or channel identifier provided by the social platform.
          example: UCyAKME2KZyPuoEqfofNFy4Q
        platformPicture:
          type: string
          nullable: true
          description: Profile image URL returned by the connected social platform, if
            available.
          example: https://cdn.publishq.com/uploads/avatars/f6370ca1-1f47-4f68-be72-c1206913b326.jpg
        workspaceId:
          type: string
          nullable: true
          format: uuid
          description: Workspace currently associated with the account, if one exists.
          example: 4c57ad21-7dc1-4f0b-a8f6-c7b3fdd8c9e7
      required:
        - id
        - platform
        - platformUsername
        - platformUserId
    AccountsResponse:
      type: object
      properties:
        accounts:
          type: array
          items:
            $ref: "#/components/schemas/Account"
          description: Connected social accounts visible to the authenticated API key.
      required:
        - accounts
    Workspace:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique workspace identifier in PublishQ.
          example: 4c57ad21-7dc1-4f0b-a8f6-c7b3fdd8c9e7
        name:
          type: string
          description: Workspace name shown in the PublishQ UI.
          example: Personal Brand
        description:
          type: string
          nullable: true
          description: Optional workspace description.
          example: The home base for every story worth telling.
        color:
          type: string
          description: Workspace accent color in hex format.
          example: "#2563eb"
        accountCount:
          type: integer
          description: Number of social accounts assigned to this workspace.
          example: 5
        createdAt:
          type: string
          format: date-time
          description: When the workspace was created.
      required:
        - id
        - name
        - color
        - accountCount
        - createdAt
    CreateWorkspaceRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 50
          description: Workspace name shown in PublishQ.
        description:
          type: string
          maxLength: 500
          description: Optional short description of the workspace.
        color:
          type: string
          pattern: ^#[0-9A-Fa-f]{6}$
          default: "#6366f1"
          description: Workspace accent color as a hex string like `#6366f1`.
      required:
        - name
      description: Create a new workspace.
    WorkspacesResponse:
      type: object
      properties:
        workspaces:
          type: array
          items:
            $ref: "#/components/schemas/Workspace"
          description: Workspaces visible to the authenticated API key owner.
      required:
        - workspaces
    Media:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique media identifier in PublishQ.
          example: f6370ca1-1f47-4f68-be72-c1206913b326
        type:
          $ref: "#/components/schemas/MediaType"
        url:
          type: string
          format: uri
          description: Direct URL to the uploaded or generated media file.
          example: https://cdn.publishq.com/uploads/images/f6370ca1-1f47-4f68-be72-c1206913b326.png
        source:
          $ref: "#/components/schemas/MediaSource"
        created_at:
          type: string
          format: date-time
          description: When the media item was created in PublishQ.
          example: 2026-03-28T16:25:41.000Z
        usageCount:
          type: integer
          description: Number of posts this media item was used in.
          example: 3
      required:
        - id
        - type
        - url
        - source
        - created_at
        - usageCount
    MediaType:
      type: string
      enum:
        - IMAGE
        - VIDEO
      description: Media file type.
      example: IMAGE
    MediaSource:
      type: string
      enum:
        - UPLOAD
        - GENERATED
        - AI_GENERATED
      description: "How the media was created: uploaded by the user, generated inside
        PublishQ, or AI-generated inside PublishQ."
      example: UPLOAD
    MediaListResponse:
      type: object
      properties:
        media:
          type: array
          items:
            $ref: "#/components/schemas/Media"
        pagination:
          $ref: "#/components/schemas/Pagination"
      required:
        - media
        - pagination
    UploadMediaRequest:
      type: object
      properties:
        file:
          type: string
          format: binary
      required:
        - file
    TikTokCreatorInfoResponse:
      type: object
      properties:
        privacyLevelOptions:
          type: array
          items:
            type: string
          description: Privacy levels available for this account. Use one of these values
            as `privacyLevel` in `platformSpecificSettings`.
          example:
            - PUBLIC_TO_EVERYONE
            - MUTUAL_FOLLOW_FRIENDS
            - SELF_ONLY
        commentDisabled:
          type: boolean
          description: True if the creator has disabled comments at the account level.
        duetDisabled:
          type: boolean
          description: True if the creator has disabled Duets at the account level.
        stitchDisabled:
          type: boolean
          description: True if the creator has disabled Stitches at the account level.
        maxVideoPostDurationSec:
          type: number
          description: Maximum video duration in seconds this creator can post (typically
            180, 300, or 600).
          example: 300
      required:
        - privacyLevelOptions
        - commentDisabled
        - duetDisabled
        - stitchDisabled
        - maxVideoPostDurationSec
    ApiKeyScope:
      type: string
      enum:
        - posts:read
        - posts:write
        - accounts:read
        - workspaces:read
        - workspaces:write
        - media:read
        - media:write
  parameters: {}
paths:
  /posts:
    get:
      operationId: listPosts
      tags:
        - Posts
      summary: List posts
      description: Returns a paginated list of posts.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: integer
            minimum: 0
            exclusiveMinimum: true
            default: 1
            example: 1
            description: Page number (starts from 1).
          required: false
          description: Page number (starts from 1).
          name: page
          in: query
        - schema:
            type: integer
            minimum: 0
            exclusiveMinimum: true
            maximum: 100
            default: 20
            example: 20
            description: Page size. Max 100.
          required: false
          description: Page size. Max 100.
          name: limit
          in: query
      responses:
        "200":
          description: Posts retrieved successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostsResponse"
              examples:
                mixedStatuses:
                  summary: List with posts in various states
                  value:
                    posts:
                      - &a5
                        id: 2b3c4d5e-6f7a-8901-bcde-f12345678901
                        content: Check out our latest product update!
                        status: SCHEDULED
                        mediaIds:
                          - f6a7b8c9-d0e1-2345-6789-abcdef012345
                        scheduledAt: 2026-04-05T14:00:00Z
                        createdAt: 2026-04-01T08:30:00Z
                        accounts:
                          - accountId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                            platform: TWITTER
                            accountName: "@publishq"
                            status: SCHEDULED
                            postOverrides:
                              content: New product update just dropped 🔥 Thread below 👇
                          - accountId: b2c3d4e5-f6a7-8901-bcde-f12345678901
                            platform: YOUTUBE
                            accountName: PublishQ Channel
                            status: SCHEDULED
                            postOverrides:
                              content: Product Update April 2026
                            platformSpecificSettings:
                              description: >-
                                Full walkthrough of our latest features.
                                Timestamps below.


                                0:00 Intro

                                0:45 New API

                                2:30 Dashboard updates
                              privacyStatus: public
                          - accountId: c3d4e5f6-a7b8-9012-cdef-123456789012
                            platform: TIKTOK
                            accountName: publishq
                            status: SCHEDULED
                            postOverrides:
                              thumbnailTimestamp: 3500
                            platformSpecificSettings:
                              privacyLevel: PUBLIC_TO_EVERYONE
                              disableComment: false
                              disableDuet: false
                              disableStitch: false
                              brandContentToggle: false
                              brandOrganicToggle: false
                          - accountId: d4e5f6a7-b8c9-0123-def0-1234567890ab
                            platform: INSTAGRAM
                            accountName: publishq_official
                            status: SCHEDULED
                            platformSpecificSettings:
                              postType: REEL
                          - accountId: e5f6a7b8-c9d0-1234-5678-9abcdef01234
                            platform: BLUESKY
                            accountName: "@publishq.bsky.social"
                            status: SCHEDULED
                      - &a3
                        id: 9f8e7d6c-5b4a-3210-fedc-ba0987654321
                        content: Just shipped our new API — schedule posts everywhere with one call 🚀
                        status: PUBLISHED
                        mediaIds:
                          - d4c3b2a1-0987-6543-21fe-dcba09876543
                        publishedAt: 2026-04-01T12:01:23Z
                        createdAt: 2026-03-30T10:00:00Z
                        accounts:
                          - accountId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                            platform: TWITTER
                            accountName: "@publishq"
                            status: PUBLISHED
                            externalUrl: https://x.com/publishq/status/1234567890
                            publishedAt: 2026-04-01T12:01:05Z
                          - accountId: b2c3d4e5-f6a7-8901-bcde-f12345678901
                            platform: LINKEDIN
                            accountName: PublishQ Inc.
                            status: PUBLISHED
                            externalUrl: https://www.linkedin.com/feed/update/urn:li:share:7654321
                            publishedAt: 2026-04-01T12:01:23Z
                      - &a4
                        id: 1a2b3c4d-5e6f-7890-abcd-ef1234567890
                        content: Happy holidays from the team! 🎄
                        status: FAILED
                        error: "Publishing failed on 1 account: @oldhandle (Twitter). Check
                          accounts[].errorMessage for details."
                        mediaIds:
                          - e5f6a7b8-c9d0-1234-5678-9abcdef01234
                        createdAt: 2026-03-30T09:00:00Z
                        accounts:
                          - accountId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                            platform: TWITTER
                            accountName: "@oldhandle"
                            status: FAILED
                            errorMessage: Token expired. Please reconnect your Twitter account.
                          - accountId: b2c3d4e5-f6a7-8901-bcde-f12345678901
                            platform: INSTAGRAM
                            accountName: publishq_official
                            status: PUBLISHED
                            externalUrl: https://www.instagram.com/p/ABC123/
                            publishedAt: 2026-04-01T12:01:10Z
                            platformSpecificSettings:
                              postType: REEL
                          - accountId: c3d4e5f6-a7b8-9012-cdef-123456789012
                            platform: LINKEDIN
                            accountName: PublishQ Inc.
                            status: PUBLISHED
                            externalUrl: https://www.linkedin.com/feed/update/urn:li:share:9876543
                            publishedAt: 2026-04-01T12:01:15Z
                    pagination:
                      page: 1
                      limit: 20
                      total: 3
                      totalPages: 1
                      hasNext: false
                      hasPrev: false
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: &a1
                  value:
                    error:
                      title: Unauthorized
                      status: 401
                      code: missing_auth
                      message: Missing Authorization header
                      instance: /api/v1/posts
                invalidApiKey: &a2
                  value:
                    error:
                      title: Unauthorized
                      status: 401
                      code: invalid_api_key
                      message: Invalid API key
                      instance: /api/v1/posts
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/posts
                      details:
                        retryAfter: 60
    post:
      operationId: createPost
      tags:
        - Posts
      summary: Create posts
      description: Creates a post for one or more connected accounts.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePostRequest"
            examples:
              simpleSchedule:
                summary: Schedule to Twitter and LinkedIn
                value:
                  content: Just shipped our new API — schedule posts everywhere with one call 🚀
                  scheduledAt: 2026-04-01T12:00:00Z
                  mediaIds:
                    - d4c3b2a1-0987-6543-21fe-dcba09876543
                  accounts:
                    - accountId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                    - accountId: b2c3d4e5-f6a7-8901-bcde-f12345678901
              withOverridesAndTikTok:
                summary: Post with per-account overrides and TikTok settings
                value:
                  content: Check out our latest product update!
                  publishNow: true
                  mediaIds:
                    - f6a7b8c9-d0e1-2345-6789-abcdef012345
                  thumbnailTimestamp: 0
                  accounts:
                    - accountId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                      postOverrides:
                        content: New product update just dropped 🔥 Thread below 👇
                    - accountId: b2c3d4e5-f6a7-8901-bcde-f12345678901
                      postOverrides:
                        content: Product Update April 2026
                      platformSpecificSettings:
                        description: Full walkthrough of our latest features.
                        privacyStatus: public
                    - accountId: c3d4e5f6-a7b8-9012-cdef-123456789012
                      postOverrides:
                        thumbnailTimestamp: 3500
                      platformSpecificSettings:
                        privacyLevel: PUBLIC_TO_EVERYONE
                        disableComment: false
                        disableDuet: false
                        disableStitch: false
                        brandContentToggle: false
                        brandOrganicToggle: false
              draft:
                summary: Save as draft (no scheduledAt, no publishNow)
                value:
                  content: Work in progress — will finalize later
                  accounts:
                    - accountId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
      responses:
        "201":
          description: Posts created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Post"
        "400":
          description: Validation or account/media ownership error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                validationFailed:
                  value:
                    error:
                      title: Bad Request
                      status: 400
                      code: validation_failed
                      message: Request validation failed
                      instance: /api/v1/posts
                      details:
                        errors:
                          - path: text
                            message: "Invalid input: expected string, received undefined"
                            code: invalid_type
                unknownField:
                  value:
                    error:
                      title: Bad Request
                      status: 400
                      code: validation_failed
                      message: Request validation failed
                      instance: /api/v1/posts
                      details:
                        errors:
                          - path: accounts.0.postOverrides
                            message: 'Unrecognized key: "extraField"'
                            code: unrecognized_keys
                missingTikTokSettings:
                  value:
                    error:
                      title: Bad Request
                      status: 400
                      code: platform_validation_failed
                      message: Validation failed for some platforms
                      instance: /api/v1/posts
                      details:
                        validationErrors:
                          - platform: TIKTOK
                            accountId: acc_123
                            accountName: "@publishq"
                            errors:
                              - platformSpecificSettings.disableComment is
                                required for TikTok
                              - platformSpecificSettings.disableDuet is required
                                for TikTok
                              - platformSpecificSettings.disableStitch is
                                required for TikTok
                              - platformSpecificSettings.brandContentToggle is
                                required for TikTok
                              - platformSpecificSettings.brandOrganicToggle is
                                required for TikTok
                socialAccountsNotFound:
                  value:
                    error:
                      title: Bad Request
                      status: 400
                      code: social_accounts_not_found
                      message: Some social accounts were not found or are inactive
                      instance: /api/v1/posts
                      details:
                        missingAccountIds:
                          - acc_123
                mediaNotFound:
                  value:
                    error:
                      title: Bad Request
                      status: 400
                      code: media_not_found
                      message: Some media files were not found or do not belong to you
                      instance: /api/v1/posts
                      details:
                        missingMediaIds:
                          - media_123
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "402":
          description: Plan limits exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                postingLimitsExceeded: &a7
                  value:
                    error:
                      title: Payment Required
                      status: 402
                      code: posting_limits_exceeded
                      message: Posting limits exceeded
                      instance: /api/v1/posts
                      details:
                        limitErrors:
                          - Monthly scheduled post limit reached
                        upgradeRequired: true
        "403":
          description: API key lacks the required scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                insufficientScope:
                  value:
                    error:
                      title: Forbidden
                      status: 403
                      code: insufficient_scope
                      message: "Missing required scope: posts:write"
                      instance: /api/v1/posts
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/posts
                      details:
                        retryAfter: 60
  /posts/{id}:
    get:
      operationId: getPost
      tags:
        - Posts
      summary: Get post
      description: Get one post by its PublishQ post ID. Returns the post details and
        the connected account metadata needed to identify where it belongs.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
            example: 1b5c779b-d7de-4e4e-bf55-95b9317b6d93
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Post retrieved successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Post"
              examples:
                allPublished:
                  summary: All accounts published successfully
                  value: *a3
                partiallyFailed:
                  summary: One account failed, others published
                  value: *a4
                withOverridesAndTikTok:
                  summary: Post with per-account overrides and TikTok settings
                  value: *a5
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "404":
          description: Post not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                postNotFound: &a6
                  value:
                    error:
                      title: Not Found
                      status: 404
                      code: post_not_found
                      message: Post not found
                      instance: /api/v1/posts
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/posts/1b5c779b-d7de-4e4e-bf55-95b9317b6d93
                      details:
                        retryAfter: 60
    delete:
      operationId: deletePost
      tags:
        - Posts
      summary: Delete post
      description: Deletes a scheduled or draft post by its ID. Returns no body on success.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
            example: 1b5c779b-d7de-4e4e-bf55-95b9317b6d93
          required: true
          name: id
          in: path
      responses:
        "204":
          description: Post deleted successfully
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "403":
          description: API key lacks the required scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                insufficientScope:
                  value:
                    error:
                      title: Forbidden
                      status: 403
                      code: insufficient_scope
                      message: "Missing required scope: posts:write"
                      instance: /api/v1/posts/1b5c779b-d7de-4e4e-bf55-95b9317b6d93
        "404":
          description: Post not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                postNotFound: *a6
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/posts/1b5c779b-d7de-4e4e-bf55-95b9317b6d93
                      details:
                        retryAfter: 60
    patch:
      operationId: updatePost
      tags:
        - Posts
      summary: Update post
      description: Update a draft or scheduled post. You can change the content, the
        scheduled date, or both. Published and publishing posts cannot be
        updated.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
            example: 1b5c779b-d7de-4e4e-bf55-95b9317b6d93
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePostRequest"
            examples:
              updateContentAndSchedule:
                summary: Update content and reschedule
                value:
                  content: Updated copy with a stronger hook and cleaner CTA 🚀
                  scheduledAt: 2026-04-15T14:00:00Z
                  mediaIds:
                    - d4c3b2a1-0987-6543-21fe-dcba09876543
                  thumbnailTimestamp: 5500
              updateContentOnly:
                summary: Update content only
                value:
                  content: Fixed a typo in the launch post
              updateContentKeepMedia:
                summary: Update content and keep existing media unchanged
                value:
                  content: Same media, updated copy only
              rescheduleOnly:
                summary: Reschedule only
                value:
                  scheduledAt: 2026-04-20T09:00:00Z
              replaceAllMedia:
                summary: Replace all current media with a new set
                value:
                  mediaIds:
                    - e5f6a7b8-c9d0-1234-5678-9abcdef01234
                    - f6a7b8c9-d0e1-2345-6789-abcdef012345
              replaceSingleMediaAndThumbnail:
                summary: Replace all current media with one file and set thumbnail
                value:
                  mediaIds:
                    - e5f6a7b8-c9d0-1234-5678-9abcdef01234
                  thumbnailTimestamp: 3500
              removeAllMedia:
                summary: Remove all media from the post
                value:
                  mediaIds: []
              updateThumbnailOnly:
                summary: Update thumbnail timestamp only and keep current media
                value:
                  thumbnailTimestamp: 2500
      responses:
        "200":
          description: Post updated successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Post"
              examples:
                updated:
                  summary: Post after update
                  value:
                    id: 1b5c779b-d7de-4e4e-bf55-95b9317b6d93
                    content: Updated copy with a stronger hook and cleaner CTA 🚀
                    status: SCHEDULED
                    mediaIds:
                      - d4c3b2a1-0987-6543-21fe-dcba09876543
                    thumbnailTimestamp: 5500
                    scheduledAt: 2026-04-15T14:00:00Z
                    createdAt: 2026-03-30T10:00:00Z
                    accounts:
                      - accountId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                        platform: TWITTER
                        accountName: "@publishq"
                        status: SCHEDULED
                      - accountId: b2c3d4e5-f6a7-8901-bcde-f12345678901
                        platform: LINKEDIN
                        accountName: PublishQ
                        status: SCHEDULED
        "400":
          description: Invalid or missing scheduledAt
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "402":
          description: Plan limits exceeded for the target date
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                postingLimitsExceeded: *a7
        "403":
          description: API key lacks the required scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                insufficientScope:
                  value:
                    error:
                      title: Forbidden
                      status: 403
                      code: insufficient_scope
                      message: "Missing required scope: posts:write"
                      instance: /api/v1/posts/1b5c779b-d7de-4e4e-bf55-95b9317b6d93
        "404":
          description: Post not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                postNotFound: *a6
        "409":
          description: Post cannot be updated (already published or publishing)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/posts/1b5c779b-d7de-4e4e-bf55-95b9317b6d93
                      details:
                        retryAfter: 60
  /posts/{id}/accounts/{accountId}:
    delete:
      operationId: removePostAccount
      tags:
        - Posts
      summary: Remove account from post
      description: Removes one account from an existing post so it will no longer be
        published there. Other accounts in the same post are not changed.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
            example: 1b5c779b-d7de-4e4e-bf55-95b9317b6d93
          required: true
          name: id
          in: path
        - schema:
            type: string
            format: uuid
            example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
          required: true
          name: accountId
          in: path
      responses:
        "204":
          description: Account removed from post successfully
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "403":
          description: API key lacks the required scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                insufficientScope:
                  value:
                    error:
                      title: Forbidden
                      status: 403
                      code: insufficient_scope
                      message: "Missing required scope: posts:write"
                      instance: /api/v1/posts/1b5c779b-d7de-4e4e-bf55-95b9317b6d93/accounts/a1b2c3d4-e5f6-7890-abcd-ef1234567890
        "404":
          description: Post or account entry not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                postNotFound: *a6
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/posts/1b5c779b-d7de-4e4e-bf55-95b9317b6d93/accounts/a1b2c3d4-e5f6-7890-abcd-ef1234567890
                      details:
                        retryAfter: 60
  /accounts:
    get:
      operationId: listAccounts
      tags:
        - Accounts
      summary: List accounts
      description: List connected social accounts for the authenticated API key owner.
        You can filter by workspace and by one or more platforms.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
            description: Return only accounts assigned to the specified workspace. If
              omitted, accounts from all workspaces are returned.
            example: 4c57ad21-7dc1-4f0b-a8f6-c7b3fdd8c9e7
          required: false
          description: Return only accounts assigned to the specified workspace. If
            omitted, accounts from all workspaces are returned.
          name: workspaceId
          in: query
        - schema:
            type: array
            items:
              $ref: "#/components/schemas/SocialPlatform"
            description: Return only accounts connected to the specified social platforms.
              Repeat the parameter to apply OR logic, for example
              `?platform=twitter&platform=instagram`.
            example:
              - TWITTER
              - INSTAGRAM
          required: false
          description: Return only accounts connected to the specified social platforms.
            Repeat the parameter to apply OR logic, for example
            `?platform=twitter&platform=instagram`.
          name: platform
          in: query
      responses:
        "200":
          description: Accounts retrieved successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountsResponse"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/accounts
                      details:
                        retryAfter: 60
  /accounts/{id}/tiktok-creator-info:
    get:
      operationId: getTikTokCreatorInfo
      tags:
        - Accounts
      summary: Get TikTok creator info
      description: Returns the TikTok creator's current privacy options, interaction
        settings, and maximum video duration. Call this before posting to TikTok
        — the `privacyLevel` you set must match one of the returned
        `privacyLevelOptions`.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
            example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Creator info retrieved successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TikTokCreatorInfoResponse"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "404":
          description: Account not found or not a TikTok account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/accounts/{id}/tiktok-creator-info
                      details:
                        retryAfter: 60
  /workspaces:
    get:
      operationId: listWorkspaces
      tags:
        - Workspaces
      summary: List workspaces
      description: Return your workspaces, including their names, colors, and account
        counts.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Workspaces retrieved successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspacesResponse"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "403":
          description: API key lacks the required scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                insufficientScope:
                  value:
                    error:
                      title: Forbidden
                      status: 403
                      code: insufficient_scope
                      message: "Missing required scope: workspaces:read"
                      instance: /api/v1/workspaces
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/workspaces
                      details:
                        retryAfter: 60
    post:
      operationId: createWorkspace
      tags:
        - Workspaces
      summary: Create workspace
      description: Create a workspace to organize your connected social accounts.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWorkspaceRequest"
      responses:
        "201":
          description: Workspace created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                validationFailed:
                  value:
                    error:
                      title: Bad Request
                      status: 400
                      code: validation_failed
                      message: Request validation failed
                      instance: /api/v1/workspaces
                      details:
                        errors:
                          - path: name
                            message: Workspace name is required
                            code: too_small
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "403":
          description: API key lacks the required scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                insufficientScope:
                  value:
                    error:
                      title: Forbidden
                      status: 403
                      code: insufficient_scope
                      message: "Missing required scope: workspaces:write"
                      instance: /api/v1/workspaces
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/workspaces
                      details:
                        retryAfter: 60
  /workspaces/{id}:
    get:
      operationId: getWorkspace
      tags:
        - Workspaces
      summary: Get workspace
      description: Return one workspace by ID, including its name, color, description,
        and account count.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
            example: 4c57ad21-7dc1-4f0b-a8f6-c7b3fdd8c9e7
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Workspace retrieved successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "403":
          description: API key lacks the required scope or workspace access
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                insufficientScope:
                  value:
                    error:
                      title: Forbidden
                      status: 403
                      code: insufficient_scope
                      message: "Missing required scope: workspaces:read"
                      instance: /api/v1/workspaces/4c57ad21-7dc1-4f0b-a8f6-c7b3fdd8c9e7
        "404":
          description: Workspace not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                workspaceNotFound:
                  value:
                    error:
                      title: Not Found
                      status: 404
                      code: workspace_not_found
                      message: Workspace not found
                      instance: /api/v1/workspaces/4c57ad21-7dc1-4f0b-a8f6-c7b3fdd8c9e7
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/workspaces/4c57ad21-7dc1-4f0b-a8f6-c7b3fdd8c9e7
                      details:
                        retryAfter: 60
  /media:
    get:
      operationId: listMedia
      tags:
        - Media
      summary: List media
      description: Return your uploaded and generated media files. You can filter by
        ID, type, and source before reusing them in posts.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: integer
            minimum: 0
            exclusiveMinimum: true
            default: 1
            example: 1
            description: Page number (starts from 1).
          required: false
          description: Page number (starts from 1).
          name: page
          in: query
        - schema:
            type: integer
            minimum: 0
            exclusiveMinimum: true
            maximum: 100
            default: 20
            example: 20
            description: Page size. Max 100.
          required: false
          description: Page size. Max 100.
          name: limit
          in: query
        - schema:
            type: array
            items:
              type: string
              format: uuid
            description: Return only the specified media IDs. Repeat the parameter to apply
              OR logic, for example `?ids=id1&ids=id2`.
            example:
              - f6370ca1-1f47-4f68-be72-c1206913b326
              - 4c57ad21-7dc1-4f0b-a8f6-c7b3fdd8c9e7
          required: false
          description: Return only the specified media IDs. Repeat the parameter to apply
            OR logic, for example `?ids=id1&ids=id2`.
          name: ids
          in: query
        - schema:
            type: array
            items:
              $ref: "#/components/schemas/MediaType"
            description: Return only media items of the specified types. Repeat the
              parameter to apply OR logic, for example
              `?mediaTypes=image&mediaTypes=video`.
            example:
              - IMAGE
              - VIDEO
          required: false
          description: Return only media items of the specified types. Repeat the
            parameter to apply OR logic, for example
            `?mediaTypes=image&mediaTypes=video`.
          name: mediaTypes
          in: query
        - schema:
            type: array
            items:
              $ref: "#/components/schemas/MediaSource"
            description: Return only media items from the specified sources. Repeat the
              parameter to apply OR logic, for example
              `?sources=upload&sources=ai_generated`.
            example:
              - UPLOAD
              - AI_GENERATED
          required: false
          description: Return only media items from the specified sources. Repeat the
            parameter to apply OR logic, for example
            `?sources=upload&sources=ai_generated`.
          name: sources
          in: query
      responses:
        "200":
          description: Media retrieved successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MediaListResponse"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/media
                      details:
                        retryAfter: 60
    post:
      operationId: uploadMedia
      tags:
        - Media
      summary: Upload media
      description: Upload one media file with multipart form data. The returned media
        ID can then be attached to posts.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/UploadMediaRequest"
      responses:
        "201":
          description: Media uploaded successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Media"
        "400":
          description: Invalid upload request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                validationFailed:
                  value:
                    error:
                      title: Bad Request
                      status: 400
                      code: validation_failed
                      message: Request validation failed
                      instance: /api/v1/media
                      details:
                        errors:
                          - path: mediaTypes.0
                            message: 'Invalid option: expected one of "IMAGE"|"VIDEO"'
                            code: invalid_value
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "403":
          description: API key lacks the required scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                insufficientScope:
                  value:
                    error:
                      title: Forbidden
                      status: 403
                      code: insufficient_scope
                      message: "Missing required scope: media:write"
                      instance: /api/v1/media
        "413":
          description: File too large
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                fileTooLarge:
                  value:
                    error:
                      title: Error
                      status: 413
                      code: file_too_large
                      message: File size too large. Maximum size is 500MB.
                      instance: /api/v1/media
        "415":
          description: Unsupported file type
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                unsupportedFileType:
                  value:
                    error:
                      title: Unsupported Media Type
                      status: 415
                      code: unsupported_file_type
                      message: Unsupported file type
                      instance: /api/v1/media
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/media
                      details:
                        retryAfter: 60
  /media/{id}:
    get:
      operationId: getMedia
      tags:
        - Media
      summary: Get media
      description: Return one media file by ID, including its URL, source, and usage count.
      security:
        - ApiKeyAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
            example: f6370ca1-1f47-4f68-be72-c1206913b326
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Media retrieved successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Media"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                missingAuth: *a1
                invalidApiKey: *a2
        "404":
          description: Media not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                mediaNotFound:
                  value:
                    error:
                      title: Not Found
                      status: 404
                      code: media_not_found
                      message: Media not found
                      instance: /api/v1/media
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimitExceeded:
                  value:
                    error:
                      title: Too Many Requests
                      status: 429
                      code: rate_limit_exceeded
                      message: Too Many Requests
                      instance: /api/v1/media/f6370ca1-1f47-4f68-be72-c1206913b326
                      details:
                        retryAfter: 60
