{
  "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.\n\n## Base URL\n\n```\nhttps://api.postplanify.com/api/v1\n```\n\n## Authentication\n\nAuthenticate every request with an API key in the `Authorization` header:\n\n```\nAuthorization: Bearer sk_live_xxxxxxxxxxxx\n```\n\nYou 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.\n\n## Rate Limits\n\n- **100 requests per minute** per API key\n- **6 posts per hour** per social account (based on scheduled publish time)\n- **100 posts per day** per social account (based on scheduled publish time)\n- Standard rate limit headers (`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`) are included in every response\n- If you exceed the limit, you'll receive a `429` response: wait and retry after the reset window\n\nThe per-account limits prevent overloading social platform APIs. Space out your scheduled times to stay within limits.\n\n## Response Format\n\nAll successful responses include `\"ok\": true` and a `data` field:\n\n```json\n{\n  \"ok\": true,\n  \"data\": { ... }\n}\n```\n\nPaginated endpoints also include a `pagination` object:\n\n```json\n{\n  \"ok\": true,\n  \"data\": [ ... ],\n  \"pagination\": {\n    \"page\": 1,\n    \"limit\": 20,\n    \"total\": 48,\n    \"has_more\": true\n  }\n}\n```\n\n## Errors\n\nAll errors return `\"ok\": false` with a machine-readable `code` and a human-readable `message`:\n\n```json\n{\n  \"ok\": false,\n  \"error\": {\n    \"code\": \"NOT_FOUND\",\n    \"message\": \"Post not found\"\n  }\n}\n```\n\n## Timestamps & Timezones\n\nAll 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.\n\nFor example, to schedule a post at 3:00 PM Eastern Time (UTC-5), send:\n```\n\"scheduledAt\": \"2026-03-15T20:00:00.000Z\"\n```\n\nCommon error codes:\n\n| HTTP Status | Code | Meaning |\n|---|---|---|\n| 400 | `VALIDATION_ERROR` | Invalid or missing parameters |\n| 401 | `UNAUTHORIZED` | Missing, invalid, or expired API key |\n| 404 | `NOT_FOUND` | Resource doesn't exist or you don't have access |\n| 429 | `RATE_LIMIT_EXCEEDED` | Too many requests: slow down |\n| 500 | `INTERNAL_ERROR` | Something went wrong on our end |\n"
  },
  "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"
          },
          "customLabel": {
            "type": "string",
            "nullable": true,
            "description": "User-set label to distinguish accounts (e.g. \"Downtown\")",
            "example": "Downtown"
          }
        }
      },
      "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"
          },
          "customLabel": {
            "type": "string",
            "nullable": true,
            "description": "User-set label to distinguish accounts (e.g. \"Downtown\")",
            "example": "Downtown"
          }
        }
      },
      "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
          },
          "shortenLinks": {
            "type": "boolean",
            "description": "Whether links are shortened with the workspace's Bitly account at publish time",
            "example": false
          },
          "utmCampaign": {
            "type": "string",
            "nullable": true,
            "description": "The `utm_campaign` added to shortened links, if any",
            "example": null
          },
          "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)"
          },
          "label": {
            "$ref": "#/components/schemas/PostLabel",
            "nullable": true,
            "description": "The content-pillar label assigned to this post (null if none)"
          },
          "metrics": {
            "$ref": "#/components/schemas/PostMetrics",
            "nullable": true,
            "description": "Latest performance figures for the post. Always present on `GET /posts/{id}`;\non `GET /posts` only when `includeMetrics=true`.\n\n`null` means no analytics have been collected for this post, which is not\nthe same as zero engagement. Posts that have not been published yet are\nalways `null`.\n"
          }
        }
      },
      "PostMetrics": {
        "type": "object",
        "nullable": true,
        "description": "A post's most recent daily analytics snapshot. Snapshots are collected once per\nday per post, so `measuredAt` shows how fresh these figures are.\n",
        "properties": {
          "views": {
            "type": "integer",
            "nullable": true,
            "example": 1842
          },
          "likes": {
            "type": "integer",
            "nullable": true,
            "example": 96
          },
          "comments": {
            "type": "integer",
            "nullable": true,
            "example": 12
          },
          "shares": {
            "type": "integer",
            "nullable": true,
            "example": 7
          },
          "engagementRate": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "Engagement as a share of views, where the platform reports it",
            "example": 0.061
          },
          "platformMetrics": {
            "type": "object",
            "nullable": true,
            "description": "Platform-specific extras, present only where the platform reports them.\nFor example X returns bookmarks and quotes, Instagram returns reach and saves.\n",
            "example": {
              "bookmarks": 14,
              "quotes": 3
            }
          },
          "measuredAt": {
            "type": "string",
            "format": "date",
            "description": "The date this snapshot was taken",
            "example": "2026-09-04"
          }
        }
      },
      "PostLabel": {
        "type": "object",
        "nullable": true,
        "description": "A user-defined content label attached to a post (used for organizing content pillars)",
        "properties": {
          "name": {
            "type": "string",
            "example": "Education"
          },
          "color": {
            "type": "string",
            "description": "Hex color code",
            "example": "#3b82f6"
          }
        }
      },
      "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"
          },
          "source": {
            "type": "string",
            "enum": [
              "upload",
              "library"
            ],
            "description": "Where the media lives: `upload` (added via the API) or `library` (the workspace's dashboard media library). Present on `GET /media` results.",
            "example": "library"
          }
        }
      },
      "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.\n",
            "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.\n\nThree post types are supported:\n- **STANDARD**: General update or announcement (default)\n- **EVENT**: Promote an event with dates. Requires `eventTitle`, `startDate`, and `endDate`.\n- **OFFER**: Share a deal or promotion. Requires `eventTitle`, `startDate`, and `endDate`. Automatically includes a \"View offer\" button: do not set `actionType`.\n",
        "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.\n",
        "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)"
          },
          "shortenLinks": {
            "type": "boolean",
            "default": false,
            "description": "Shorten links in the caption, thread posts and first comment with the workspace's connected Bitly account when the post is published. Ignored if the workspace has no Bitly connected (the original links are posted). See \"Link shortening\" under Create post.",
            "example": true
          },
          "utmCampaign": {
            "type": "string",
            "nullable": true,
            "maxLength": 100,
            "description": "Value for `utm_campaign` added to links when `shortenLinks` is true. `utm_source` (the platform) and `utm_medium=social` are added automatically.",
            "example": "black_friday"
          }
        }
      },
      "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": "Media IDs to attach, from `GET /media` (API uploads or the workspace's dashboard library) or `POST /media/upload`. Library media must belong to this post's `workspaceId`. 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)"
          },
          "shortenLinks": {
            "type": "boolean",
            "default": false,
            "description": "Shorten links in the caption, thread posts and first comment with the workspace's connected Bitly account when the post is published. Ignored if the workspace has no Bitly connected (the original links are posted). See \"Link shortening\" under Create post.",
            "example": true
          },
          "utmCampaign": {
            "type": "string",
            "nullable": true,
            "maxLength": 100,
            "description": "Value for `utm_campaign` added to links when `shortenLinks` is true. `utm_source` (the platform) and `utm_medium=social` are added automatically.",
            "example": "black_friday"
          }
        }
      },
      "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.\n",
            "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.\n",
            "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:\n- Instagram: 2,200 characters\n- Facebook: 8,000 characters\n- LinkedIn: 1,250 characters\n- Google Business: 4,096 characters\n\nReplying to a hidden comment is rejected with a `400` error.\n",
            "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.\n\nThis endpoint only works for social accounts with platform `PINTEREST`. Calling it on any other platform returns a `400` error.\n",
        "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": "includeMetrics",
            "in": "query",
            "required": false,
            "description": "Set to `true` to attach each post's latest performance metrics. Off by default\nso listings stay light. Adds no extra requests: metrics for the whole page are\nfetched in a single query.\n",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": true
          },
          {
            "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.\n\n## Workflow\n\n1. **Upload media**: `POST /media/upload` for each file. Save the returned `id` values.\n2. **Create the post**: call this endpoint with `mediaIds`, `scheduledAt`, and platform params.\n3. **Done**: the post is queued and will publish automatically at the scheduled time.\n\n**Media ordering:** For carousel posts (Instagram, TikTok, etc.), files appear in the order of the `mediaIds` array. List the IDs in the order you want them to appear.\n\n## Media rules by platform\n\n| Platform | Media required? | Limits | Notes |\n|----------|----------------|--------|-------|\n| Instagram | Yes | 1–10 images/videos | Stories: exactly 1 |\n| Facebook | No | Up to 10 images OR 1 video | Cannot mix types. Stories: exactly 1 |\n| X | No | Up to 4 images/videos | Text-only posts allowed |\n| YouTube | Yes | Exactly 1 video | `youtubeParams.title` is required |\n| TikTok | Yes | 1 video OR 1+ images | Cannot mix types |\n| LinkedIn | No | Up to 20 images OR 1 video OR 1 PDF | Cannot mix types |\n| Pinterest | Yes | Exactly 1 image or video | `pinterestParams.boardId` is required |\n| Threads | No | Up to 20 images/videos | Text-only posts allowed |\n| Bluesky | No | Up to 4 images OR 1 video | Cannot mix types |\n| Google Business | No | Up to 1 image | No video via API. Text-only posts allowed |\n\n## Platform-specific parameters\n\nSome platforms require or accept additional parameters. Pass them only for their matching platform: sending `youtubeParams` on an Instagram post will return a validation error.\n\n- **YouTube**: `youtubeParams` required: `title` (max 100 chars), optional `categoryId` (default \"22\"), `tags` (comma-separated, max 500 chars), `selfDeclaredMadeForKids`\n- **TikTok**: `tiktokParams` optional: `privacyLevel` (defaults to `PUBLIC_TO_EVERYONE`), `disableDuet`, `disableComment`, `disableStitch`, `isDraftType`, `isCommercialContent`, `isYourBrand`, `isBrandedContent`, `autoAddMusic`, `photosPostTitle` (max 90 chars, photo posts only)\n- **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)\n- **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`\n- **X**: `xCommunityId` optional, `xTaggedUsers` optional (comma-separated usernames, max 10, requires media)\n\n## Link shortening\n\nSet `shortenLinks: true` to shorten the links in a post with the workspace's Bitly account. Bitly is connected per workspace in the dashboard under Settings > Integrations. These fields work on every platform.\n\n- Links in the caption, thread posts and the first comment are shortened when the post is published, not when it is created, so the stored caption keeps the original links until then.\n- Each link gets `utm_source` (the platform, e.g. `instagram`) and `utm_medium=social`, plus `utm_campaign` if you set `utmCampaign`. Because `utm_source` differs per platform, each platform gets its own short link and its own click count in Bitly.\n- Links that are already Bitly links are left as they are.\n- If the workspace has no Bitly connected, or Bitly fails (for example the monthly link limit is reached), the post is still published with the original links and a warning is added to the post's logs.\n\n## Caption limits\n\nCaption length limits are enforced by each platform at publish time:\n\n| Platform | Max caption length |\n|----------|--------------------|\n| Instagram | 2,200 characters |\n| TikTok | 2,200 characters |\n| Threads | 500 characters |\n| Pinterest | 500 characters (description) |\n| Bluesky | 300 characters |\n| Google Business | 1,500 characters |\n| Facebook, X, YouTube, LinkedIn | No strict limit |\n\n## Stories\n\nSet `isStory: true` to post as a story. Only Instagram and Facebook support stories. Stories require exactly 1 media file.\n\n## Example\n\n```json\n{\n  \"workspaceId\": \"d35f525f-63e6-40ae-8946-8c120edb0225\",\n  \"socialAccountId\": \"c47eb231-8eb2-4600-ac24-6951c7b5e3ce\",\n  \"caption\": \"Check out our latest product launch!\",\n  \"scheduledAt\": \"2026-03-15T20:00:00.000Z\",\n  \"mediaIds\": [\"e1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6\"]\n}\n```\n",
        "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, 100/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. Includes the post's latest performance metrics in the `metrics` field.",
        "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.\n\nSend 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.\n\nPlatform-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.\n",
        "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.\n\nThe post is not deleted: its status changes to `CANCELLED` and it remains visible in your post list.\n",
        "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 media",
        "description": "Returns media you can attach to posts. **To list a workspace's media library\n(the files uploaded from the PostPlanify dashboard, which is usually what \"my\nmedia\" means), you must pass `workspaceId`.** Without it, only files uploaded\nthrough the API are returned (usually a small or empty subset). Each item has a\n`source` of `\"upload\"` or `\"library\"`; both can be used as `mediaIds` in\n`POST /posts`.\n",
        "tags": [
          "Media"
        ],
        "parameters": [
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Include this workspace's media library. Omit to return only your API uploads."
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "video"
              ]
            },
            "description": "Filter library media by type."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Library page number (default 1)."
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "Library items per page (default 50, max 100)."
          }
        ],
        "responses": {
          "200": {
            "description": "List of 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.\n\n**Allowed types:** JPEG, PNG, GIF, WebP, MP4, MOV, PDF\n\n**Max file size:** 100 MB\n",
        "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/from-url": {
      "post": {
        "operationId": "uploadMediaFromUrl",
        "summary": "Upload media from URL",
        "description": "Download a file from a public URL and store it, the same as an upload. Use the returned `id` in `mediaIds` when creating posts.\n\n**Allowed types:** JPEG, PNG, GIF, WebP, MP4, MOV, PDF\n\n**Max file size:** 100 MB\n",
        "tags": [
          "Media"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Public http(s) URL of the file. Redirects are followed.",
                    "example": "https://example.com/photo.jpg"
                  },
                  "filename": {
                    "type": "string",
                    "description": "Optional file name. Defaults to the name in the URL."
                  }
                }
              }
            }
          }
        },
        "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 or invalid URL, download failed, or file type not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "File larger than 100 MB",
            "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 daily view/engagement trends over the same window.\n",
        "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": {
                        "days": {
                          "type": "integer",
                          "description": "Window size (in days) used to compute the metrics in this response. Currently fixed to 7 for the public API.",
                          "example": 7
                        },
                        "accounts": {
                          "type": "array",
                          "description": "All social accounts with metrics over the response window",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "platform": {
                                "type": "string",
                                "example": "INSTAGRAM"
                              },
                              "accountName": {
                                "type": "string"
                              },
                              "profilePictureUrl": {
                                "type": "string",
                                "nullable": true
                              },
                              "customLabel": {
                                "type": "string",
                                "nullable": true,
                                "description": "User-set label to distinguish accounts (e.g. \"Downtown\")"
                              },
                              "analyticsLastSyncAt": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "currentFollowers": {
                                "type": "integer",
                                "nullable": true
                              },
                              "followersChange": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Follower change over the response window"
                              },
                              "followersChangePct": {
                                "type": "number",
                                "nullable": true
                              },
                              "views": {
                                "type": "integer"
                              },
                              "engagement": {
                                "type": "integer"
                              },
                              "avgEngagementRate": {
                                "type": "number",
                                "nullable": true
                              },
                              "postCount": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "topPosts": {
                          "type": "array",
                          "description": "Top 10 posts by views in the response window",
                          "items": {
                            "type": "object",
                            "properties": {
                              "platformPostId": {
                                "type": "string"
                              },
                              "platform": {
                                "type": "string"
                              },
                              "text": {
                                "type": "string",
                                "nullable": true
                              },
                              "thumbnailUrl": {
                                "type": "string",
                                "nullable": true
                              },
                              "postUrl": {
                                "type": "string",
                                "nullable": true,
                                "description": "Direct link to the post on the source platform (null when the platform doesn't expose one)"
                              },
                              "label": {
                                "$ref": "#/components/schemas/PostLabel",
                                "nullable": true,
                                "description": "The content-pillar label assigned to this post (null if none or if the post wasn't published via PostPlanify)"
                              },
                              "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 over the response window",
                          "items": {
                            "type": "object",
                            "properties": {
                              "date": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "views": {
                                "type": "integer"
                              },
                              "engagement": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "followerSnapshots": {
                          "type": "array",
                          "description": "Daily follower snapshots over the response window"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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).\n\nThe 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.\n",
        "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:\n- `meta.lastUpdated`: when analytics were last synced\n- Post items include `engagementRatePct` and `totalInteractions`\n- Post items include `internalLabel` (`{ name, color }` or `null`) when the post was published via PostPlanify and has a content-pillar label assigned\n- Account/user objects include follower and following counts\n"
                    }
                  }
                }
              }
            }
          },
          "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.\n\nUse the `days` query parameter to control the time range (1-365 days, default 30).\n",
        "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**.\n\nComments 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.\n\n## Ordering\n\nComments are returned newest first (by platform `timestamp`).\n\n## Subscription\n\nThis endpoint requires the workspace owner to have an active Growth, Premium, or Scale subscription.\n",
        "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).\n\nThe `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`.\n",
        "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).\n\n## Requirements\n\n- The comment's social account must still be connected and have the scopes required for the platform:\n  - **Instagram**: `instagram_manage_comments` or `instagram_business_manage_comments`\n  - **Facebook**: `pages_manage_engagement`\n  - **LinkedIn**: no extra scopes required\n  - **Google Business**: no extra scopes required\n- The comment must not be hidden (`isHidden: false`). Unhide it in the PostPlanify dashboard first if needed.\n- The reply must respect the platform's length limit (see `message` description).\n\n## Success response\n\nReturns the new reply's platform-native ID. The reply is also persisted to PostPlanify's inbox and `brandReplied` is set on the parent comment.\n\n## LinkedIn URN encoding\n\nFor 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`.\n",
        "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.\n"
    },
    {
      "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.\n"
    },
    {
      "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.\n"
    },
    {
      "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.\n"
    },
    {
      "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.\n"
    },
    {
      "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.\n"
    }
  ]
}
