openapi: 3.1.0
info:
  title: PostPlanify API
  version: 1.0.0
  description: |
    The PostPlanify API lets you schedule and manage social media posts across all major platforms programmatically. Use it to build custom integrations, automate your publishing workflows, or connect PostPlanify to your own tools.

    ## Base URL

    ```
    https://api.postplanify.com/api/v1
    ```

    ## Authentication

    Authenticate every request with an API key in the `Authorization` header:

    ```
    Authorization: Bearer sk_live_xxxxxxxxxxxx
    ```

    You can generate and manage API keys from your [dashboard](https://postplanify.com/dashboard/api-keys). Keys start with `sk_live_` and should be kept secret — never expose them in client-side code or public repositories.

    ## Rate Limits

    - **100 requests per minute** per API key
    - **6 posts per hour** per social account (based on scheduled publish time)
    - **100 posts per day** per social account (based on scheduled publish time)
    - Standard rate limit headers (`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`) are included in every response
    - If you exceed the limit, you'll receive a `429` response — wait and retry after the reset window

    The per-account limits prevent overloading social platform APIs. Space out your scheduled times to stay within limits.

    ## Response Format

    All successful responses include `"ok": true` and a `data` field:

    ```json
    {
      "ok": true,
      "data": { ... }
    }
    ```

    Paginated endpoints also include a `pagination` object:

    ```json
    {
      "ok": true,
      "data": [ ... ],
      "pagination": {
        "page": 1,
        "limit": 20,
        "total": 48,
        "has_more": true
      }
    }
    ```

    ## Errors

    All errors return `"ok": false` with a machine-readable `code` and a human-readable `message`:

    ```json
    {
      "ok": false,
      "error": {
        "code": "NOT_FOUND",
        "message": "Post not found"
      }
    }
    ```

    ## Timestamps & Timezones

    All timestamps are in **UTC** using ISO 8601 format (`2026-03-15T14:00:00.000Z`). The API does not accept or return timezone offsets — convert to UTC before sending.

    For example, to schedule a post at 3:00 PM Eastern Time (UTC-5), send:
    ```
    "scheduledAt": "2026-03-15T20:00:00.000Z"
    ```

    Common error codes:

    | HTTP Status | Code | Meaning |
    |---|---|---|
    | 400 | `VALIDATION_ERROR` | Invalid or missing parameters |
    | 401 | `UNAUTHORIZED` | Missing, invalid, or expired API key |
    | 404 | `NOT_FOUND` | Resource doesn't exist or you don't have access |
    | 429 | `RATE_LIMIT_EXCEEDED` | Too many requests — slow down |
    | 500 | `INTERNAL_ERROR` | Something went wrong on our end |

servers:
  - url: https://api.postplanify.com/api/v1
    description: Production
  - url: http://localhost:3030/api/v1
    description: Local development

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key (starts with `sk_live_`)

  schemas:
    Workspace:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: "d35f525f-63e6-40ae-8946-8c120edb0225"
        name:
          type: string
          example: "My Brand"
        websiteUrl:
          type: string
          nullable: true
          example: "https://mybrand.com"
        logoUrl:
          type: string
          nullable: true
          example: "https://cdn.postplanify.com/logos/mybrand.png"
        domain:
          type: string
          nullable: true
          example: "mybrand.com"
        socialAccounts:
          type: array
          items:
            $ref: '#/components/schemas/SocialAccountSummary'

    SocialAccountSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: "c47eb231-8eb2-4600-ac24-6951c7b5e3ce"
        platform:
          $ref: '#/components/schemas/SocialPlatform'
          example: "INSTAGRAM"
        accountName:
          type: string
          example: "mybrand"
        accountUrl:
          type: string
          nullable: true
          example: "https://instagram.com/mybrand"
        profilePictureUrl:
          type: string
          nullable: true
          example: "https://scontent.cdninstagram.com/profile.jpg"

    SocialAccount:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: "c47eb231-8eb2-4600-ac24-6951c7b5e3ce"
        workspaceId:
          type: string
          format: uuid
          example: "d35f525f-63e6-40ae-8946-8c120edb0225"
        platform:
          $ref: '#/components/schemas/SocialPlatform'
          example: "INSTAGRAM"
        accountName:
          type: string
          example: "mybrand"
        accountUrl:
          type: string
          nullable: true
          example: "https://instagram.com/mybrand"
        profilePictureUrl:
          type: string
          nullable: true
          example: "https://scontent.cdninstagram.com/profile.jpg"

    SocialPlatform:
      type: string
      enum:
        - FACEBOOK
        - INSTAGRAM
        - X
        - LINKEDIN
        - TIKTOK
        - YOUTUBE
        - THREADS
        - BLUESKY
        - PINTEREST
        - GOOGLE_BUSINESS

    PostStatus:
      type: string
      enum:
        - SCHEDULED
        - PUBLISHED
        - CANCELLED
        - RESCHEDULED

    Post:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: "a004b44d-2e69-4236-873a-081dc0d159a7"
        workspaceId:
          type: string
          format: uuid
          example: "d35f525f-63e6-40ae-8946-8c120edb0225"
        caption:
          type: string
          nullable: true
          example: "Check out our latest product launch! 🚀"
        status:
          $ref: '#/components/schemas/PostStatus'
          example: "SCHEDULED"
        platform:
          $ref: '#/components/schemas/SocialPlatform'
          example: "INSTAGRAM"
        socialPlatformPostId:
          type: string
          nullable: true
          description: The native post ID on the social platform
          example: "17890523456789"
        isStory:
          type: boolean
          example: false
        scheduledAt:
          type: string
          format: date-time
          nullable: true
          example: "2026-03-01T12:00:00.000Z"
        publishedAt:
          type: string
          format: date-time
          nullable: true
          example: null
        createdAt:
          type: string
          format: date-time
          example: "2026-02-19T10:30:00.000Z"
        updatedAt:
          type: string
          format: date-time
          example: "2026-02-19T10:30:00.000Z"
        socialAccount:
          $ref: '#/components/schemas/SocialAccountSummary'
        medias:
          type: array
          items:
            $ref: '#/components/schemas/PostMedia'
        logs:
          type: array
          items:
            $ref: '#/components/schemas/PostLog'
        platformParams:
          type: object
          nullable: true
          description: Platform-specific parameters (YouTube, TikTok, Pinterest, or Google Business fields)

    PostMedia:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: "f8a2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
        name:
          type: string
          example: "product-launch.jpg"
        url:
          type: string
          example: "https://cdn.postplanify.com/posts/all-types/product-launch.jpg"
        type:
          type: string
          description: "Media type (e.g. IMAGE_JPEG, VIDEO_MP4)"
          example: "IMAGE_JPEG"
        size:
          type: number
          description: File size in bytes
          example: 245000
        width:
          type: integer
          nullable: true
          example: 1080
        height:
          type: integer
          nullable: true
          example: 1080
        duration:
          type: number
          nullable: true
          description: Video duration in seconds
          example: null
        coverImageUrl:
          type: string
          nullable: true
          example: null

    PostLog:
      type: object
      properties:
        type:
          type: string
          enum: [SUCCESS, ERROR, WARNING, INFO]
          example: "SUCCESS"
        message:
          type: string
          nullable: true
          example: "Post created successfully"
        createdAt:
          type: string
          format: date-time
          example: "2026-02-19T10:30:00.000Z"

    MediaUpload:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: "e1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6"
        name:
          type: string
          description: Original filename
          example: "banner.png"
        url:
          type: string
          description: Public URL of the uploaded file
          example: "https://cdn.postplanify.com/api-uploads/banner.png"
        type:
          type: string
          description: MIME type
          example: "image/png"
        size:
          type: number
          description: File size in bytes
          example: 542000
        createdAt:
          type: string
          format: date-time
          example: "2026-02-19T10:00:00.000Z"

    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: "b59da42d-7fe6-4c7e-bfff-a6a255e8c04d"
        email:
          type: string
          format: email
          example: "john@example.com"
        fullName:
          type: string
          example: "John Doe"

    YouTubeParams:
      type: object
      description: YouTube-specific parameters. Only applicable to YouTube posts.
      required: [title]
      properties:
        title:
          type: string
          maxLength: 100
          description: Video title (required)
          example: "My Video Title"
        selfDeclaredMadeForKids:
          type: boolean
          default: false
          description: Whether the video is made for kids (COPPA compliance)
          example: false
        categoryId:
          type: string
          default: "22"
          description: |
            YouTube video category ID. Defaults to "22" (People & Blogs). Common categories: "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.
          enum: ["1", "2", "10", "15", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44"]
          example: "22"
        tags:
          type: string
          maxLength: 500
          description: Comma-separated tags (e.g. "tech,tutorial,howto")
          example: "tech,tutorial,howto"

    TikTokParams:
      type: object
      description: TikTok-specific parameters. Only applicable to TikTok posts.
      properties:
        privacyLevel:
          type: string
          enum: [SELF_ONLY, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, PUBLIC_TO_EVERYONE]
          description: Who can view the post. Defaults to `PUBLIC_TO_EVERYONE`.
          example: "PUBLIC_TO_EVERYONE"
        disableDuet:
          type: boolean
          default: false
          description: Prevent others from creating duets with this video
          example: false
        disableComment:
          type: boolean
          default: false
          description: Disable comments on the post
          example: false
        disableStitch:
          type: boolean
          default: false
          description: Prevent others from stitching this video
          example: false
        isDraftType:
          type: boolean
          default: false
          description: Save as draft in TikTok inbox instead of publishing directly
          example: false
        isCommercialContent:
          type: boolean
          default: false
          description: Disclose the post as commercial content
          example: false
        isYourBrand:
          type: boolean
          default: false
          description: The post promotes your own brand or business
          example: false
        isBrandedContent:
          type: boolean
          default: false
          description: The post is a paid partnership or sponsored content. Cannot be `true` when `privacyLevel` is `SELF_ONLY`.
          example: false
        autoAddMusic:
          type: boolean
          default: true
          description: Automatically add music to photo posts
          example: true
        photosPostTitle:
          type: string
          maxLength: 90
          description: Title for photo/carousel posts only (max 90 characters)
          example: "Check out these photos"

    PinterestBoard:
      type: object
      properties:
        id:
          type: string
          description: Board ID — use this as `pinterestParams.boardId` when creating a post
          example: "715376267631025391"
        name:
          type: string
          description: Board display name
          example: "Style Inspiration"

    PinterestParams:
      type: object
      description: Pinterest-specific parameters. Only applicable to Pinterest posts.
      required: [boardId]
      properties:
        title:
          type: string
          maxLength: 100
          description: Pin title
          example: "Spring Collection 2026"
        boardId:
          type: string
          description: Target board ID (required). Use `GET /social-accounts/{id}/pinterest-boards` to list available boards and their IDs.
          example: "board-abc123"
        boardName:
          type: string
          description: Board name — stored for reference, auto-filled from the selected board
          example: "Style Inspiration"
        link:
          type: string
          description: Destination URL when the pin is clicked. Must be a valid URL (contain a dot, no spaces).
          example: "https://mybrand.com/spring"
        altText:
          type: string
          maxLength: 500
          description: Alt text for accessibility (max 500 characters)
          example: "Spring fashion collection lookbook"

    GoogleBusinessParams:
      type: object
      description: |
        Google Business Profile post parameters. Only applicable to Google Business posts.

        Three post types are supported:
        - **STANDARD** — General update or announcement (default)
        - **EVENT** — Promote an event with dates. Requires `eventTitle`, `startDate`, and `endDate`.
        - **OFFER** — Share a deal or promotion. Requires `eventTitle`, `startDate`, and `endDate`. Automatically includes a "View offer" button — do not set `actionType`.
      properties:
        topicType:
          type: string
          enum: [STANDARD, EVENT, OFFER]
          default: STANDARD
          description: Post type
          example: "STANDARD"
        actionType:
          type: string
          enum: [BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP, CALL]
          nullable: true
          description: CTA button type. Not supported for OFFER posts (they get an automatic "View offer" button). CALL uses the business profile phone number and does not require `actionUrl`.
          example: "LEARN_MORE"
        actionUrl:
          type: string
          nullable: true
          description: CTA button URL. Required when `actionType` is set (except CALL).
          example: "https://mybrand.com/book"
        eventTitle:
          type: string
          maxLength: 58
          nullable: true
          description: Event or offer title (max 58 characters). Required for EVENT and OFFER posts.
          example: "Grand Opening Weekend"
        startDate:
          type: string
          format: date-time
          nullable: true
          description: Event/offer start date (ISO 8601). Required for EVENT and OFFER posts. Must be before `endDate`.
          example: "2026-04-01T09:00:00.000Z"
        endDate:
          type: string
          format: date-time
          nullable: true
          description: Event/offer end date (ISO 8601). Required for EVENT and OFFER posts. Must be after `startDate`.
          example: "2026-04-03T18:00:00.000Z"
        couponCode:
          type: string
          maxLength: 58
          nullable: true
          description: Coupon code for OFFER posts (max 58 characters)
          example: "SAVE20"
        redeemOnlineUrl:
          type: string
          nullable: true
          description: Redemption URL for OFFER posts
          example: "https://mybrand.com/redeem"
        termsConditions:
          type: string
          nullable: true
          description: Terms and conditions for OFFER posts
          example: "Valid for new customers only. Cannot be combined with other offers."

    EditPostRequest:
      type: object
      description: |
        Partial update — only include the fields you want to change. Platform-specific parameters must match the post's platform or the request will be rejected.
      properties:
        caption:
          type: string
          description: New post caption
          example: "Updated caption for our launch post!"
        scheduledAt:
          type: string
          format: date-time
          description: New scheduled time (UTC, must be in the future)
          example: "2026-03-20T15:00:00.000Z"
        youtubeParams:
          $ref: '#/components/schemas/YouTubeParams'
        tiktokParams:
          $ref: '#/components/schemas/TikTokParams'
        pinterestParams:
          $ref: '#/components/schemas/PinterestParams'
        googleBusinessParams:
          $ref: '#/components/schemas/GoogleBusinessParams'
        xCommunityId:
          type: string
          nullable: true
          description: X community ID (X posts only)
        xTaggedUsers:
          type: string
          nullable: true
          description: Tagged users for media posts (X posts only)

    CreatePostRequest:
      type: object
      required: [workspaceId, socialAccountId, scheduledAt]
      properties:
        workspaceId:
          type: string
          format: uuid
          description: The workspace this post belongs to
          example: "d35f525f-63e6-40ae-8946-8c120edb0225"
        socialAccountId:
          type: string
          format: uuid
          description: The social account to publish to
          example: "c47eb231-8eb2-4600-ac24-6951c7b5e3ce"
        caption:
          type: string
          description: Post text/caption. Length limits vary by platform (e.g. Instagram 2200, Threads 500, Bluesky 300).
          example: "Check out our latest product launch!"
        scheduledAt:
          type: string
          format: date-time
          description: When to publish (UTC, must be in the future)
          example: "2026-03-15T20:00:00.000Z"
        mediaIds:
          type: array
          items:
            type: string
            format: uuid
          description: IDs of previously uploaded media files (from `POST /media/upload`). Requirements vary by platform.
          example: ["e1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6"]
        isStory:
          type: boolean
          default: false
          description: Post as a story (Instagram and Facebook only, requires exactly 1 media file)
          example: false
        youtubeParams:
          $ref: '#/components/schemas/YouTubeParams'
        tiktokParams:
          $ref: '#/components/schemas/TikTokParams'
        pinterestParams:
          $ref: '#/components/schemas/PinterestParams'
        googleBusinessParams:
          $ref: '#/components/schemas/GoogleBusinessParams'
        xCommunityId:
          type: string
          description: X community ID (X posts only)
        xTaggedUsers:
          type: string
          description: Comma-separated X usernames to tag in media (X posts only, max 10)

    Pagination:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        has_more:
          type: boolean

    ErrorResponse:
      type: object
      properties:
        ok:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string

    Comment:
      type: object
      properties:
        id:
          type: string
          description: |
            Platform-native comment identifier. For Instagram, Facebook and Google Business this is a numeric string. For LinkedIn it is a URN (e.g. `urn:li:comment:(activity:12345,67890)`) — **always URL-encode it** when placing it in a path parameter.
          example: "17890123456789012"
        workspaceId:
          type: string
          format: uuid
          example: "d35f525f-63e6-40ae-8946-8c120edb0225"
        parentCommentId:
          type: string
          nullable: true
          description: ID of the comment this one is a reply to, or `null` for top-level comments.
        text:
          type: string
          example: "Love this product!"
        username:
          type: string
          description: |
            Commenter's username. LinkedIn does not expose commenter names through its Comments API — LinkedIn comments return `"LinkedIn User"` unless the commenter is the connected account itself.
          example: "janedoe"
        platformUserId:
          type: string
          nullable: true
        profilePictureUrl:
          type: string
          nullable: true
        timestamp:
          type: string
          format: date-time
          description: When the comment was posted on the platform.
        likeCount:
          type: integer
          example: 3
        replyCount:
          type: integer
          description: Number of direct replies (nested comments).
          example: 0
        isHidden:
          type: boolean
          description: Whether the comment is hidden on the platform. Only Instagram and Facebook support hiding.
        isRead:
          type: boolean
          description: Internal read state in PostPlanify. Not reflected on the platform.
        brandReplied:
          type: boolean
          description: True if you have already replied to this comment through PostPlanify.
        rating:
          type: integer
          nullable: true
          description: Star rating (1–5). Only set for Google Business reviews; `null` for other platforms.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        socialAccount:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            platform:
              $ref: '#/components/schemas/SocialPlatform'
            accountName:
              type: string
            profilePictureUrl:
              type: string
              nullable: true
        post:
          type: object
          nullable: true
          description: The post (or review target) this comment is attached to.
          properties:
            id:
              type: string
              description: Platform-native post ID.
            platform:
              $ref: '#/components/schemas/SocialPlatform'
            mediaType:
              type: string
              nullable: true
            permalink:
              type: string
              nullable: true
            mediaUrl:
              type: string
              nullable: true
            caption:
              type: string
              nullable: true
            postedAt:
              type: string
              format: date-time
              nullable: true

    ReplyToCommentRequest:
      type: object
      required: [message]
      properties:
        message:
          type: string
          description: |
            The reply body. Platform-specific length limits apply at publish time:
            - Instagram: 2,200 characters
            - Facebook: 8,000 characters
            - LinkedIn: 1,250 characters
            - Google Business: 4,096 characters

            Replying to a hidden comment is rejected with a `400` error.
          example: "Thanks so much — glad you love it!"

    ReplyToCommentResponse:
      type: object
      properties:
        id:
          type: string
          description: Platform-native ID of the newly created reply.
        parentCommentId:
          type: string
          description: The comment you replied to.

paths:
  /me:
    get:
      operationId: getMe
      summary: Get current user
      description: Returns the user associated with the API key. Useful for verifying your key is working.
      tags: [User]
      responses:
        '200':
          description: Current user
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/User'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /workspaces:
    get:
      operationId: listWorkspaces
      summary: List workspaces
      description: Returns all workspaces you have access to, along with their connected social accounts. Use this to get a `workspaceId` for other endpoints.
      tags: [Workspaces]
      responses:
        '200':
          description: List of workspaces
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Workspace'

  /workspaces/{id}:
    get:
      operationId: getWorkspace
      summary: Get workspace
      description: Returns a single workspace and its connected social accounts.
      tags: [Workspaces]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Workspace details
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/Workspace'
        '404':
          description: Workspace not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /social-accounts:
    get:
      operationId: listSocialAccounts
      summary: List social accounts
      description: Returns all connected social accounts across all your workspaces.
      tags: [Social Accounts]
      responses:
        '200':
          description: List of social accounts
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SocialAccount'

  /social-accounts/{id}:
    get:
      operationId: getSocialAccount
      summary: Get social account
      description: Returns a single social account. You need a social account's `id` when creating a post.
      tags: [Social Accounts]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Social account details
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/SocialAccount'
        '404':
          description: Social account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /social-accounts/{id}/pinterest-boards:
    get:
      operationId: getPinterestBoards
      summary: List Pinterest boards
      description: |
        Returns the boards available on a Pinterest social account. Use the returned `id` value as `pinterestParams.boardId` when creating a Pinterest post.

        This endpoint only works for social accounts with platform `PINTEREST`. Calling it on any other platform returns a `400` error.
      tags: [Social Accounts]
      parameters:
        - name: id
          in: path
          required: true
          description: Social account ID (must be a Pinterest account)
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: List of Pinterest boards
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PinterestBoard'
              example:
                ok: true
                data:
                  - id: "715376267631025391"
                    name: "Style Inspiration"
                  - id: "715376267631025392"
                    name: "Recipes"
                  - id: "715376267631025393"
                    name: "Travel"
        '400':
          description: Not a Pinterest account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Social account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /posts:
    get:
      operationId: listPosts
      summary: List posts
      description: Returns a paginated list of posts in a workspace. Supports filtering by status, platform, social account, and date range.
      tags: [Posts]
      parameters:
        - name: workspaceId
          in: query
          required: true
          description: Filter posts by workspace
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          required: false
          description: Filter by post status
          schema:
            $ref: '#/components/schemas/PostStatus'
        - name: socialAccountId
          in: query
          required: false
          description: Filter by a specific social account
          schema:
            type: string
            format: uuid
        - name: platform
          in: query
          required: false
          description: Filter by platform
          schema:
            $ref: '#/components/schemas/SocialPlatform'
        - name: from
          in: query
          required: false
          description: "Start of date range (ISO 8601). Matches posts scheduled or published on or after this date."
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: "End of date range (ISO 8601). Matches posts scheduled or published before this date."
          schema:
            type: string
            format: date-time
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
            minimum: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Paginated list of posts
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Post'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: Validation error (missing workspaceId or invalid status)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Workspace not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

    post:
      operationId: createPost
      summary: Schedule a post
      description: |
        Creates and schedules a post to a social account. The post is added to the publish queue and will be automatically published at the specified `scheduledAt` time.

        ## Workflow

        1. **Upload media** — `POST /media/upload` for each file. Save the returned `id` values.
        2. **Create the post** — call this endpoint with `mediaIds`, `scheduledAt`, and platform params.
        3. **Done** — the post is queued and will publish automatically at the scheduled time.

        **Media ordering:** For carousel posts (Instagram, TikTok, etc.), files appear in the order they were uploaded. Upload them one at a time, in the order you want them to appear.

        ## Media rules by platform

        | Platform | Media required? | Limits | Notes |
        |----------|----------------|--------|-------|
        | Instagram | Yes | 1–10 images/videos | Stories: exactly 1 |
        | Facebook | No | Up to 10 images OR 1 video | Cannot mix types. Stories: exactly 1 |
        | X | No | Up to 4 images/videos | Text-only posts allowed |
        | YouTube | Yes | Exactly 1 video | `youtubeParams.title` is required |
        | TikTok | Yes | 1 video OR 1+ images | Cannot mix types |
        | LinkedIn | No | Up to 20 images OR 1 video OR 1 PDF | Cannot mix types |
        | Pinterest | Yes | Exactly 1 image or video | `pinterestParams.boardId` is required |
        | Threads | No | Up to 20 images/videos | Text-only posts allowed |
        | Bluesky | No | Up to 4 images OR 1 video | Cannot mix types |
        | Google Business | No | Up to 1 image | No video via API. Text-only posts allowed |

        ## Platform-specific parameters

        Some platforms require or accept additional parameters. Pass them only for their matching platform — sending `youtubeParams` on an Instagram post will return a validation error.

        - **YouTube** — `youtubeParams` required: `title` (max 100 chars), optional `categoryId` (default "22"), `tags` (comma-separated, max 500 chars), `selfDeclaredMadeForKids`
        - **TikTok** — `tiktokParams` optional: `privacyLevel` (defaults to `PUBLIC_TO_EVERYONE`), `disableDuet`, `disableComment`, `disableStitch`, `isDraftType`, `isCommercialContent`, `isYourBrand`, `isBrandedContent`, `autoAddMusic`, `photosPostTitle` (max 90 chars, photo posts only)
        - **Pinterest** — `pinterestParams` required: `boardId` (get IDs from `GET /social-accounts/{id}/pinterest-boards`), optional `title` (max 100 chars), `link` (valid URL), `altText` (max 500 chars)
        - **Google Business** — `googleBusinessParams` optional: `topicType` (default `STANDARD`; `EVENT` and `OFFER` require `eventTitle`, `startDate`, `endDate`), `actionType` (CTA button, not for OFFER), `actionUrl` (required for non-CALL CTA), `couponCode` (OFFER only, max 58 chars), `redeemOnlineUrl`, `termsConditions`
        - **X** — `xCommunityId` optional, `xTaggedUsers` optional (comma-separated usernames, max 10, requires media)

        ## Caption limits

        Caption length limits are enforced by each platform at publish time:

        | Platform | Max caption length |
        |----------|--------------------|
        | Instagram | 2,200 characters |
        | TikTok | 2,200 characters |
        | Threads | 500 characters |
        | Pinterest | 500 characters (description) |
        | Bluesky | 300 characters |
        | Google Business | 1,500 characters |
        | Facebook, X, YouTube, LinkedIn | No strict limit |

        ## Stories

        Set `isStory: true` to post as a story. Only Instagram and Facebook support stories. Stories require exactly 1 media file.

        ## Example

        ```json
        {
          "workspaceId": "d35f525f-63e6-40ae-8946-8c120edb0225",
          "socialAccountId": "c47eb231-8eb2-4600-ac24-6951c7b5e3ce",
          "caption": "Check out our latest product launch!",
          "scheduledAt": "2026-03-15T20:00:00.000Z",
          "mediaIds": ["e1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6"]
        }
        ```
      tags: [Posts]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePostRequest'
      responses:
        '201':
          description: Post created and scheduled
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/Post'
        '400':
          description: Validation error (missing fields, invalid media, wrong platform params)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Workspace requires an active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Workspace not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many posts scheduled for this social account in the same time window (max 6/hour, 25/day)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /posts/{id}:
    get:
      operationId: getPost
      summary: Get post
      description: Returns a single post with its media, logs, social account, and platform-specific parameters.
      tags: [Posts]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Post details
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/Post'
        '404':
          description: Post not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

    patch:
      operationId: updatePost
      summary: Edit post
      description: |
        Updates a scheduled post. Only posts with status `SCHEDULED` or `RESCHEDULED` can be edited.

        Send only the fields you want to change — all fields are optional. If `scheduledAt` is updated, the post is automatically rescheduled with its publish queue job updated.

        Platform-specific parameters (`youtubeParams`, `tiktokParams`, `pinterestParams`, `googleBusinessParams`, `xCommunityId`, `xTaggedUsers`) must match the post's platform. Sending `pinterestParams` on a YouTube post will return a validation error.
      tags: [Posts]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EditPostRequest'
      responses:
        '200':
          description: Post updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/Post'
        '400':
          description: Validation error (post not editable, scheduledAt in the past, wrong platform params)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Post not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

    delete:
      operationId: deletePost
      summary: Delete post
      description: Permanently removes a post. This action cannot be undone.
      tags: [Posts]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Post deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
        '404':
          description: Post not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /posts/{id}/cancel:
    post:
      operationId: cancelPost
      summary: Cancel a scheduled post
      description: |
        Cancels a scheduled post and removes it from the publish queue. Only posts with status `SCHEDULED` or `RESCHEDULED` can be cancelled.

        The post is not deleted — its status changes to `CANCELLED` and it remains visible in your post list.
      tags: [Posts]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Post cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/Post'
        '400':
          description: Post is not in a cancellable state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Post not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /media:
    get:
      operationId: listMedia
      summary: List uploaded media
      description: Returns all media files you've uploaded through the API.
      tags: [Media]
      responses:
        '200':
          description: List of uploaded media
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/MediaUpload'

  /media/upload:
    post:
      operationId: uploadMedia
      summary: Upload media
      description: |
        Upload a media file (image, video, or PDF). The file is stored and can be referenced when creating posts.

        **Allowed types:** JPEG, PNG, GIF, WebP, MP4, MOV, PDF

        **Max file size:** 100 MB
      tags: [Media]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: File uploaded successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/MediaUpload'
        '400':
          description: Missing file or invalid file type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /media/{id}:
    delete:
      operationId: deleteMedia
      summary: Delete media
      description: Deletes an uploaded media file. Files already attached to published posts are not affected.
      tags: [Media]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Media deleted
        '404':
          description: Media not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /analytics/{workspaceId}/overview:
    get:
      operationId: getAnalyticsOverview
      summary: Get brand overview
      description: |
        Get an aggregated analytics overview for a workspace. Returns all connected social accounts with 7-day metrics (views, engagement, follower changes), top 10 posts by views, and 30-day daily view/engagement trends.
      tags: [Analytics]
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Brand overview
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      accounts:
                        type: array
                        description: All social accounts with 7-day metrics
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                              format: uuid
                            platform:
                              type: string
                              example: "INSTAGRAM"
                            accountName:
                              type: string
                            profilePictureUrl:
                              type: string
                              nullable: true
                            analyticsLastSyncAt:
                              type: string
                              format: date-time
                              nullable: true
                            currentFollowers:
                              type: integer
                              nullable: true
                            followersChange:
                              type: integer
                              nullable: true
                              description: Follower change over last 7 days
                            followersChangePct:
                              type: number
                              nullable: true
                            views7d:
                              type: integer
                            engagement7d:
                              type: integer
                            avgEngagementRate:
                              type: number
                              nullable: true
                            posts7d:
                              type: integer
                      topPosts:
                        type: array
                        description: Top 10 posts by views in the last 7 days
                        items:
                          type: object
                          properties:
                            platformPostId:
                              type: string
                            platform:
                              type: string
                            text:
                              type: string
                              nullable: true
                            thumbnailUrl:
                              type: string
                              nullable: true
                            views:
                              type: integer
                            likes:
                              type: integer
                            comments:
                              type: integer
                            shares:
                              type: integer
                            engagementRate:
                              type: number
                              nullable: true
                      dailyViews:
                        type: array
                        description: Daily views and engagement for the last 30 days
                        items:
                          type: object
                          properties:
                            date:
                              type: string
                              format: date-time
                            views:
                              type: integer
                            engagement:
                              type: integer
                      followerSnapshots:
                        type: array
                        description: Daily follower snapshots for the last 30 days
        '404':
          description: Workspace not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /analytics/{workspaceId}/{socialAccountId}:
    get:
      operationId: getAccountAnalytics
      summary: Get account analytics
      description: |
        Get analytics data for a specific social account. Returns account-level metrics (followers, following) and post-level metrics (views, likes, comments, engagement rate).

        The response format varies by platform — each platform returns its own metric fields. All platforms include a `meta.lastUpdated` timestamp showing when data was last synced.
      tags: [Analytics]
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: socialAccountId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Account analytics data (format varies by platform)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: object
                    description: |
                      Platform-specific analytics data. Common fields across platforms:
                      - `meta.lastUpdated` — when analytics were last synced
                      - Post items include `engagementRatePct` and `totalInteractions`
                      - Account/user objects include follower and following counts
        '404':
          description: Workspace or social account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /analytics/{workspaceId}/{socialAccountId}/trends:
    get:
      operationId: getAnalyticsTrends
      summary: Get analytics trends
      description: |
        Get historical trend data for a social account. Returns daily follower snapshots and aggregated post metrics (views, likes, comments, shares, engagement) over time.

        Use the `days` query parameter to control the time range (1-365 days, default 30).
      tags: [Analytics]
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: socialAccountId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: days
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 365
            default: 30
          description: Number of days to look back
      responses:
        '200':
          description: Trend data
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      accountSnapshots:
                        type: array
                        description: Daily follower/following snapshots
                        items:
                          type: object
                          properties:
                            date:
                              type: string
                              format: date-time
                            followersCount:
                              type: integer
                            followingCount:
                              type: integer
                      dailyMetrics:
                        type: array
                        description: Daily aggregated post metrics
                        items:
                          type: object
                          properties:
                            date:
                              type: string
                              format: date-time
                            totalViews:
                              type: integer
                            totalLikes:
                              type: integer
                            totalComments:
                              type: integer
                            totalShares:
                              type: integer
                            totalEngagement:
                              type: integer
                            weightedEngagementRate:
                              type: number
                              nullable: true
                      account:
                        type: object
                        properties:
                          platform:
                            type: string
        '404':
          description: Workspace or social account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /comments:
    get:
      operationId: listComments
      summary: List comments
      description: |
        Returns a paginated list of top-level comments across all connected inbox-capable accounts in a workspace. Inbox is available for **Instagram, Facebook, LinkedIn, and Google Business**.

        Comments are synced in the background as your social accounts are polled — results reflect the last synced state. Replies to other comments are not included; use `GET /comments/{id}/replies` to fetch those.

        ## Ordering

        Comments are returned newest first (by platform `timestamp`).

        ## Subscription

        This endpoint requires the workspace owner to have an active Growth, Premium, or Enterprise subscription.
      tags: [Comments]
      parameters:
        - name: workspaceId
          in: query
          required: true
          description: Workspace to list comments from.
          schema:
            type: string
            format: uuid
        - name: socialAccountId
          in: query
          required: false
          description: Filter by a specific social account.
          schema:
            type: string
            format: uuid
        - name: platform
          in: query
          required: false
          description: Filter by platform. Must be one of the inbox-capable platforms (`INSTAGRAM`, `FACEBOOK`, `LINKEDIN`, `GOOGLE_BUSINESS`).
          schema:
            type: string
            enum: [INSTAGRAM, FACEBOOK, LINKEDIN, GOOGLE_BUSINESS]
        - name: unreadOnly
          in: query
          required: false
          description: If `true`, only return comments that have not yet been marked as read.
          schema:
            type: boolean
            default: false
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
            minimum: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 50
      responses:
        '200':
          description: Paginated list of comments
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Comment'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: Missing `workspaceId` or invalid `platform`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Workspace requires an active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Workspace not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /comments/{platformCommentId}/replies:
    get:
      operationId: listCommentReplies
      summary: List replies to a comment
      description: |
        Returns the replies to a specific comment. The API may fetch fresh replies from the social platform on your behalf (with a short cooldown to stay under platform rate limits).

        The `platformCommentId` must be the exact ID returned by `GET /comments`. For LinkedIn, URL-encode the URN — for example `urn:li:comment:(activity:12345,67890)` becomes `urn%3Ali%3Acomment%3A%28activity%3A12345%2C67890%29`.
      tags: [Comments]
      parameters:
        - name: platformCommentId
          in: path
          required: true
          description: Platform-native comment ID (URL-encoded for LinkedIn URNs).
          schema:
            type: string
      responses:
        '200':
          description: List of replies (oldest first)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Comment'
        '404':
          description: Comment not found, or you do not have access to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /comments/{platformCommentId}/reply:
    post:
      operationId: replyToComment
      summary: Reply to a comment
      description: |
        Posts a public reply to a comment on the originating platform. The reply is published immediately via the platform's API (there is no scheduling for replies).

        ## Requirements

        - The comment's social account must still be connected and have the scopes required for the platform:
          - **Instagram** — `instagram_manage_comments` or `instagram_business_manage_comments`
          - **Facebook** — `pages_manage_engagement`
          - **LinkedIn** — no extra scopes required
          - **Google Business** — no extra scopes required
        - The comment must not be hidden (`isHidden: false`). Unhide it in the PostPlanify dashboard first if needed.
        - The reply must respect the platform's length limit (see `message` description).

        ## Success response

        Returns the new reply's platform-native ID. The reply is also persisted to PostPlanify's inbox and `brandReplied` is set on the parent comment.

        ## LinkedIn URN encoding

        For LinkedIn, URL-encode the parent comment's URN in the path — for example `urn:li:comment:(activity:12345,67890)` becomes `urn%3Ali%3Acomment%3A%28activity%3A12345%2C67890%29`.
      tags: [Comments]
      parameters:
        - name: platformCommentId
          in: path
          required: true
          description: Platform-native comment ID of the parent comment (URL-encoded for LinkedIn URNs).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReplyToCommentRequest'
      responses:
        '201':
          description: Reply posted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/ReplyToCommentResponse'
        '400':
          description: Validation error — missing message, reply too long, or replying to a hidden comment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Workspace requires an active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Comment not found, or you do not have access to it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

tags:
  - name: User
    description: Retrieve the profile associated with your API key.
  - name: Workspaces
    description: |
      A workspace groups your social accounts, posts, and team members together. Most API calls require a `workspaceId` — list your workspaces first to get one.
  - name: Social Accounts
    description: |
      Social accounts are the connected profiles (Instagram, X, YouTube, etc.) within a workspace. You need a `socialAccountId` when creating a post. For Pinterest accounts, use the boards endpoint to discover available board IDs.
  - name: Posts
    description: |
      Posts are the core resource. Each post belongs to one workspace and one social account. You can create, list, retrieve, edit, and delete posts through the API. Upload media first via the Media endpoints, then reference the IDs when creating a post. Draft posts are not accessible via the API.
  - name: Media
    description: |
      Upload images, videos, or PDFs before attaching them to a post. Uploaded files are stored and return a `url` and metadata you can reference later.
  - name: Analytics
    description: |
      View analytics for your social accounts. Get account-level metrics (followers, engagement), post-level performance data, historical trends, and aggregated brand overviews. Analytics data is synced periodically — check `meta.lastUpdated` for freshness.
  - name: Comments
    description: |
      Read and reply to comments and reviews synced from Instagram, Facebook, LinkedIn, and Google Business. Comments are synced continuously in the background — listing returns the most recent synced state. Replies are posted to the platform immediately.
