openapi: 3.1.0
info:
  title: Late API
  version: "1.0.0"
  description: |
    API reference for Late. Authenticate with a Bearer API key.
    Base URL: https://getlate.dev/api

servers:
  - url: https://getlate.dev/api
    description: Production
  - url: http://localhost:3000/api
    description: Local
tags:
  - name: Posts
  - name: Users
  - name: Usage
  - name: Profiles
  - name: Accounts
  - name: Account Groups
  - name: API Keys
  - name: Invites
  - name: Connect
  - name: Media
  # - name: Video Clips  # AI Clipping feature temporarily disabled
  - name: Reddit Search
  - name: Facebook
  - name: GMB Reviews
  - name: LinkedIn Mentions
  - name: Pinterest
  - name: TikTok
  - name: Queue
  - name: Analytics
  - name: Tools
    description: |
      Media download and utility tools. Available to paid plans only.
      Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
      All responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.
  - name: Webhooks
    description: |
      Configure webhooks to receive real-time notifications about post status changes and account events.

      **Available Events:**
      - `post.scheduled` - When a post is successfully scheduled
      - `post.published` - When a post is successfully published
      - `post.failed` - When a post fails to publish on all platforms
      - `post.partial` - When a post publishes to some platforms but fails on others
      - `account.disconnected` - When a social account is disconnected (token expired/revoked)

      **Security:**
      - Optional HMAC-SHA256 signature sent in `X-Late-Signature` header
      - Configure a secret key in webhook settings to enable signature verification
      - Custom headers can be added to webhook requests for additional authentication
  - name: Logs
    description: |
      Publishing logs for transparency and debugging. Shows detailed records of all post publishing attempts.

      **Log Data Includes:**
      - Platform API endpoint called
      - HTTP status code
      - Request body (content preview, media URLs)
      - Response body (platform post ID/URL or error details)
      - Duration and retry attempts

      **Retention:** Logs are automatically deleted after 7 days via TTL index.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: API key authentication - use your Late API key as a Bearer token
    connectToken:
      type: apiKey
      in: header
      name: X-Connect-Token
      description: |
        Short-lived connect token for API users during OAuth flows. 
        Automatically generated when initiating OAuth without a browser session.
        Valid for 15 minutes. Used to authenticate Facebook page selection API calls.
  parameters:
    PageParam:
      name: page
      in: query
      description: Page number (1-based)
      schema: { type: integer, minimum: 1, default: 1 }
    LimitParam:
      name: limit
      in: query
      description: Page size
      schema: { type: integer, minimum: 1, maximum: 100, default: 10 }
  responses:
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Unauthorized
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Not found
  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
        details:
          type: object
          additionalProperties: true
    Webhook:
      type: object
      description: Individual webhook configuration for receiving real-time notifications
      properties:
        _id:
          type: string
          description: Unique webhook identifier
        name:
          type: string
          description: Webhook name (for identification)
          maxLength: 50
        url:
          type: string
          format: uri
          description: Webhook endpoint URL
        secret:
          type: string
          description: Secret key for HMAC-SHA256 signature (not returned in responses for security)
        events:
          type: array
          items:
            type: string
            enum: [post.scheduled, post.published, post.failed, post.partial, account.disconnected]
          description: Events subscribed to
        isActive:
          type: boolean
          description: Whether webhook delivery is enabled
        lastFiredAt:
          type: string
          format: date-time
          description: Timestamp of last successful webhook delivery
        failureCount:
          type: integer
          description: Consecutive delivery failures (resets on success, webhook disabled at 10)
        customHeaders:
          type: object
          additionalProperties:
            type: string
          description: Custom headers included in webhook requests
    WebhookLog:
      type: object
      description: Webhook delivery log entry
      properties:
        _id:
          type: string
        webhookId:
          type: string
          description: ID of the webhook that was triggered
        webhookName:
          type: string
          description: Name of the webhook that was triggered
        event:
          type: string
          enum: [post.scheduled, post.published, post.failed, post.partial, account.disconnected, webhook.test]
        url:
          type: string
          format: uri
        status:
          type: string
          enum: [success, failed]
        statusCode:
          type: integer
          description: HTTP status code from webhook endpoint
        requestPayload:
          type: object
          description: Payload sent to webhook endpoint
        responseBody:
          type: string
          description: Response body from webhook endpoint (truncated to 10KB)
        errorMessage:
          type: string
          description: Error message if delivery failed
        attemptNumber:
          type: integer
          description: Delivery attempt number (max 3 retries)
        responseTime:
          type: integer
          description: Response time in milliseconds
        createdAt:
          type: string
          format: date-time
    WebhookPayloadPost:
      type: object
      description: Webhook payload for post events
      properties:
        event:
          type: string
          enum: [post.scheduled, post.published, post.failed, post.partial]
        post:
          type: object
          properties:
            id:
              type: string
            content:
              type: string
            status:
              type: string
            scheduledFor:
              type: string
              format: date-time
            publishedAt:
              type: string
              format: date-time
            platforms:
              type: array
              items:
                type: object
                properties:
                  platform:
                    type: string
                  status:
                    type: string
                  publishedUrl:
                    type: string
                  error:
                    type: string
        timestamp:
          type: string
          format: date-time
    WebhookPayloadAccountDisconnected:
      type: object
      description: Webhook payload for account disconnected events
      properties:
        event:
          type: string
          enum: [account.disconnected]
        account:
          type: object
          properties:
            accountId:
              type: string
              description: The account's unique identifier (same as used in /v1/accounts/{accountId})
            profileId:
              type: string
              description: The profile's unique identifier this account belongs to
            platform:
              type: string
            username:
              type: string
            displayName:
              type: string
            disconnectionType:
              type: string
              enum: [intentional, unintentional]
              description: Whether the disconnection was intentional (user action) or unintentional (token expired/revoked)
            reason:
              type: string
              description: Human-readable reason for the disconnection
        timestamp:
          type: string
          format: date-time
    PostLog:
      type: object
      description: Publishing log entry showing details of a post publishing attempt
      properties:
        _id:
          type: string
        postId:
          oneOf:
            - type: string
            - type: object
              description: Populated post reference
              properties:
                _id:
                  type: string
                content:
                  type: string
                status:
                  type: string
        userId:
          type: string
        profileId:
          type: string
        platform:
          type: string
          enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram]
        accountId:
          type: string
        accountUsername:
          type: string
        action:
          type: string
          enum: [publish, retry, media_upload, rate_limit_pause, token_refresh, cancelled]
          description: |
            Type of action logged:
            - `publish` - Initial publish attempt
            - `retry` - Retry after failure
            - `media_upload` - Media upload step
            - `rate_limit_pause` - Account paused due to rate limits
            - `token_refresh` - Token was refreshed
            - `cancelled` - Post was cancelled
        status:
          type: string
          enum: [success, failed, pending, skipped]
        statusCode:
          type: integer
          description: HTTP status code from platform API
        endpoint:
          type: string
          description: Platform API endpoint called
        request:
          type: object
          properties:
            contentPreview:
              type: string
              description: First 200 chars of caption
            mediaCount:
              type: integer
            mediaTypes:
              type: array
              items:
                type: string
            mediaUrls:
              type: array
              items:
                type: string
              description: URLs of media items sent to platform
            scheduledFor:
              type: string
              format: date-time
            rawBody:
              type: string
              description: Full request body JSON (max 5000 chars)
        response:
          type: object
          properties:
            platformPostId:
              type: string
              description: ID returned by platform on success
            platformPostUrl:
              type: string
              description: URL of published post
            errorMessage:
              type: string
              description: Error message on failure
            errorCode:
              type: string
              description: Platform-specific error code
            rawBody:
              type: string
              description: Full response body JSON (max 5000 chars)
        durationMs:
          type: integer
          description: How long the operation took in milliseconds
        attemptNumber:
          type: integer
          description: Attempt number (1 for first try, 2+ for retries)
        createdAt:
          type: string
          format: date-time
    PostLogDetail:
      allOf:
        - $ref: '#/components/schemas/PostLog'
        - type: object
          description: Detailed log entry with populated references
          properties:
            postId:
              type: object
              description: Populated post with full details
              properties:
                _id:
                  type: string
                content:
                  type: string
                status:
                  type: string
                scheduledFor:
                  type: string
                  format: date-time
                platforms:
                  type: array
                  items:
                    type: object
                mediaItems:
                  type: array
                  items:
                    type: object
            accountId:
              type: object
              description: Populated account reference
              properties:
                _id:
                  type: string
                platform:
                  type: string
                username:
                  type: string
                displayName:
                  type: string
            metadata:
              type: object
              description: Additional metadata (e.g., rate limit info)
    MediaItem:
      type: object
      description: |
        Media referenced in posts. URLs must be publicly reachable over HTTPS by the destination platforms.
        When using third‑party storage, ensure signed links remain valid until upload completes.

        **Uploading Media:**
        Use `POST /v1/media/presign` to get a presigned URL, then upload your file directly to cloud storage.
        Supports files up to 5GB. See the `/v1/media/presign` endpoint documentation for details.

        **Automatic Compression:**
        - Bluesky: images larger than ~1MB are automatically recompressed to meet the platform's blob size limit.
        - Instagram: images >8 MB and videos >100 MB (stories) or >300 MB (reels) are automatically compressed.
      properties:
        type:
          type: string
          enum: [image, video, gif, document]
        url:
          type: string
          format: uri
        filename:
          type: string
        size:
          type: integer
          description: Optional file size in bytes
        mimeType:
          type: string
          description: Optional MIME type (e.g. image/jpeg, video/mp4)
        thumbnail:
          type: string
          format: uri
          description: Optional thumbnail image URL for videos
        instagramThumbnail:
          type: string
          format: uri
          description: Optional custom cover image URL for Instagram Reels
        tiktokProcessed:
          type: boolean
          description: Internal flag indicating the image was resized for TikTok
    PlatformTarget:
      type: object
      properties:
        platform:
          type: string
          example: twitter
          description: "Supported values: twitter, threads, instagram, youtube, facebook, linkedin, pinterest, reddit, tiktok, bluesky, googlebusiness, telegram"
        accountId:
          oneOf:
            - type: string
            - $ref: '#/components/schemas/SocialAccount'
        customContent:
          type: string
        customMedia:
          type: array
          items:
            $ref: '#/components/schemas/MediaItem'
        scheduledFor:
          type: string
          format: date-time
          description: Optional per-platform scheduled time override (uses post.scheduledFor when omitted)
        platformSpecificData:
          description: Platform-specific overrides and options.
          oneOf:
            - $ref: '#/components/schemas/TwitterPlatformData'
            - $ref: '#/components/schemas/ThreadsPlatformData'
            - $ref: '#/components/schemas/FacebookPlatformData'
            - $ref: '#/components/schemas/InstagramPlatformData'
            - $ref: '#/components/schemas/LinkedInPlatformData'
            - $ref: '#/components/schemas/PinterestPlatformData'
            - $ref: '#/components/schemas/YouTubePlatformData'
            - $ref: '#/components/schemas/GoogleBusinessPlatformData'
            - $ref: '#/components/schemas/TikTokPlatformData'
            - $ref: '#/components/schemas/TelegramPlatformData'
          additionalProperties: true
        status:
          type: string
          example: pending
          description: "Platform-specific status: pending, publishing, published, failed"
        platformPostId:
          type: string
          description: The native post ID on the platform (populated after successful publish)
          example: "1234567890123456789"
        platformPostUrl:
          type: string
          format: uri
          description: |
            Public URL of the published post on the platform.
            Populated after successful publish. For immediate posts (publishNow=true), 
            this is included in the response. For scheduled posts, fetch the post 
            via GET /v1/posts/{postId} after the scheduled time.
          example: "https://twitter.com/acmecorp/status/1234567890123456789"
        publishedAt:
          type: string
          format: date-time
          description: Timestamp when the post was published to this platform
    Post:
      type: object
      properties:
        _id: { type: string }
        userId:
          oneOf:
            - type: string
            - $ref: '#/components/schemas/User'
        title:
          type: string
          description: |
            YouTube: title must be ≤ 100 characters.
        content: { type: string }
        mediaItems:
          type: array
          items: { $ref: '#/components/schemas/MediaItem' }
        platforms:
          type: array
          items: { $ref: '#/components/schemas/PlatformTarget' }
        scheduledFor: { type: string, format: date-time }
        timezone: { type: string }
        status: { type: string, enum: [draft, scheduled, publishing, published, failed, partial] }
        tags:
          type: array
          description: |
            YouTube tag constraints when targeting YouTube:
            - No count cap; duplicates removed.
            - Each tag must be ≤ 100 chars.
            - Combined characters across all tags ≤ 500.
          items: { type: string }
        hashtags:
          type: array
          items: { type: string }
        mentions:
          type: array
          items: { type: string }
        visibility: { type: string, enum: [public, private, unlisted] }
        metadata:
          type: object
          additionalProperties: true
        queuedFromProfile:
          type: string
          description: Profile ID if the post was scheduled via the queue
        queueId:
          type: string
          description: Queue ID if the post was scheduled via a specific queue
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    TwitterPlatformData:
      type: object
      properties:
        threadItems:
          type: array
          description: Sequence of tweets in a thread. First item is the root tweet.
          items:
            type: object
            properties:
              content: { type: string }
              mediaItems:
                type: array
                items: { $ref: '#/components/schemas/MediaItem' }

    ThreadsPlatformData:
      type: object
      properties:
        threadItems:
          type: array
          description: Sequence of posts in a Threads thread (root then replies in order).
          items:
            type: object
            properties:
              content: { type: string }
              mediaItems:
                type: array
                items: { $ref: '#/components/schemas/MediaItem' }
      description: |
        Constraints:
        - Carousel posts support up to 10 images (no videos in carousels).
        - Single posts support one image or one video.
        - Videos must be H.264/AAC MP4 format, max 5 minutes duration.
        - Images must be JPEG or PNG, max 8 MB each.
        - threadItems creates a reply chain (Threads equivalent of Twitter threads).

    FacebookPlatformData:
      type: object
      properties:
        contentType:
          type: string
          enum: [story]
          description: Set to 'story' to publish as a Facebook Page Story (24-hour ephemeral content). Requires media.
        firstComment:
          type: string
          description: Optional first comment to post immediately after publishing (feed posts only, not stories)
        pageId:
          type: string
          description: Target Facebook Page ID. If omitted, uses the selected/default page on the connection.
      description: |
        Constraints:
        - Posts cannot mix videos and images.
        - Multiple images supported via attached_media (up to 10 images for feed posts).
        - Multiple videos in the same post are not supported.
        - Stories require media (single image or video); text captions are not displayed with stories.
        - Stories are ephemeral (disappear after 24 hours).

    InstagramPlatformData:
      type: object
      properties:
        contentType:
          type: string
          enum: [story]
          description: Set to 'story' to publish as a Story. Default posts become Reels or feed depending on media.
        shareToFeed:
          type: boolean
          default: true
          description: For Reels only. When true (default), the Reel appears on both the Reels tab and your main profile feed. Set to false to post to the Reels tab only.
        collaborators:
          type: array
          items: { type: string }
          description: Up to 3 Instagram usernames to invite as collaborators (feed/Reels only)
        firstComment:
          type: string
          description: Optional first comment to add after the post is created (not applied to Stories)
        trialParams:
          type: object
          description: |
            Trial Reels configuration. Trial reels are only shared to non-followers initially.
            They can later be "graduated" (converted to regular reels visible to followers)
            either manually in the Instagram app or automatically based on performance.
            Only applies to Reels (video posts).
          properties:
            graduationStrategy:
              type: string
              enum: [MANUAL, SS_PERFORMANCE]
              description: |
                The graduation strategy specifies when a trial reel becomes a regular reel:
                - MANUAL: The trial reel can only be manually graduated from the native Instagram app.
                - SS_PERFORMANCE: The trial reel will be automatically graduated if it performs well with non-followers.
        userTags:
          type: array
          description: Tag Instagram users in photos by username and position coordinates. Only works for single image posts and the first image of carousel posts. Not supported for stories or videos.
          items:
            type: object
            required: [username, x, y]
            properties:
              username:
                type: string
                description: Instagram username (@ symbol is optional and will be removed automatically)
                example: friend_username
              x:
                type: number
                minimum: 0
                maximum: 1
                description: X coordinate position from left edge (0.0 = left, 0.5 = center, 1.0 = right)
                example: 0.5
              y:
                type: number
                minimum: 0
                maximum: 1
                description: Y coordinate position from top edge (0.0 = top, 0.5 = center, 1.0 = bottom)
                example: 0.5
        audioName:
          type: string
          description: |
            Custom name for the original audio in Reels. Replaces the default "Original Audio" label.
            Only applies to Reels (video posts). Can only be set once - either during creation or
            later from the Instagram audio page in the app.
          example: "My Podcast Intro"
      description: |
        Constraints:
        - Feed posts require images with aspect ratio between 0.8 (4:5 portrait) and 1.91 (1.91:1 landscape).
        - Images outside this range (e.g., 9:16 Stories/TikTok format) must use contentType 'story'.
        - Validation happens at post creation; invalid images are rejected immediately with helpful error messages.
        - Carousels support up to 10 media items.
        - Stories require media; no captions are published with Stories.
        - User tags: coordinates range from 0.0 to 1.0 representing position from top-left corner. Tagged users receive notifications.
        
        **Automatic Compression (similar to Bluesky):**
        - All images (story, post, carousel, thumbnails) exceeding 8 MB are automatically compressed using quality reduction and resizing.
        - Story videos exceeding 100 MB are automatically compressed.
        - Reel videos exceeding 300 MB are automatically compressed.
        - Compression uses Sharp (images) and FFmpeg (videos) to maintain quality while meeting size limits.
        - Original files are preserved; compressed versions are uploaded to blob storage automatically.

    LinkedInPlatformData:
      type: object
      properties:
        firstComment:
          type: string
          description: Optional first comment to add after the post is created
        disableLinkPreview:
          type: boolean
          description: Set to true to disable automatic link previews for URLs in the post content (default is false)
      description: |
        Constraints:
        - Multi-image posts support up to 20 images.
        - Multi-video posts are not supported.
        - Single PDF document posts are supported (max 100MB, ~300 pages). Documents cannot be mixed with other media.
        - Post ID is returned in the x-restli-id response header.
        - Link previews are automatically generated for URLs when no media is attached (can be disabled with disableLinkPreview).

    PinterestPlatformData:
      type: object
      properties:
        title:
          type: string
          maxLength: 100
          description: Pin title. Defaults to first line of content or "Pin". Must be ≤ 100 characters.
        boardId:
          type: string
          description: Target Pinterest board ID. If omitted, the first available board is used.
        link:
          type: string
          format: uri
          description: Destination link (pin URL)
        coverImageUrl:
          type: string
          format: uri
          description: Optional cover image for video pins
        coverImageKeyFrameTime:
          type: integer
          description: Optional key frame time in seconds for derived video cover

    YouTubePlatformData:
      type: object
      properties:
        title:
          type: string
          maxLength: 100
          description: Video title. Defaults to first line of content or "Untitled Video". Must be ≤ 100 characters.
        visibility:
          type: string
          enum: [public, private, unlisted]
          default: public
          description: |
            Video visibility setting:
            - public: Anyone can search for and watch (default)
            - unlisted: Only people with the link can watch
            - private: Only you and people you specifically share with can watch
        firstComment:
          type: string
          maxLength: 10000
          description: Optional first comment to post immediately after video upload. Up to 10,000 characters (YouTube's comment limit).
      description: |
        YouTube video upload settings:
        - Videos ≤ 3 minutes are automatically detected as YouTube Shorts
        - Videos > 3 minutes become regular YouTube videos
        - Custom thumbnails supported for regular videos (via mediaItem.thumbnail)
        - Custom thumbnails NOT supported for Shorts via API
        - Scheduled videos are uploaded immediately as the specified visibility and published at scheduled time
        - Visibility defaults to "public" if not specified

    GoogleBusinessPlatformData:
      type: object
      properties:
        callToAction:
          type: object
          description: Optional call-to-action button displayed on the post
          properties:
            type:
              type: string
              enum: [LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL]
              description: |
                Button action type:
                - LEARN_MORE: Link to more information
                - BOOK: Booking/reservation link
                - ORDER: Online ordering link
                - SHOP: E-commerce/shopping link
                - SIGN_UP: Registration/signup link
                - CALL: Phone call action
            url:
              type: string
              format: uri
              description: Destination URL for the CTA button (required when callToAction is provided)
          required: [type, url]
      description: |
        Google Business Profile post settings:
        - Posts support text content and a single image (no videos)
        - Images must be publicly accessible URLs
        - Call-to-action buttons drive user engagement
        - Posts appear on your Google Business Profile and in Google Search/Maps

    TikTokPlatformData:
      type: object
      description: |
        TikTok platform-specific settings for video/photo posting.

        **Constraints:**
        - Photo carousels support up to 35 images.
        - **Title length limits**:
          - Videos: up to 2200 chars (full content used as title)
          - Photos: content is automatically truncated to 90 chars for title (hashtags/URLs stripped). Use 'description' field for longer text (up to 4000 chars).
        - privacyLevel must be chosen from creator_info.privacy_level_options (no defaulting).
        - allowDuet and allowStitch required for videos; allowComment for all.
        - contentPreviewConfirmed and expressConsentGiven must be true before posting.

        **Note:** Both camelCase and snake_case field names are accepted for backwards compatibility.
        The nested `tiktokSettings` object format is also still supported but deprecated.
      properties:
        draft:
          type: boolean
          description: >
            When true, Late sends the post to the TikTok Creator Inbox as a draft instead of publishing it immediately.
            When omitted or false, TikTok uses direct posting (live publish) as usual.
        privacyLevel:
          type: string
          description: One of the values returned by the TikTok creator info API for the account
        allowComment:
          type: boolean
          description: Allow comments on the post
        allowDuet:
          type: boolean
          description: Allow duets (required for video posts)
        allowStitch:
          type: boolean
          description: Allow stitches (required for video posts)
        commercialContentType:
          type: string
          enum: [none, brand_organic, brand_content]
          description: Type of commercial content disclosure
        brandPartnerPromote:
          type: boolean
          description: Whether the post promotes a brand partner
        isBrandOrganicPost:
          type: boolean
          description: Whether the post is a brand organic post
        contentPreviewConfirmed:
          type: boolean
          description: User has confirmed they previewed the content
        expressConsentGiven:
          type: boolean
          description: User has given express consent for posting
        mediaType:
          type: string
          enum: [video, photo]
          description: Optional override. Defaults based on provided media items.
        videoCoverTimestampMs:
          type: integer
          description: Optional for video posts. Timestamp in milliseconds to select which frame to use as thumbnail (defaults to 1000ms/1 second).
          minimum: 0
        photoCoverIndex:
          type: integer
          description: Optional for photo carousels. Index of image to use as cover, 0-based (defaults to 0/first image).
          minimum: 0
        autoAddMusic:
          type: boolean
          description: When true, TikTok may add recommended music (photos only)
        videoMadeWithAi:
          type: boolean
          description: Set true to disclose AI-generated content
        description:
          type: string
          maxLength: 4000
          description: |
            Optional long-form description for photo posts (max 4000 chars).
            Recommended for photo posts when content exceeds 90 characters, as photo titles are automatically truncated to 90 chars (after stripping hashtags/URLs).

    TikTokSettings:
      type: object
      deprecated: true
      description: |
        **DEPRECATED**: Use flat properties directly in TikTokPlatformData instead.
        This nested format is supported for backwards compatibility only.
      properties:
        privacyLevel: { type: string }
        allowComment: { type: boolean }
        allowDuet: { type: boolean }
        allowStitch: { type: boolean }
        commercialContentType: { type: string }
        brandPartnerPromote: { type: boolean }
        isBrandOrganicPost: { type: boolean }
        contentPreviewConfirmed: { type: boolean }
        expressConsentGiven: { type: boolean }
        mediaType: { type: string }
        videoCoverTimestampMs: { type: integer }
        photoCoverIndex: { type: integer }
        autoAddMusic: { type: boolean }
        videoMadeWithAi: { type: boolean }
        description: { type: string }
    TelegramPlatformData:
      type: object
      properties:
        parseMode:
          type: string
          enum: [HTML, Markdown, MarkdownV2]
          description: Text formatting mode for the message (default is HTML)
        disableWebPagePreview:
          type: boolean
          description: Disable link preview generation for URLs in the message
        disableNotification:
          type: boolean
          description: Send the message silently (users will receive notification without sound)
        protectContent:
          type: boolean
          description: Protect message content from forwarding and saving
      description: |
        Telegram channel/group posting settings:
        - Supports text, images (up to 10), videos (up to 10), and mixed media albums
        - Posts to channels display the channel name and logo as author
        - Posts to groups display the bot name (Late) as author
        - Message IDs are returned for analytics tracking
        - Captions support up to 1024 characters for media posts, 4096 for text-only

        **Analytics:**
        - **Not available via API.** The Telegram Bot API does not expose message analytics (views, forwards, reactions).
        - View counts are only visible to channel admins directly in the Telegram app.
        - This is a Telegram platform limitation that affects all third-party tools.
    QueueSlot:
      type: object
      properties:
        dayOfWeek:
          type: integer
          description: Day of week (0=Sunday, 6=Saturday)
          minimum: 0
          maximum: 6
        time:
          type: string
          description: Time in HH:mm format (24-hour)
          pattern: '^([0-1][0-9]|2[0-3]):[0-5][0-9]$'
    QueueSchedule:
      type: object
      properties:
        _id:
          type: string
          description: Unique queue identifier
        profileId:
          type: string
          description: Profile ID this queue belongs to
        name:
          type: string
          description: Queue name (e.g., "Morning Posts", "Evening Content")
        timezone:
          type: string
          description: IANA timezone (e.g., America/New_York)
        slots:
          type: array
          items:
            $ref: '#/components/schemas/QueueSlot'
        active:
          type: boolean
          description: Whether the queue is active
        isDefault:
          type: boolean
          description: Whether this is the default queue for the profile (used when no queueId specified)
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Pagination:
      type: object
      properties:
        page: { type: integer }
        limit: { type: integer }
        total: { type: integer }
        pages: { type: integer }
    Profile:
      type: object
      properties:
        _id: { type: string }
        userId: { type: string }
        name: { type: string }
        description: { type: string }
        color: { type: string }
        isDefault: { type: boolean }
        isOverLimit:
          type: boolean
          description: |
            Only present when `includeOverLimit=true` is used. Indicates if this profile
            exceeds the user's plan limit. Over-limit profiles cannot be used for posting
            but can be managed (disconnected accounts, deleted).
        createdAt: { type: string, format: date-time }
    SocialAccount:
      type: object
      properties:
        _id: { type: string }
        platform: { type: string }
        profileId:
          oneOf:
            - type: string
            - $ref: '#/components/schemas/Profile'
        username: { type: string }
        displayName: { type: string }
        profileUrl:
          type: string
          description: |
            Full profile URL for the connected account. Available for all platforms:
            - Twitter/X: https://x.com/{username}
            - Instagram: https://instagram.com/{username}
            - TikTok: https://tiktok.com/@{username}
            - YouTube: https://youtube.com/@{handle} or https://youtube.com/channel/{id}
            - LinkedIn Personal: https://www.linkedin.com/in/{vanityName}/
            - LinkedIn Organization: https://www.linkedin.com/company/{vanityName}/
            - Threads: https://threads.net/@{username}
            - Pinterest: https://pinterest.com/{username}
            - Reddit: https://reddit.com/user/{username}
            - Bluesky: https://bsky.app/profile/{handle}
            - Facebook: https://facebook.com/{username} or https://facebook.com/{pageId}
            - Google Business: Google Maps URL for the business location
        isActive: { type: boolean }
        followersCount:
          type: number
          description: Follower count (only included if user has analytics add-on)
        followersLastUpdated:
          type: string
          format: date-time
          description: Last time follower count was updated (only included if user has analytics add-on)
    AccountWithFollowerStats:
      allOf:
        - $ref: '#/components/schemas/SocialAccount'
        - type: object
          properties:
            profilePicture: { type: string }
            currentFollowers: { type: number, description: Current follower count }
            lastUpdated: { type: string, format: date-time }
            growth: { type: number, description: Follower change over period }
            growthPercentage: { type: number, description: Percentage growth }
            dataPoints: { type: number, description: Number of historical snapshots }
    ApiKey:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        keyPreview: { type: string }
        expiresAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
        key:
          type: string
          description: Returned only once, on creation
    UsageStats:
      type: object
      properties:
        planName: { type: string }
        billingPeriod: { type: string, enum: [monthly, yearly] }
        signupDate: { type: string, format: date-time }
        limits:
          type: object
          properties:
            uploads: { type: integer }
            profiles: { type: integer }
        usage:
          type: object
          properties:
            uploads: { type: integer }
            profiles: { type: integer }
            lastReset: { type: string, format: date-time }
    # VideoClipJob, VideoClip, VideoClipJobProcessing, VideoClipJobCompleted, VideoClipJobFailed, VideoClipUsageStats
    # schemas removed - AI Clipping feature temporarily disabled
    PostAnalytics:
      type: object
      properties:
        impressions: { type: integer, example: 0 }
        reach: { type: integer, example: 0 }
        likes: { type: integer, example: 0 }
        comments: { type: integer, example: 0 }
        shares: { type: integer, example: 0 }
        clicks: { type: integer, example: 0 }
        views: { type: integer, example: 0 }
        engagementRate: { type: number, example: 0 }
        lastUpdated: { type: string, format: date-time }
    PlatformAnalytics:
      type: object
      properties:
        platform: { type: string }
        status: { type: string }
        accountId: { type: string }
        accountUsername: { type: string }
        analytics:
          $ref: '#/components/schemas/PostAnalytics'
        accountMetrics:
          type: object
          nullable: true
          properties:
            followers: { type: integer, nullable: true, description: 'Followers/fans count (e.g., Instagram, Facebook Pages, Twitter)' }
            subscribers: { type: integer, nullable: true, description: 'Subscribers count (e.g., YouTube)' }
            lastUpdated: { type: string, format: date-time, nullable: true }
    AnalyticsOverview:
      type: object
      properties:
        totalPosts: { type: integer }
        publishedPosts: { type: integer }
        scheduledPosts: { type: integer }
        lastSync: { type: string, format: date-time }
    AnalyticsSinglePostResponse:
      type: object
      properties:
        postId: { type: string }
        status: { type: string }
        content: { type: string }
        scheduledFor: { type: string, format: date-time }
        publishedAt: { type: string, format: date-time }
        analytics:
          $ref: '#/components/schemas/PostAnalytics'
        platformAnalytics:
          type: array
          items:
            $ref: '#/components/schemas/PlatformAnalytics'
        platform: { type: string }
        platformPostUrl: { type: string, format: uri }
        isExternal: { type: boolean }
    AnalyticsListResponse:
      type: object
      properties:
        overview:
          $ref: '#/components/schemas/AnalyticsOverview'
        posts:
          type: array
          items:
            type: object
            properties:
              _id: { type: string }
              content: { type: string }
              scheduledFor: { type: string, format: date-time }
              publishedAt: { type: string, format: date-time }
              status: { type: string }
              analytics:
                $ref: '#/components/schemas/PostAnalytics'
              platforms:
                type: array
                items:
                  $ref: '#/components/schemas/PlatformAnalytics'
              platform: { type: string }
              platformPostUrl: { type: string, format: uri }
              isExternal: { type: boolean }
              thumbnailUrl: { type: string, format: uri }
              mediaType: { type: string, enum: [image, video, gif, document] }
              mediaItems:
                type: array
                items:
                  $ref: '#/components/schemas/MediaItem'
        pagination:
          $ref: '#/components/schemas/Pagination'
        accounts:
          type: array
          description: Connected social accounts (followerCount and followersLastUpdated only included if user has analytics add-on)
          items:
            $ref: '#/components/schemas/SocialAccount'
        hasAnalyticsAccess:
          type: boolean
          description: Whether user has analytics add-on access
    # LinkedIn Aggregate Analytics Responses
    LinkedInAggregateAnalyticsTotalResponse:
      type: object
      description: Response for TOTAL aggregation (lifetime totals)
      properties:
        accountId: { type: string }
        platform: { type: string, example: linkedin }
        accountType: { type: string, example: personal }
        username: { type: string }
        aggregation: { type: string, enum: [TOTAL] }
        dateRange:
          type: object
          nullable: true
          properties:
            startDate: { type: string, format: date }
            endDate: { type: string, format: date }
        analytics:
          type: object
          properties:
            impressions: { type: integer, description: Total impressions across all posts }
            reach: { type: integer, description: Unique members reached across all posts }
            reactions: { type: integer, description: Total reactions across all posts }
            comments: { type: integer, description: Total comments across all posts }
            shares: { type: integer, description: Total reshares across all posts }
            engagementRate: { type: number, description: Overall engagement rate as percentage }
        note: { type: string }
        lastUpdated: { type: string, format: date-time }
    LinkedInAggregateAnalyticsDailyResponse:
      type: object
      description: Response for DAILY aggregation (time series breakdown)
      properties:
        accountId: { type: string }
        platform: { type: string, example: linkedin }
        accountType: { type: string, example: personal }
        username: { type: string }
        aggregation: { type: string, enum: [DAILY] }
        dateRange:
          type: object
          nullable: true
          properties:
            startDate: { type: string, format: date }
            endDate: { type: string, format: date }
        analytics:
          type: object
          description: |
            Daily breakdown of each metric. Each metric contains an array of date/count pairs.
            Note: 'reach' (MEMBERS_REACHED) is not available with DAILY aggregation per LinkedIn API limitations.
          properties:
            impressions:
              type: array
              items:
                type: object
                properties:
                  date: { type: string, format: date }
                  count: { type: integer }
            reactions:
              type: array
              items:
                type: object
                properties:
                  date: { type: string, format: date }
                  count: { type: integer }
            comments:
              type: array
              items:
                type: object
                properties:
                  date: { type: string, format: date }
                  count: { type: integer }
            shares:
              type: array
              items:
                type: object
                properties:
                  date: { type: string, format: date }
                  count: { type: integer }
        skippedMetrics:
          type: array
          description: Metrics that were skipped due to API limitations
          items: { type: string }
        note: { type: string }
        lastUpdated: { type: string, format: date-time }
    # ============================================
    # Response Schemas
    # ============================================
    # Posts Responses
    PostsListResponse:
      type: object
      properties:
        posts:
          type: array
          items:
            $ref: '#/components/schemas/Post'
        pagination:
          $ref: '#/components/schemas/Pagination'
    PostGetResponse:
      type: object
      properties:
        post:
          $ref: '#/components/schemas/Post'
    PostCreateResponse:
      type: object
      properties:
        message:
          type: string
        post:
          $ref: '#/components/schemas/Post'
    PostUpdateResponse:
      type: object
      properties:
        message:
          type: string
        post:
          $ref: '#/components/schemas/Post'
    PostDeleteResponse:
      type: object
      properties:
        message:
          type: string
    PostRetryResponse:
      type: object
      properties:
        message:
          type: string
        post:
          $ref: '#/components/schemas/Post'
    # Profiles Responses
    ProfilesListResponse:
      type: object
      properties:
        profiles:
          type: array
          items:
            $ref: '#/components/schemas/Profile'
    ProfileGetResponse:
      type: object
      properties:
        profile:
          $ref: '#/components/schemas/Profile'
    ProfileCreateResponse:
      type: object
      properties:
        message:
          type: string
        profile:
          $ref: '#/components/schemas/Profile'
    ProfileUpdateResponse:
      type: object
      properties:
        message:
          type: string
        profile:
          $ref: '#/components/schemas/Profile'
    ProfileDeleteResponse:
      type: object
      properties:
        message:
          type: string
    # Accounts Responses
    AccountsListResponse:
      type: object
      properties:
        accounts:
          type: array
          items:
            $ref: '#/components/schemas/SocialAccount'
        hasAnalyticsAccess:
          type: boolean
          description: Whether user has analytics add-on access
    AccountGetResponse:
      type: object
      properties:
        account:
          $ref: '#/components/schemas/SocialAccount'
    FollowerStatsResponse:
      type: object
      properties:
        accounts:
          type: array
          items:
            $ref: '#/components/schemas/AccountWithFollowerStats'
        dateRange:
          type: object
          properties:
            from:
              type: string
              format: date-time
            to:
              type: string
              format: date-time
        aggregation:
          type: string
          enum: [daily, weekly, monthly]
    # Media Responses
    UploadedFile:
      type: object
      properties:
        type:
          type: string
          enum: [image, video, document]
        url:
          type: string
          format: uri
        filename:
          type: string
        size:
          type: integer
        mimeType:
          type: string
    MediaUploadResponse:
      type: object
      properties:
        files:
          type: array
          items:
            $ref: '#/components/schemas/UploadedFile'
    UploadTokenResponse:
      type: object
      properties:
        token:
          type: string
        uploadUrl:
          type: string
          format: uri
        expiresAt:
          type: string
          format: date-time
        status:
          type: string
          enum: [pending, completed, expired]
    UploadTokenStatusResponse:
      type: object
      properties:
        token:
          type: string
        status:
          type: string
          enum: [pending, completed, expired]
        files:
          type: array
          items:
            $ref: '#/components/schemas/UploadedFile'
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
    # Queue Responses
    QueueSlotsResponse:
      type: object
      properties:
        exists:
          type: boolean
        schedule:
          $ref: '#/components/schemas/QueueSchedule'
        nextSlots:
          type: array
          items:
            type: string
            format: date-time
    QueueUpdateResponse:
      type: object
      properties:
        success:
          type: boolean
        schedule:
          $ref: '#/components/schemas/QueueSchedule'
        nextSlots:
          type: array
          items:
            type: string
            format: date-time
        reshuffledCount:
          type: integer
    QueueDeleteResponse:
      type: object
      properties:
        success:
          type: boolean
        deleted:
          type: boolean
    QueuePreviewResponse:
      type: object
      properties:
        profileId:
          type: string
        count:
          type: integer
        slots:
          type: array
          items:
            type: string
            format: date-time
    QueueNextSlotResponse:
      type: object
      properties:
        profileId:
          type: string
        nextSlot:
          type: string
          format: date-time
        timezone:
          type: string
    # Tools Responses
    DownloadFormat:
      type: object
      properties:
        formatId:
          type: string
        ext:
          type: string
        resolution:
          type: string
        filesize:
          type: integer
        quality:
          type: string
    DownloadResponse:
      type: object
      properties:
        url:
          type: string
          format: uri
        title:
          type: string
        thumbnail:
          type: string
          format: uri
        duration:
          type: integer
        formats:
          type: array
          items:
            $ref: '#/components/schemas/DownloadFormat'
    TranscriptSegment:
      type: object
      properties:
        text:
          type: string
        start:
          type: number
        duration:
          type: number
    TranscriptResponse:
      type: object
      properties:
        transcript:
          type: string
        segments:
          type: array
          items:
            $ref: '#/components/schemas/TranscriptSegment'
        language:
          type: string
    HashtagInfo:
      type: object
      properties:
        hashtag:
          type: string
        status:
          type: string
          enum: [safe, banned, restricted, unknown]
        postCount:
          type: integer
    HashtagCheckResponse:
      type: object
      properties:
        hashtags:
          type: array
          items:
            $ref: '#/components/schemas/HashtagInfo'
    CaptionResponse:
      type: object
      properties:
        caption:
          type: string
    # Users Responses
    User:
      type: object
      properties:
        _id:
          type: string
        email:
          type: string
        name:
          type: string
        role:
          type: string
        createdAt:
          type: string
          format: date-time
    UsersListResponse:
      type: object
      properties:
        users:
          type: array
          items:
            $ref: '#/components/schemas/User'
    UserGetResponse:
      type: object
      properties:
        user:
          $ref: '#/components/schemas/User'
security:
  - bearerAuth: []
paths:
  # ============================================
  # Tools API - Media Download & Utilities
  # ============================================
  /v1/tools/youtube/download:
    get:
      tags: [Tools]
      summary: Download YouTube video or audio
      description: |
        Download YouTube videos or audio. Returns available formats or direct download URL.
        
        **Rate Limits:** Build (50/day), Accelerate (500/day), Unlimited (unlimited)
      security:
        - bearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: YouTube video URL or video ID
          schema:
            type: string
            example: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
        - name: action
          in: query
          description: "Action to perform: 'download' returns download URL, 'formats' lists available formats"
          schema:
            type: string
            enum: [download, formats]
            default: download
        - name: format
          in: query
          description: Desired format (when action=download)
          schema:
            type: string
            enum: [video, audio]
            default: video
        - name: quality
          in: query
          description: Desired quality (when action=download)
          schema:
            type: string
            enum: [hd, sd]
            default: hd
        - name: formatId
          in: query
          description: Specific format ID from formats list
          schema:
            type: string
      responses:
        "200":
          description: Success
          headers:
            X-RateLimit-Limit:
              schema: { type: string }
              description: Daily rate limit
            X-RateLimit-Remaining:
              schema: { type: string }
              description: Remaining calls today
            X-RateLimit-Reset:
              schema: { type: string }
              description: Unix timestamp when limit resets
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  title: { type: string }
                  downloadUrl: { type: string, format: uri }
                  formats:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        label: { type: string }
                        ext: { type: string }
                        type: { type: string }
                        height: { type: integer }
                        width: { type: integer }
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          description: Tools API not available on free plan
        "429":
          description: Daily rate limit exceeded

  /v1/tools/youtube/transcript:
    get:
      tags: [Tools]
      summary: Get YouTube video transcript
      description: |
        Extract transcript/captions from a YouTube video.
        
        **Rate Limits:** Build (50/day), Accelerate (500/day), Unlimited (unlimited)
      security:
        - bearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: YouTube video URL or video ID
          schema:
            type: string
        - name: lang
          in: query
          description: Language code for transcript
          schema:
            type: string
            default: en
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  videoId: { type: string }
                  language: { type: string }
                  fullText: { type: string }
                  segments:
                    type: array
                    items:
                      type: object
                      properties:
                        text: { type: string }
                        start: { type: number }
                        duration: { type: number }
        "404":
          description: No transcript available

  /v1/tools/instagram/download:
    get:
      tags: [Tools]
      summary: Download Instagram reel or post
      description: |
        Download Instagram reels, posts, or photos.
        
        **Rate Limits:** Build (50/day), Accelerate (500/day), Unlimited (unlimited)
      security:
        - bearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: Instagram reel or post URL
          schema:
            type: string
            example: "https://www.instagram.com/reel/ABC123/"
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  title: { type: string }
                  downloadUrl: { type: string, format: uri }

  /v1/tools/instagram/hashtag-checker:
    post:
      tags: [Tools]
      summary: Check Instagram hashtags for bans
      description: |
        Check if Instagram hashtags are banned, restricted, or safe to use.
        
        **Rate Limits:** Build (50/day), Accelerate (500/day), Unlimited (unlimited)
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [hashtags]
              properties:
                hashtags:
                  type: array
                  maxItems: 20
                  items:
                    type: string
                  example: ["travel", "followforfollow", "fitness"]
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        hashtag: { type: string }
                        status:
                          type: string
                          enum: [banned, restricted, safe, unknown]
                        reason: { type: string }
                        confidence: { type: number }
                  summary:
                    type: object
                    properties:
                      banned: { type: integer }
                      restricted: { type: integer }
                      safe: { type: integer }

  /v1/tools/tiktok/download:
    get:
      tags: [Tools]
      summary: Download TikTok video
      description: |
        Download TikTok videos with or without watermark.
        
        **Rate Limits:** Build (50/day), Accelerate (500/day), Unlimited (unlimited)
      security:
        - bearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: TikTok video URL or ID
          schema:
            type: string
        - name: action
          in: query
          description: "'formats' to list available formats"
          schema:
            type: string
            enum: [download, formats]
            default: download
        - name: formatId
          in: query
          description: Specific format ID (0 = no watermark, etc.)
          schema:
            type: string
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  title: { type: string }
                  downloadUrl: { type: string, format: uri }
                  formats:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        label: { type: string }
                        ext: { type: string }

  /v1/tools/twitter/download:
    get:
      tags: [Tools]
      summary: Download Twitter/X video
      description: |
        Download videos from Twitter/X posts.
        
        **Rate Limits:** Build (50/day), Accelerate (500/day), Unlimited (unlimited)
      security:
        - bearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: Twitter/X post URL
          schema:
            type: string
            example: "https://x.com/user/status/123456789"
        - name: action
          in: query
          schema:
            type: string
            enum: [download, formats]
            default: download
        - name: formatId
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  title: { type: string }
                  downloadUrl: { type: string, format: uri }

  /v1/tools/facebook/download:
    get:
      tags: [Tools]
      summary: Download Facebook video
      description: |
        Download videos and reels from Facebook.
        
        **Rate Limits:** Build (50/day), Accelerate (500/day), Unlimited (unlimited)
      security:
        - bearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: Facebook video or reel URL
          schema:
            type: string
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  title: { type: string }
                  downloadUrl: { type: string, format: uri }
                  thumbnail: { type: string, format: uri }

  /v1/tools/linkedin/download:
    get:
      tags: [Tools]
      summary: Download LinkedIn video
      description: |
        Download videos from LinkedIn posts.
        
        **Rate Limits:** Build (50/day), Accelerate (500/day), Unlimited (unlimited)
      security:
        - bearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: LinkedIn post URL
          schema:
            type: string
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  title: { type: string }
                  downloadUrl: { type: string, format: uri }

  /v1/tools/bluesky/download:
    get:
      tags: [Tools]
      summary: Download Bluesky video
      description: |
        Download videos from Bluesky posts.
        
        **Rate Limits:** Build (50/day), Accelerate (500/day), Unlimited (unlimited)
      security:
        - bearerAuth: []
      parameters:
        - name: url
          in: query
          required: true
          description: Bluesky post URL
          schema:
            type: string
            example: "https://bsky.app/profile/user.bsky.social/post/abc123"
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  title: { type: string }
                  text: { type: string }
                  downloadUrl: { type: string, format: uri }
                  thumbnail: { type: string, format: uri }

  # ============================================
  # Analytics
  # ============================================
  /v1/analytics:
    get:
      tags: [Analytics]
      summary: Unified analytics for posts
      description: |
        Returns analytics for posts. If postId is provided, returns analytics for a single post.
        Otherwise returns a paginated list of posts with overview stats.
        
        **Important: Understanding Post IDs**
        
        This endpoint uses two types of posts:
        - **Late Posts** - Posts scheduled/created via the Late API (e.g., via `POST /v1/posts`)
        - **External Posts** - Posts synced from social platforms for analytics tracking
        
        When you schedule a post via Late and it gets published, **both** records exist:
        1. The original Late Post (returned when you created the post)
        2. An External Post (created when we sync analytics from the platform)
        
        **List endpoint behavior:**
        - Returns External Post IDs (`_id` field)
        - Use the `isExternal` field to identify post origin:
          - `isExternal: true` - Synced from platform (may have been originally scheduled via Late)
          - `isExternal: false` - Late-scheduled post (shown when querying by Late post ID)
        
        **Single post behavior (`postId` parameter):**
        - Accepts **both** Late Post IDs and External Post IDs
        - If you pass a Late Post ID, the API automatically resolves it to the corresponding External Post analytics
        - Both return the same analytics data for the same underlying social media post
        
        **Correlating posts:** Use `platformPostUrl` (e.g., `https://www.instagram.com/reel/ABC123/`) as the unique identifier - it's consistent across both Late and External post records.
        
        **Note:** For follower count history and growth metrics, use the dedicated `/v1/accounts/follower-stats` endpoint.
        
        **LinkedIn Analytics:**
        - **Personal Accounts:** Per-post analytics available for posts published through Late. External posts cannot be synced due to LinkedIn API restrictions.
        - **Organization Accounts:** Full analytics support including external post syncing.

        **Telegram Analytics:**
        - **Not available.** The Telegram Bot API does not provide message view counts, forwards, or engagement metrics. This is a Telegram platform limitation, not a Late limitation. View counts are only visible to channel admins in the Telegram app.

        **Data Freshness:** Analytics data is cached and refreshed at most once per hour. When you call this endpoint, if the cache is older than 60 minutes, a background refresh is triggered and you'll see updated data on subsequent requests. There is no rate limit on API requests.
      parameters:
        - name: postId
          in: query
          schema: { type: string }
          description: |
            Returns analytics for a single post. Accepts both Late Post IDs (from `POST /v1/posts`) 
            and External Post IDs (from this endpoint's list response). The API automatically 
            resolves Late Post IDs to their corresponding External Post analytics.
        - name: platform
          in: query
          schema: { type: string }
          description: Filter by platform (default "all")
        - name: profileId
          in: query
          schema: { type: string }
          description: Filter by profile ID (default "all")
        - name: fromDate
          in: query
          schema: { type: string, format: date }
          description: Inclusive lower bound
        - name: toDate
          in: query
          schema: { type: string, format: date }
          description: Inclusive upper bound
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
          description: Page size (default 50)
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
          description: Page number (default 1)
        - name: sortBy
          in: query
          schema: { type: string, enum: [date, engagement], default: date }
          description: Sort by date or engagement
        - name: order
          in: query
          schema: { type: string, enum: [asc, desc], default: desc }
          description: Sort order
      responses:
        '200':
          description: Analytics result
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/AnalyticsSinglePostResponse'
                  - $ref: '#/components/schemas/AnalyticsListResponse'
              examples:
                singlePost:
                  summary: Single post analytics
                  value:
                    postId: "65f1c0a9e2b5af0012ab34cd"
                    status: "published"
                    content: "Check out our new product launch!"
                    scheduledFor: "2024-11-01T10:00:00Z"
                    publishedAt: "2024-11-01T10:00:05Z"
                    analytics:
                      impressions: 15420
                      reach: 12350
                      likes: 342
                      comments: 28
                      shares: 45
                      clicks: 189
                      views: 0
                      engagementRate: 2.78
                      lastUpdated: "2024-11-02T08:30:00Z"
                    platformAnalytics:
                      - platform: "twitter"
                        status: "published"
                        accountId: "64e1f0a9e2b5af0012ab34cd"
                        accountUsername: "@acmecorp"
                        analytics:
                          impressions: 15420
                          reach: 12350
                          likes: 342
                          comments: 28
                          shares: 45
                          clicks: 189
                          views: 0
                          engagementRate: 2.78
                          lastUpdated: "2024-11-02T08:30:00Z"
                    platform: "twitter"
                    platformPostUrl: "https://twitter.com/acmecorp/status/123456789"
                    isExternal: false
                postList:
                  summary: Paginated analytics list
                  description: |
                    Note: The list endpoint returns External Post IDs. Posts originally 
                    scheduled via Late will have isExternal: true in this response.
                    Use platformPostUrl to correlate with your original Late Post IDs.
                  value:
                    overview:
                      totalPosts: 156
                      publishedPosts: 142
                      scheduledPosts: 0
                      lastSync: "2024-11-02T08:30:00Z"
                    posts:
                      - _id: "65f1c0a9e2b5af0012ab34cd"
                        content: "Check out our new product launch!"
                        scheduledFor: "2024-11-01T10:00:00Z"
                        publishedAt: "2024-11-01T10:00:05Z"
                        status: "published"
                        analytics:
                          impressions: 15420
                          likes: 342
                          comments: 28
                          engagementRate: 2.78
                          lastUpdated: "2024-11-02T08:30:00Z"
                        platforms:
                          - platform: "instagram"
                            status: "published"
                            analytics:
                              impressions: 15420
                              likes: 342
                              comments: 28
                        platform: "instagram"
                        platformPostUrl: "https://www.instagram.com/reel/ABC123xyz/"
                        isExternal: true
                        thumbnailUrl: "https://storage.example.com/thumb.jpg"
                        mediaType: "video"
                    pagination:
                      page: 1
                      limit: 50
                      total: 156
                      pages: 4
                    accounts:
                      - _id: "64e1f0..."
                        platform: "twitter"
                        username: "@acmecorp"
                        displayName: "Acme Corp"
                        isActive: true
                    hasAnalyticsAccess: true
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402':
          description: Analytics add-on required
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string, example: Analytics add-on required }
                  code: { type: string, example: analytics_addon_required }
        '404': { $ref: '#/components/responses/NotFound' }
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/account-groups:
    get:
      tags: [Account Groups]
      summary: List account groups for the authenticated user
      responses:
        '200':
          description: Groups
          content:
            application/json:
              schema:
                type: object
                properties:
                  groups:
                    type: array
                    items:
                      type: object
                      properties:
                        _id: { type: string }
                        name: { type: string }
                        accountIds:
                          type: array
                          items: { type: string }
              examples:
                example:
                  value:
                    groups:
                      - _id: "6507a1b2c3d4e5f6a7b8c9d0"
                        name: "Marketing Accounts"
                        accountIds:
                          - "64e1f0a9e2b5af0012ab34cd"
                          - "64e1f0a9e2b5af0012ab34ce"
                      - _id: "6507a1b2c3d4e5f6a7b8c9d1"
                        name: "Personal Brand"
                        accountIds:
                          - "64e1f0a9e2b5af0012ab34cf"
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Account Groups]
      summary: Create a new account group
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, accountIds]
              properties:
                name: { type: string }
                accountIds:
                  type: array
                  items: { type: string }
            example:
              name: "Marketing Accounts"
              accountIds:
                - "64e1f0a9e2b5af0012ab34cd"
                - "64e1f0a9e2b5af0012ab34ce"
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  group:
                    type: object
                    properties:
                      _id: { type: string }
                      name: { type: string }
                      accountIds:
                        type: array
                        items: { type: string }
              example:
                message: "Account group created successfully"
                group:
                  _id: "6507a1b2c3d4e5f6a7b8c9d0"
                  name: "Marketing Accounts"
                  accountIds:
                    - "64e1f0a9e2b5af0012ab34cd"
                    - "64e1f0a9e2b5af0012ab34ce"
        '400': { description: Invalid request }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { description: Group name already exists }
  /v1/account-groups/{groupId}:
    put:
      tags: [Account Groups]
      summary: Update an account group
      parameters:
        - name: groupId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                accountIds:
                  type: array
                  items: { type: string }
            example:
              name: "Updated Marketing Accounts"
              accountIds:
                - "64e1f0a9e2b5af0012ab34cd"
                - "64e1f0a9e2b5af0012ab34ce"
                - "64e1f0a9e2b5af0012ab34cf"
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  group:
                    type: object
              example:
                message: "Account group updated successfully"
                group:
                  _id: "6507a1b2c3d4e5f6a7b8c9d0"
                  name: "Updated Marketing Accounts"
                  accountIds:
                    - "64e1f0a9e2b5af0012ab34cd"
                    - "64e1f0a9e2b5af0012ab34ce"
                    - "64e1f0a9e2b5af0012ab34cf"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: Group name already exists }
    delete:
      tags: [Account Groups]
      summary: Delete an account group
      parameters:
        - name: groupId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
              example:
                message: "Account group deleted successfully"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/media/presign:
    post:
      tags: [Media]
      summary: Get a presigned URL for direct file upload (up to 5GB)
      description: |
        Get a presigned URL to upload files directly to cloud storage. This is the recommended method for uploading files of any size, especially files larger than ~4MB.

        **How it works:**
        1. Call this endpoint with the filename and content type
        2. Receive an `uploadUrl` (presigned) and `publicUrl`
        3. PUT your file directly to the `uploadUrl`
        4. Use the `publicUrl` in your posts

        **Benefits:**
        - Supports files up to 5GB
        - Files upload directly to storage (faster, no server bottleneck)
        - No 413 errors from server body size limits

        **Example:**
        ```javascript
        // Step 1: Get presigned URL
        const response = await fetch('https://getlate.dev/api/v1/media/presign', {
          method: 'POST',
          headers: {
            'Authorization': 'Bearer YOUR_API_KEY',
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            filename: 'my-video.mp4',
            contentType: 'video/mp4'
          })
        });
        const { uploadUrl, publicUrl } = await response.json();

        // Step 2: Upload file directly to storage
        await fetch(uploadUrl, {
          method: 'PUT',
          body: file,
          headers: { 'Content-Type': 'video/mp4' }
        });

        // Step 3: Use publicUrl when creating your post
        // The publicUrl is ready to use immediately after upload completes
        ```
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, contentType]
              properties:
                filename:
                  type: string
                  description: Name of the file to upload
                  example: "my-video.mp4"
                contentType:
                  type: string
                  description: MIME type of the file
                  enum:
                    - image/jpeg
                    - image/jpg
                    - image/png
                    - image/webp
                    - image/gif
                    - video/mp4
                    - video/mpeg
                    - video/quicktime
                    - video/avi
                    - video/x-msvideo
                    - video/webm
                    - video/x-m4v
                    - application/pdf
                  example: "video/mp4"
                size:
                  type: integer
                  description: Optional file size in bytes for pre-validation (max 5GB)
                  example: 15234567
      responses:
        '200':
          description: Presigned URL generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  uploadUrl:
                    type: string
                    format: uri
                    description: Presigned URL to PUT your file to (expires in 1 hour)
                  publicUrl:
                    type: string
                    format: uri
                    description: Public URL where the file will be accessible after upload
                  key:
                    type: string
                    description: Storage key/path of the file
                  type:
                    type: string
                    enum: [image, video, document]
                    description: Detected file type based on content type
              example:
                uploadUrl: "<presigned-upload-url>"
                publicUrl: "https://media.getlate.dev/temp/1234567890_abc123_my-video.mp4"
                key: "temp/1234567890_abc123_my-video.mp4"
                type: "video"
        '400':
          description: Invalid request (missing filename, contentType, or unsupported content type)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
              examples:
                missing_filename:
                  value: { error: "filename and contentType are required" }
                invalid_type:
                  value: { error: "Content type not allowed: text/plain" }
                file_too_large:
                  value: { error: "File too large. Maximum size is 5GB." }
        '401': { $ref: '#/components/responses/Unauthorized' }
  # Video Clips API paths removed - AI Clipping feature temporarily disabled
  # /v1/video-clips, /v1/video-clips-replicate, /v1/video-clips/process,
  # /v1/video-clips/jobs, /v1/video-clips/status/{jobId}, /v1/video-clips/monthly-stats
  /v1/reddit/search:
    get:
      tags: [Reddit Search]
      summary: Search Reddit posts via a connected account
      parameters:
        - name: accountId
          in: query
          required: true
          schema: { type: string }
        - name: subreddit
          in: query
          schema: { type: string }
        - name: q
          in: query
          required: true
          schema: { type: string }
        - name: restrict_sr
          in: query
          schema: { type: string, enum: ['0','1'] }
        - name: sort
          in: query
          schema: { type: string, enum: [relevance, hot, top, new, comments], default: new }
        - name: limit
          in: query
          schema: { type: integer, default: 25, maximum: 100 }
        - name: after
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  posts:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        title: { type: string }
                        selftext: { type: string }
                        author: { type: string }
                        subreddit: { type: string }
                        score: { type: integer }
                        num_comments: { type: integer }
                        created_utc: { type: number }
                        permalink: { type: string }
                  after: { type: string }
              example:
                posts:
                  - id: "1abc234"
                    title: "How to grow on social media in 2025"
                    selftext: "Here are my tips..."
                    author: "marketingpro"
                    subreddit: "socialmedia"
                    score: 156
                    num_comments: 42
                    created_utc: 1730000000
                    permalink: "/r/socialmedia/comments/1abc234/how_to_grow/"
                after: "t3_1abc234"
        '400': { description: Missing params }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found }
  /v1/reddit/feed:
    get:
      tags: [Reddit Search]
      summary: Fetch subreddit feed via a connected account
      parameters:
        - name: accountId
          in: query
          required: true
          schema: { type: string }
        - name: subreddit
          in: query
          schema: { type: string }
        - name: sort
          in: query
          schema: { type: string, enum: [hot, new, top, rising], default: hot }
        - name: limit
          in: query
          schema: { type: integer, default: 25, maximum: 100 }
        - name: after
          in: query
          schema: { type: string }
        - name: t
          in: query
          schema: { type: string, enum: [hour, day, week, month, year, all] }
      responses:
        '200':
          description: Feed items
          content:
            application/json:
              schema:
                type: object
                properties:
                  posts:
                    type: array
                    items:
                      type: object
                  after: { type: string }
              example:
                posts:
                  - id: "1xyz789"
                    title: "Top marketing trends this week"
                    author: "trendwatcher"
                    subreddit: "marketing"
                    score: 892
                    num_comments: 134
                    created_utc: 1730100000
                    permalink: "/r/marketing/comments/1xyz789/top_marketing_trends/"
                  - id: "1def456"
                    title: "My social media strategy that worked"
                    author: "growthexpert"
                    subreddit: "marketing"
                    score: 567
                    num_comments: 89
                    created_utc: 1730050000
                    permalink: "/r/marketing/comments/1def456/my_social_media_strategy/"
                after: "t3_1def456"
        '400': { description: Missing params }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found }
  /v1/usage-stats:
    get:
      tags: [Usage]
      summary: Get plan and usage stats for current account
      responses:
        '200':
          description: Usage stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageStats'
              example:
                planName: "Pro"
                billingPeriod: "monthly"
                signupDate: "2024-01-15T10:30:00Z"
                limits:
                  uploads: 500
                  profiles: 10
                usage:
                  uploads: 127
                  profiles: 3
                  lastReset: "2024-11-01T00:00:00Z"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/posts:
    get:
      tags: [Posts]
      summary: List posts visible to the authenticated user
      description: |
        **Getting Post URLs:**
        For published posts, each platform entry includes `platformPostUrl` with the public URL.
        Use `status=published` filter to fetch only published posts with their URLs.
        
        Notes and constraints by platform when interpreting the response:
        - YouTube: posts always include at least one video in mediaItems.
        - Instagram/TikTok: posts always include media; drafts may omit media until finalized in client.
        - TikTok: mediaItems will not mix photos and videos in the same post.
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: status
          in: query
          schema: { type: string, enum: [draft, scheduled, published, failed] }
        - name: platform
          in: query
          schema: { type: string, example: twitter }
        - name: profileId
          in: query
          schema: { type: string }
        - name: createdBy
          in: query
          schema: { type: string }
        - name: dateFrom
          in: query
          schema: { type: string, format: date }
        - name: dateTo
          in: query
          schema: { type: string, format: date }
        - name: includeHidden
          in: query
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Paginated posts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostsListResponse'
              examples:
                scheduledPost:
                  summary: Scheduled post (pending publish)
                  value:
                    posts:
                      - _id: "65f1c0a9e2b5af0012ab34cd"
                        title: "Launch post"
                        content: "We just launched!"
                        status: "scheduled"
                        scheduledFor: "2024-11-01T10:00:00Z"
                        timezone: "UTC"
                        platforms:
                          - platform: "twitter"
                            accountId:
                              _id: "64e1f0..."
                              platform: "twitter"
                              username: "@acme"
                              displayName: "Acme Corp"
                              isActive: true
                            status: "pending"
                        tags: ["launch"]
                        createdAt: "2024-10-01T12:00:00Z"
                        updatedAt: "2024-10-01T12:00:00Z"
                    pagination:
                      page: 1
                      limit: 10
                      total: 1
                      pages: 1
                publishedPost:
                  summary: Published post with platformPostUrl
                  value:
                    posts:
                      - _id: "65f1c0a9e2b5af0012ab34cd"
                        title: "Launch post"
                        content: "We just launched!"
                        status: "published"
                        scheduledFor: "2024-11-01T10:00:00Z"
                        publishedAt: "2024-11-01T10:00:05Z"
                        timezone: "UTC"
                        platforms:
                          - platform: "twitter"
                            accountId:
                              _id: "64e1f0a9e2b5af0012ab34de"
                              platform: "twitter"
                              username: "@acmecorp"
                              displayName: "Acme Corporation"
                              isActive: true
                            status: "published"
                            publishedAt: "2024-11-01T10:00:05Z"
                            platformPostId: "1852634789012345678"
                            platformPostUrl: "https://twitter.com/acmecorp/status/1852634789012345678"
                          - platform: "linkedin"
                            accountId:
                              _id: "64e1f0a9e2b5af0012ab34ef"
                              platform: "linkedin"
                              username: "acme-corporation"
                              displayName: "Acme Corporation"
                              isActive: true
                            status: "published"
                            publishedAt: "2024-11-01T10:00:06Z"
                            platformPostId: "urn:li:share:7123456789012345678"
                            platformPostUrl: "https://www.linkedin.com/feed/update/urn:li:share:7123456789012345678"
                        tags: ["launch"]
                        createdAt: "2024-10-01T12:00:00Z"
                        updatedAt: "2024-11-01T10:00:06Z"
                    pagination:
                      page: 1
                      limit: 10
                      total: 1
                      pages: 1
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Posts]
      summary: Create a draft, scheduled, or immediate post
      description: |
        **Getting Post URLs:**
        - For immediate posts (`publishNow: true`): The response includes `platformPostUrl` in each platform entry under `post.platforms[]`.
        - For scheduled posts: Fetch the post via `GET /v1/posts/{postId}` after the scheduled time; `platformPostUrl` will be populated once published.
        
        Platform constraints:
        - YouTube requires a video in mediaItems; optional custom thumbnail via MediaItem.thumbnail.
        - Instagram and TikTok require media; do not mix videos and images for TikTok.
        - Instagram carousels support up to 10 items; Stories publish as 'story'.
        - Threads carousels support up to 10 images (no videos in carousels); single posts support one image or video.
        - Facebook Stories require media (single image or video); set contentType to 'story' in platformSpecificData.
        - LinkedIn multi-image supports up to 20 images; single PDF documents supported (max 100MB, ~300 pages, cannot mix with other media).
        - Pinterest supports single image via image_url or a single video per Pin; boardId is required.
        - Bluesky supports up to 4 images per post. Images may be automatically recompressed to ≤ ~1MB to satisfy Bluesky's blob limit. When no media is attached, a link preview may be generated for URLs in the text.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                content: { type: string }
                mediaItems:
                  type: array
                  items:
                    type: object
                    properties:
                      type: { type: string, enum: [image, video, gif, document] }
                      url: { type: string, format: uri }
                platforms:
                  type: array
                  items:
                    type: object
                    properties:
                      platform: { type: string, example: twitter }
                      accountId: { type: string }
                      customContent: { type: string }
                      customMedia:
                        type: array
                        items:
                          type: object
                          properties:
                            type: { type: string, enum: [image, video, gif, document] }
                            url: { type: string, format: uri }
                      scheduledFor:
                        type: string
                        format: date-time
                        description: Optional per-platform scheduled time override. When omitted, the top-level scheduledFor is used.
                      platformSpecificData:
                        oneOf:
                          - $ref: '#/components/schemas/TwitterPlatformData'
                          - $ref: '#/components/schemas/ThreadsPlatformData'
                          - $ref: '#/components/schemas/FacebookPlatformData'
                          - $ref: '#/components/schemas/InstagramPlatformData'
                          - $ref: '#/components/schemas/LinkedInPlatformData'
                          - $ref: '#/components/schemas/PinterestPlatformData'
                          - $ref: '#/components/schemas/YouTubePlatformData'
                          - $ref: '#/components/schemas/TikTokPlatformData'
                scheduledFor: { type: string, format: date-time }
                publishNow: { type: boolean, default: false }
                isDraft: { type: boolean, default: false }
                timezone: { type: string, default: UTC }
                tags:
                  type: array
                  description: |
                    Tags/keywords for the post. YouTube-specific constraints:
                    - No count limit; duplicates are automatically removed
                    - Each tag must be ≤ 100 characters
                    - Combined total across all tags ≤ 500 characters (YouTube's limit)
                  items: { type: string }
                hashtags:
                  type: array
                  items: { type: string }
                mentions:
                  type: array
                  items: { type: string }
                crosspostingEnabled: { type: boolean, default: true }
                metadata: { type: object, additionalProperties: true }
                queuedFromProfile:
                  type: string
                  description: |
                    Profile ID to schedule via queue. When provided without scheduledFor,
                    the post will be automatically assigned to the next available slot
                    from the profile's default queue (or the queue specified by queueId).
                queueId:
                  type: string
                  description: |
                    Specific queue ID to use when scheduling via queue.
                    Only used when queuedFromProfile is also provided.
                    If omitted, uses the profile's default queue.
      responses:
        '201':
          description: Post created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostCreateResponse'
              examples:
                scheduled:
                  summary: Scheduled post (URLs populated after publish time)
                  value:
                    post:
                      _id: "65f1c0a9e2b5af0012ab34cd"
                      title: "Launch post"
                      content: "We just launched!"
                      status: "scheduled"
                      scheduledFor: "2024-11-01T10:00:00Z"
                      timezone: "UTC"
                      platforms:
                        - platform: "twitter"
                          accountId:
                            _id: "64e1f0..."
                            platform: "twitter"
                            username: "@acme"
                            displayName: "Acme Corp"
                            isActive: true
                          status: "pending"
                    message: "Post scheduled successfully"
                immediatePublish:
                  summary: Immediate post with publishNow=true (URLs included)
                  value:
                    post:
                      _id: "65f1c0a9e2b5af0012ab34cd"
                      title: "Launch post"
                      content: "We just launched!"
                      status: "published"
                      publishedAt: "2024-11-01T10:00:05Z"
                      timezone: "UTC"
                      platforms:
                        - platform: "twitter"
                          accountId:
                            _id: "64e1f0a9e2b5af0012ab34de"
                            platform: "twitter"
                            username: "@acmecorp"
                            displayName: "Acme Corporation"
                            isActive: true
                          status: "published"
                          publishedAt: "2024-11-01T10:00:05Z"
                          platformPostId: "1852634789012345678"
                          platformPostUrl: "https://twitter.com/acmecorp/status/1852634789012345678"
                        - platform: "linkedin"
                          accountId:
                            _id: "64e1f0a9e2b5af0012ab34ef"
                            platform: "linkedin"
                            username: "acme-corporation"
                            displayName: "Acme Corporation"
                            isActive: true
                          status: "published"
                          publishedAt: "2024-11-01T10:00:06Z"
                          platformPostId: "urn:li:share:7123456789012345678"
                          platformPostUrl: "https://www.linkedin.com/feed/update/urn:li:share:7123456789012345678"
                    message: "Post published successfully"
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
  /v1/posts/{postId}:
    get:
      tags: [Posts]
      summary: Get a single post
      description: |
        Fetch a single post by ID. For published posts, this returns `platformPostUrl` 
        for each platform - useful for retrieving post URLs after scheduled posts publish.
      parameters:
        - name: postId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Post
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostGetResponse'
              examples:
                scheduledPost:
                  summary: Scheduled post (pending)
                  value:
                    post:
                      _id: "65f1c0a9e2b5af0012ab34cd"
                      title: "Launch post"
                      content: "We just launched!"
                      status: "scheduled"
                      scheduledFor: "2024-11-01T10:00:00Z"
                      platforms:
                        - platform: "twitter"
                          accountId:
                            _id: "64e1f0..."
                            platform: "twitter"
                            username: "@acme"
                            displayName: "Acme Corp"
                            isActive: true
                          status: "pending"
                publishedPost:
                  summary: Published post with platformPostUrl
                  value:
                    post:
                      _id: "65f1c0a9e2b5af0012ab34cd"
                      title: "Launch post"
                      content: "We just launched!"
                      status: "published"
                      publishedAt: "2024-11-01T10:00:05Z"
                      platforms:
                        - platform: "twitter"
                          accountId:
                            _id: "64e1f0a9e2b5af0012ab34de"
                            platform: "twitter"
                            username: "@acmecorp"
                            displayName: "Acme Corporation"
                            isActive: true
                          status: "published"
                          publishedAt: "2024-11-01T10:00:05Z"
                          platformPostId: "1852634789012345678"
                          platformPostUrl: "https://twitter.com/acmecorp/status/1852634789012345678"
                        - platform: "linkedin"
                          accountId:
                            _id: "64e1f0a9e2b5af0012ab34ef"
                            platform: "linkedin"
                            username: "acme-corporation"
                            displayName: "Acme Corporation"
                            isActive: true
                          status: "published"
                          publishedAt: "2024-11-01T10:00:06Z"
                          platformPostId: "urn:li:share:7123456789012345678"
                          platformPostUrl: "https://www.linkedin.com/feed/update/urn:li:share:7123456789012345678"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Forbidden
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Posts]
      summary: Update a post
      description: |
        Update an existing post. Only draft, scheduled, failed, and partial posts can be edited.
        Published, publishing, and cancelled posts cannot be modified.
      parameters:
        - name: postId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
            example:
              content: "Updated content for our launch post!"
              scheduledFor: "2024-11-02T14:00:00Z"
      responses:
        '200':
          description: Post updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostUpdateResponse'
              example:
                message: "Post updated successfully"
                post:
                  _id: "65f1c0a9e2b5af0012ab34cd"
                  content: "Updated content for our launch post!"
                  status: "scheduled"
                  scheduledFor: "2024-11-02T14:00:00Z"
        '207':
          description: Partial publish success
        '400':
          description: Invalid request
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Forbidden
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Posts]
      summary: Delete a post
      description: |
        Delete a post. Published posts cannot be deleted. 
        When deleting a scheduled or draft post that consumed upload quota, the quota will be automatically refunded.
      parameters:
        - name: postId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostDeleteResponse'
              example:
                message: "Post deleted successfully"
        '400':
          description: Cannot delete published posts
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Forbidden
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/posts/bulk-upload:
    post:
      tags: [Posts]
      summary: Validate and schedule multiple posts from CSV
      parameters:
        - name: dryRun
          in: query
          schema: { type: boolean, default: false }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Bulk upload results
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  totalRows: { type: integer }
                  created: { type: integer }
                  failed: { type: integer }
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        row: { type: integer }
                        error: { type: string }
                  posts:
                    type: array
                    items: { $ref: '#/components/schemas/Post' }
              example:
                success: true
                totalRows: 10
                created: 8
                failed: 2
                errors:
                  - row: 3
                    error: "Invalid date format"
                  - row: 7
                    error: "Account not found"
                posts:
                  - _id: "65f1c0a9e2b5af0012ab34cd"
                    content: "First bulk post"
                    status: "scheduled"
                    scheduledFor: "2024-11-01T10:00:00Z"
        '207':
          description: Partial success
        '400':
          description: Invalid CSV or validation errors
        '401': { $ref: '#/components/responses/Unauthorized' }
  /v1/posts/{postId}/retry:
    post:
      tags: [Posts]
      summary: Retry publishing a failed or partial post
      parameters:
        - name: postId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Retry successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostRetryResponse'
              example:
                message: "Post published successfully"
                post:
                  _id: "65f1c0a9e2b5af0012ab34cd"
                  content: "Check out our new product!"
                  status: "published"
                  publishedAt: "2024-11-01T10:00:05Z"
                  platforms:
                    - platform: "twitter"
                      accountId:
                        _id: "64e1f0..."
                        platform: "twitter"
                        username: "@acme"
                        displayName: "Acme Corp"
                        isActive: true
                      status: "published"
                      platformPostId: "1234567890"
                      platformPostUrl: "https://twitter.com/acme/status/1234567890"
        '207':
          description: Partial success
        '400':
          description: Invalid state
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Forbidden
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Post is currently publishing
  /v1/users:
    get:
      tags: [Users]
      summary: List team users (root + invited)
      responses:
        '200':
          description: Users
          content:
            application/json:
              schema:
                type: object
                properties:
                  currentUserId: { type: string }
                  users:
                    type: array
                    items:
                      type: object
                      properties:
                        _id: { type: string }
                        name: { type: string }
                        email: { type: string }
                        role: { type: string }
                        isRoot: { type: boolean }
                        profileAccess:
                          type: array
                          items: { type: string }
                        createdAt: { type: string, format: date-time }
              example:
                currentUserId: "6507a1b2c3d4e5f6a7b8c9d0"
                users:
                  - _id: "6507a1b2c3d4e5f6a7b8c9d0"
                    name: "John Doe"
                    email: "john@example.com"
                    role: "owner"
                    isRoot: true
                    profileAccess: ["all"]
                    createdAt: "2024-01-15T10:30:00Z"
                  - _id: "6507a1b2c3d4e5f6a7b8c9d1"
                    name: "Jane Smith"
                    email: "jane@example.com"
                    role: "member"
                    isRoot: false
                    profileAccess:
                      - "64f0a1b2c3d4e5f6a7b8c9d0"
                      - "64f0a1b2c3d4e5f6a7b8c9d1"
                    createdAt: "2024-03-20T14:45:00Z"
        '401': { $ref: '#/components/responses/Unauthorized' }
  /v1/users/{userId}:
    get:
      tags: [Users]
      summary: Get user by id (self or invited)
      parameters:
        - name: userId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: User
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      _id: { type: string }
                      name: { type: string }
                      email: { type: string }
                      role: { type: string }
                      isRoot: { type: boolean }
                      profileAccess:
                        type: array
                        items: { type: string }
              example:
                user:
                  _id: "6507a1b2c3d4e5f6a7b8c9d0"
                  name: "John Doe"
                  email: "john@example.com"
                  role: "owner"
                  isRoot: true
                  profileAccess: ["all"]
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Forbidden
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/profiles:
    get:
      tags: [Profiles]
      summary: List profiles visible to the authenticated user
      description: |
        Returns profiles within the user's plan limit. Profiles are sorted by creation date (oldest first).
        Use `includeOverLimit=true` to include profiles that exceed the plan limit (for management/deletion purposes).
      parameters:
        - name: includeOverLimit
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: |
            When true, includes profiles that exceed the user's plan limit.
            Over-limit profiles will have `isOverLimit: true` in the response.
            Useful for managing/deleting profiles after a plan downgrade.
      responses:
        '200':
          description: Profiles
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfilesListResponse'
              examples:
                example:
                  value:
                    profiles:
                      - _id: "64f0..."
                        name: "Personal Brand"
                        color: "#ffeda0"
                        isDefault: true
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Profiles]
      summary: Create a new profile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                description: { type: string }
                color: { type: string, example: '#ffeda0' }
            example:
              name: "Marketing Team"
              description: "Profile for marketing campaigns"
              color: "#4CAF50"
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileCreateResponse'
              example:
                message: "Profile created successfully"
                profile:
                  _id: "64f0a1b2c3d4e5f6a7b8c9d0"
                  userId: "6507a1b2c3d4e5f6a7b8c9d0"
                  name: "Marketing Team"
                  description: "Profile for marketing campaigns"
                  color: "#4CAF50"
                  isDefault: false
                  createdAt: "2024-11-01T10:00:00Z"
        '400': { description: Invalid request }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: Profile limit exceeded }
  /v1/profiles/{profileId}:
    get:
      tags: [Profiles]
      summary: Get a profile by id
      parameters:
        - name: profileId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  profile: { $ref: '#/components/schemas/Profile' }
              example:
                profile:
                  _id: "64f0a1b2c3d4e5f6a7b8c9d0"
                  userId: "6507a1b2c3d4e5f6a7b8c9d0"
                  name: "Marketing Team"
                  description: "Profile for marketing campaigns"
                  color: "#4CAF50"
                  isDefault: false
                  createdAt: "2024-11-01T10:00:00Z"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Profiles]
      summary: Update a profile
      parameters:
        - name: profileId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
                color: { type: string }
                isDefault: { type: boolean }
            example:
              name: "Marketing Team (Updated)"
              color: "#2196F3"
              isDefault: true
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  profile: { $ref: '#/components/schemas/Profile' }
              example:
                message: "Profile updated successfully"
                profile:
                  _id: "64f0a1b2c3d4e5f6a7b8c9d0"
                  userId: "6507a1b2c3d4e5f6a7b8c9d0"
                  name: "Marketing Team (Updated)"
                  description: "Profile for marketing campaigns"
                  color: "#2196F3"
                  isDefault: true
                  createdAt: "2024-11-01T10:00:00Z"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Profiles]
      summary: Delete a profile (must have no connected accounts)
      parameters:
        - name: profileId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
              example:
                message: "Profile deleted successfully"
        '400': { description: Has connected accounts }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: Forbidden }
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/accounts:
    get:
      tags: [Accounts]
      summary: List connected social accounts
      description: |
        Returns list of connected social accounts.
        By default, only returns accounts from profiles within the user's plan limit.
        Follower count data (followersCount, followersLastUpdated) is only included if user has analytics add-on.
      parameters:
        - name: profileId
          in: query
          schema: { type: string }
          description: Filter accounts by profile ID
        - name: includeOverLimit
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: |
            When true, includes accounts from profiles that exceed the user's plan limit.
            Useful for disconnecting accounts from over-limit profiles so they can be deleted.
      responses:
        '200':
          description: Accounts
          content:
            application/json:
              schema:
                type: object
                properties:
                  accounts:
                    type: array
                    items: { $ref: '#/components/schemas/SocialAccount' }
                  hasAnalyticsAccess:
                    type: boolean
                    description: Whether user has analytics add-on access
              examples:
                example:
                  value:
                    accounts:
                      - _id: "64e1..."
                        platform: "twitter"
                        profileId:
                          _id: "64f0..."
                          name: "My Brand"
                          slug: "my-brand"
                        username: "@acme"
                        displayName: "Acme"
                        profileUrl: "https://x.com/acme"
                        isActive: true
                    hasAnalyticsAccess: false
        '401': { $ref: '#/components/responses/Unauthorized' }
  /v1/accounts/follower-stats:
    get:
      tags: [Accounts, Analytics]
      summary: Get follower stats and growth metrics
      description: |
        Returns follower count history and growth metrics for connected social accounts.
        **Requires analytics add-on subscription.**

        **Data Freshness:** Follower counts are automatically refreshed once per day.
      parameters:
        - name: accountIds
          in: query
          schema: { type: string }
          description: Comma-separated list of account IDs (optional, defaults to all user's accounts)
        - name: profileId
          in: query
          schema: { type: string }
          description: Filter by profile ID
        - name: fromDate
          in: query
          schema: { type: string, format: date }
          description: Start date in YYYY-MM-DD format (defaults to 30 days ago)
        - name: toDate
          in: query
          schema: { type: string, format: date }
          description: End date in YYYY-MM-DD format (defaults to today)
        - name: granularity
          in: query
          schema: { type: string, enum: [daily, weekly, monthly], default: daily }
          description: Data aggregation level
      responses:
        '200':
          description: Follower stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  accounts:
                    type: array
                    items:
                      $ref: '#/components/schemas/AccountWithFollowerStats'
                  stats:
                    type: object
                    additionalProperties:
                      type: array
                      items:
                        type: object
                        properties:
                          date: { type: string, format: date }
                          followers: { type: number }
                  dateRange:
                    type: object
                    properties:
                      from: { type: string, format: date-time }
                      to: { type: string, format: date-time }
                  granularity: { type: string }
              examples:
                example:
                  value:
                    accounts:
                      - _id: "64e1..."
                        platform: "twitter"
                        username: "@acme"
                        currentFollowers: 1250
                        growth: 50
                        growthPercentage: 4.17
                        dataPoints: 30
                    stats:
                      "64e1...":
                        - date: "2024-01-01"
                          followers: 1200
                        - date: "2024-01-02"
                          followers: 1250
                    dateRange:
                      from: "2024-01-01T00:00:00.000Z"
                      to: "2024-01-31T23:59:59.999Z"
                    granularity: "daily"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Analytics add-on required
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string, example: Analytics add-on required }
                  message: { type: string, example: Follower stats tracking requires the Analytics add-on. Please upgrade to access this feature. }
                  requiresAddon: { type: boolean, example: true }
  /v1/accounts/{accountId}:
    put:
      tags: [Accounts]
      summary: Update a social account
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                username: { type: string }
                displayName: { type: string }
            example:
              displayName: "Acme Corporation Official"
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  username: { type: string }
                  displayName: { type: string }
              example:
                message: "Account updated successfully"
                username: "@acmecorp"
                displayName: "Acme Corporation Official"
        '400': { description: Invalid request }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Accounts]
      summary: Disconnect a social account
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Disconnected
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
              example:
                message: "Account disconnected successfully"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/accounts/health:
    get:
      tags: [Accounts]
      summary: Check health of all connected accounts
      description: |
        Returns the health status of all connected social accounts, including token validity,
        permissions status, and any issues that need attention. Useful for monitoring account
        connections and identifying accounts that need reconnection.
      parameters:
        - name: profileId
          in: query
          description: Filter by profile ID
          schema: { type: string }
        - name: platform
          in: query
          description: Filter by platform
          schema:
            type: string
            enum: [facebook, instagram, linkedin, twitter, tiktok, youtube, threads, pinterest, reddit, bluesky, googlebusiness, telegram]
        - name: status
          in: query
          description: Filter by health status
          schema:
            type: string
            enum: [healthy, warning, error]
      responses:
        '200':
          description: Account health summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: object
                    properties:
                      total: { type: integer, description: Total number of accounts }
                      healthy: { type: integer, description: Number of healthy accounts }
                      warning: { type: integer, description: Number of accounts with warnings }
                      error: { type: integer, description: Number of accounts with errors }
                      needsReconnect: { type: integer, description: Number of accounts needing reconnection }
                  accounts:
                    type: array
                    items:
                      type: object
                      properties:
                        accountId: { type: string }
                        platform: { type: string }
                        username: { type: string }
                        displayName: { type: string }
                        profileId: { type: string }
                        status: { type: string, enum: [healthy, warning, error] }
                        canPost: { type: boolean }
                        canFetchAnalytics: { type: boolean }
                        tokenValid: { type: boolean }
                        tokenExpiresAt: { type: string, format: date-time }
                        needsReconnect: { type: boolean }
                        issues: { type: array, items: { type: string } }
              example:
                summary:
                  total: 5
                  healthy: 3
                  warning: 1
                  error: 1
                  needsReconnect: 1
                accounts:
                  - accountId: "abc123"
                    platform: "instagram"
                    username: "myaccount"
                    status: "healthy"
                    canPost: true
                    canFetchAnalytics: true
                    tokenValid: true
                    tokenExpiresAt: "2025-06-15T00:00:00Z"
                    needsReconnect: false
                    issues: []
                  - accountId: "def456"
                    platform: "twitter"
                    username: "mytwitter"
                    status: "error"
                    canPost: false
                    canFetchAnalytics: false
                    tokenValid: false
                    needsReconnect: true
                    issues: ["Token expired"]
        '401': { $ref: '#/components/responses/Unauthorized' }
  /v1/accounts/{accountId}/health:
    get:
      tags: [Accounts]
      summary: Check health of a specific account
      description: |
        Returns detailed health information for a specific social account, including token status,
        granted permissions, missing permissions, and actionable recommendations.
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
          description: The account ID to check
      responses:
        '200':
          description: Account health details
          content:
            application/json:
              schema:
                type: object
                properties:
                  accountId: { type: string }
                  platform: { type: string }
                  username: { type: string }
                  displayName: { type: string }
                  status:
                    type: string
                    enum: [healthy, warning, error]
                    description: Overall health status
                  tokenStatus:
                    type: object
                    properties:
                      valid: { type: boolean, description: Whether the token is valid }
                      expiresAt: { type: string, format: date-time }
                      expiresIn: { type: string, description: Human-readable time until expiry }
                      needsRefresh: { type: boolean, description: Whether token expires within 24 hours }
                  permissions:
                    type: object
                    properties:
                      posting:
                        type: array
                        items:
                          type: object
                          properties:
                            scope: { type: string }
                            granted: { type: boolean }
                            required: { type: boolean }
                      analytics:
                        type: array
                        items:
                          type: object
                          properties:
                            scope: { type: string }
                            granted: { type: boolean }
                            required: { type: boolean }
                      optional:
                        type: array
                        items:
                          type: object
                          properties:
                            scope: { type: string }
                            granted: { type: boolean }
                            required: { type: boolean }
                      canPost: { type: boolean }
                      canFetchAnalytics: { type: boolean }
                      missingRequired: { type: array, items: { type: string } }
                  issues:
                    type: array
                    items: { type: string }
                    description: List of issues found
                  recommendations:
                    type: array
                    items: { type: string }
                    description: Actionable recommendations to fix issues
              example:
                accountId: "abc123"
                platform: "instagram"
                username: "myaccount"
                displayName: "My Account"
                status: "healthy"
                tokenStatus:
                  valid: true
                  expiresAt: "2025-06-15T00:00:00Z"
                  expiresIn: "180 days"
                  needsRefresh: false
                permissions:
                  posting:
                    - scope: "instagram_basic"
                      granted: true
                      required: true
                    - scope: "instagram_content_publish"
                      granted: true
                      required: true
                  analytics:
                    - scope: "instagram_manage_insights"
                      granted: true
                      required: false
                  optional: []
                  canPost: true
                  canFetchAnalytics: true
                  missingRequired: []
                issues: []
                recommendations: []
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/api-keys:
    get:
      tags: [API Keys]
      summary: List API keys for the current user
      responses:
        '200':
          description: API keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiKeys:
                    type: array
                    items: { $ref: '#/components/schemas/ApiKey' }
              example:
                apiKeys:
                  - id: "6507a1b2c3d4e5f6a7b8c9d0"
                    name: "Production API Key"
                    keyPreview: "late_...xyz123"
                    expiresAt: "2025-12-31T23:59:59Z"
                    createdAt: "2024-01-15T10:30:00Z"
                  - id: "6507a1b2c3d4e5f6a7b8c9d1"
                    name: "Development Key"
                    keyPreview: "late_...abc789"
                    expiresAt: null
                    createdAt: "2024-03-20T14:45:00Z"
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [API Keys]
      summary: Create a new API key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                expiresIn:
                  type: integer
                  description: Days until expiry
            example:
              name: "Production API Key"
              expiresIn: 365
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  apiKey: { $ref: '#/components/schemas/ApiKey' }
              example:
                message: "API key created successfully"
                apiKey:
                  id: "6507a1b2c3d4e5f6a7b8c9d0"
                  name: "Production API Key"
                  keyPreview: "late_...xyz123"
                  key: "late_sk_1234567890abcdef1234567890abcdef"
                  expiresAt: "2025-12-31T23:59:59Z"
                  createdAt: "2024-01-15T10:30:00Z"
        '400': { description: Invalid request }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /v1/api-keys/{keyId}:
    delete:
      tags: [API Keys]
      summary: Delete an API key
      parameters:
        - name: keyId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
              example:
                message: "API key deleted successfully"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/invite/tokens:
    post:
      tags: [Invites]
      summary: Create a team member invite token
      description: |
        Generate a secure invite link to grant team members access to your profiles.
        Invites expire after 7 days and are single-use.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [scope]
              properties:
                scope:
                  type: string
                  enum: [all, profiles]
                  description: "'all' grants access to all profiles, 'profiles' restricts to specific profiles"
                profileIds:
                  type: array
                  items: { type: string }
                  description: Required if scope is 'profiles'. Array of profile IDs to grant access to.
            example:
              scope: "profiles"
              profileIds:
                - "64f0a1b2c3d4e5f6a7b8c9d0"
                - "64f0a1b2c3d4e5f6a7b8c9d1"
      responses:
        '201':
          description: Invite token created
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  scope: { type: string }
                  invitedProfileIds:
                    type: array
                    items: { type: string }
                  expiresAt: { type: string, format: date-time }
                  inviteUrl: { type: string, format: uri }
              example:
                token: "inv_abc123def456ghi789"
                scope: "profiles"
                invitedProfileIds:
                  - "64f0a1b2c3d4e5f6a7b8c9d0"
                  - "64f0a1b2c3d4e5f6a7b8c9d1"
                expiresAt: "2024-11-08T10:30:00Z"
                inviteUrl: "https://getlate.dev/invite/inv_abc123def456ghi789"
        '400': { description: Invalid request }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: One or more profiles not found or not owned }

  /v1/platform-invites:
    get:
      tags: [Invites]
      summary: List platform connection invites
      description: Get all platform connection invites you've created
      parameters:
        - name: profileId
          in: query
          schema: { type: string }
          description: Optional. Filter invites by profile ID
      responses:
        '200':
          description: Invites list
          content:
            application/json:
              schema:
                type: object
                properties:
                  invites:
                    type: array
                    items:
                      type: object
                      properties:
                        _id: { type: string }
                        token: { type: string }
                        userId: { type: string }
                        profileId:
                          type: object
                          description: Populated profile object (not a string ID)
                          properties:
                            _id: { type: string }
                            name: { type: string }
                        platform: { type: string }
                        inviterName: { type: string }
                        inviterEmail: { type: string }
                        expiresAt: { type: string, format: date-time }
                        isUsed: { type: boolean }
                        createdAt: { type: string, format: date-time }
              example:
                invites:
                  - _id: "6507a1b2c3d4e5f6a7b8c9d0"
                    token: "pinv_abc123def456"
                    userId: "6507a1b2c3d4e5f6a7b8c9d1"
                    profileId:
                      _id: "64f0a1b2c3d4e5f6a7b8c9d0"
                      name: "Client Profile"
                    platform: "instagram"
                    inviterName: "John Doe"
                    inviterEmail: "john@example.com"
                    expiresAt: "2024-11-08T10:30:00Z"
                    isUsed: false
                    createdAt: "2024-11-01T10:30:00Z"
                  - _id: "6507a1b2c3d4e5f6a7b8c9d2"
                    token: "pinv_xyz789ghi012"
                    userId: "6507a1b2c3d4e5f6a7b8c9d1"
                    profileId:
                      _id: "64f0a1b2c3d4e5f6a7b8c9d0"
                      name: "Client Profile"
                    platform: "tiktok"
                    inviterName: "John Doe"
                    inviterEmail: "john@example.com"
                    expiresAt: "2024-11-08T14:00:00Z"
                    isUsed: true
                    createdAt: "2024-11-01T14:00:00Z"
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Invites]
      summary: Create a platform connection invite
      description: |
        Generate a secure invite link for someone to connect a social account to your profile.
        Perfect for client onboarding - they connect their account without accessing your Late account.
        Invites expire after 7 days.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [profileId, platform]
              properties:
                profileId:
                  type: string
                  description: Profile ID to connect the account to
                platform:
                  type: string
                  enum: [facebook, instagram, linkedin, twitter, threads, tiktok, youtube, pinterest, reddit, bluesky, googlebusiness]
                  description: Platform to connect
            example:
              profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
              platform: "instagram"
      responses:
        '200':
          description: Platform invite created
          content:
            application/json:
              schema:
                type: object
                properties:
                  invite:
                    type: object
                    properties:
                      _id: { type: string }
                      token: { type: string }
                      userId: { type: string }
                      profileId:
                        type: object
                        description: Populated profile object (not a string ID)
                        properties:
                          _id: { type: string }
                          name: { type: string }
                      platform: { type: string }
                      inviterName: { type: string }
                      inviterEmail: { type: string }
                      expiresAt: { type: string, format: date-time }
                      isUsed: { type: boolean }
                      createdAt: { type: string, format: date-time }
                      inviteUrl: { type: string, format: uri }
              example:
                invite:
                  _id: "6507a1b2c3d4e5f6a7b8c9d0"
                  token: "pinv_abc123def456"
                  userId: "6507a1b2c3d4e5f6a7b8c9d1"
                  profileId:
                    _id: "64f0a1b2c3d4e5f6a7b8c9d0"
                    name: "Client Profile"
                  platform: "instagram"
                  inviterName: "John Doe"
                  inviterEmail: "john@example.com"
                  expiresAt: "2024-11-08T10:30:00Z"
                  isUsed: false
                  createdAt: "2024-11-01T10:30:00Z"
                  inviteUrl: "https://getlate.dev/platform-invite/pinv_abc123def456"
        '400': { description: Invalid request or platform }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: Access denied to profile }
        '404': { description: Profile or user not found }
    delete:
      tags: [Invites]
      summary: Revoke a platform connection invite
      description: Delete an unused platform invite. Only unused invites can be deleted.
      parameters:
        - name: id
          in: query
          required: true
          schema: { type: string }
          description: Invite ID to revoke
      responses:
        '200':
          description: Invite revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
              example:
                message: "Platform invite revoked successfully"
        '400': { description: Invite ID required }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Invite not found or cannot be deleted }

  /v1/connect/{platform}:
    get:
      tags: [Connect]
      summary: Start OAuth connection for a platform
      description: |
        Initiate an OAuth connection flow for any supported social media platform.
        
        **Standard Flow (Hosted UI):**
        For Facebook connections, Late hosts the page selection UI:
        
        1. Call this endpoint with your API key and `redirect_url` (optional)
        2. Redirect your user to the returned `authUrl`
        3. After OAuth, the user is redirected to Late’s hosted page selector at  
           `/connect/facebook/select-page?profileId=X&tempToken=Y&userProfile=Z&redirect_url=YOUR_URL&connect_token=CT`
        4. After they pick a page, Late saves the connection and finally redirects to your `redirect_url` (if provided)
        
        **Headless/Whitelabel Mode (Facebook, LinkedIn, Pinterest & Google Business Profile):**
        Build your own fully branded selection UI while Late handles OAuth:
        
        **Facebook:**
        1. Call this endpoint with your API key and add `&headless=true`, e.g.  
           `GET /v1/connect/facebook?profileId=PROFILE_ID&redirect_url=https://yourapp.com/callback&headless=true`
        2. Redirect your user to the returned `authUrl`
        3. After OAuth, the user is redirected directly to **your** `redirect_url` with:
           - `profileId` – your Late profile ID  
           - `tempToken` – temporary Facebook access token  
           - `userProfile` – URL‑encoded JSON user profile  
           - `connect_token` – short‑lived connect token (for API auth)  
           - `platform=facebook`  
           - `step=select_page`
        4. Use `tempToken`, `userProfile`, and the `X-Connect-Token` header with:
           - `GET /v1/connect/facebook/select-page` to fetch pages
           - `POST /v1/connect/facebook/select-page` to save the selected page
        5. In this mode, users never see Late's hosted page selector – only your UI.
        
        **LinkedIn:**
        1. Call this endpoint with `&headless=true`, e.g.  
           `GET /v1/connect/linkedin?profileId=PROFILE_ID&redirect_url=https://yourapp.com/callback&headless=true`
        2. Redirect your user to the returned `authUrl`
        3. After OAuth, the user is redirected directly to **your** `redirect_url` with:
           - `profileId` – your Late profile ID  
           - `tempToken` – temporary LinkedIn access token  
           - `userProfile` – URL‑encoded JSON with `id`, `username`, `displayName`, `profilePicture`  
           - `organizations` – URL‑encoded JSON array with `id`, `urn`, `name` for each org (logos not included to prevent URL length issues)
           - `connect_token` – short‑lived connect token (for API auth)  
           - `platform=linkedin`  
           - `step=select_organization`
        4. The `organizations` array contains minimal data (`id`, `urn`, `name`). Use it to build your selection UI.
        5. **Optional:** To fetch full organization details (logos, vanityName, website, industry, description), call `GET /v1/connect/linkedin/organizations?tempToken=X&orgIds=id1,id2,...`
        6. Call `POST /v1/connect/linkedin/select-organization` with the `X-Connect-Token` header to save the selection.
        7. In this mode, users never see Late's hosted organization selector – only your UI.
        8. Note: If the user has no organization admin access, `step=select_organization` will NOT be present,
           and the account will be connected directly as a personal account.
        
        **Pinterest:**
        1. Call this endpoint with `&headless=true`, e.g.
           `GET /v1/connect/pinterest?profileId=PROFILE_ID&redirect_url=https://yourapp.com/callback&headless=true`
        2. Redirect your user to the returned `authUrl`
        3. After OAuth, the user is redirected directly to **your** `redirect_url` with:
           - `profileId` – your Late profile ID
           - `tempToken` – temporary Pinterest access token
           - `userProfile` – URL‑encoded JSON user profile
           - `connect_token` – short‑lived connect token (for API auth)
           - `platform=pinterest`
           - `step=select_board`
        4. Use `tempToken`, `userProfile`, and the `X-Connect-Token` header with:
           - `GET /v1/connect/pinterest/select-board` to fetch boards
           - `POST /v1/connect/pinterest/select-board` to save the selected board
        5. In this mode, users never see Late's hosted board selector – only your UI.

        **Google Business Profile:**
        1. Call this endpoint with `&headless=true`, e.g.
           `GET /v1/connect/googlebusiness?profileId=PROFILE_ID&redirect_url=https://yourapp.com/callback&headless=true`
        2. Redirect your user to the returned `authUrl`
        3. After OAuth, the user is redirected directly to **your** `redirect_url` with:
           - `profileId` – your Late profile ID
           - `tempToken` – temporary Google access token
           - `userProfile` – URL‑encoded JSON user profile (includes refresh token info)
           - `connect_token` – short‑lived connect token (for API auth)
           - `platform=googlebusiness`
           - `step=select_location`
        4. Use `tempToken`, `userProfile`, and the `X-Connect-Token` header with:
           - `GET /v1/connect/googlebusiness/locations` to fetch business locations
           - `POST /v1/connect/googlebusiness/select-location` to save the selected location
        5. In this mode, users never see Late's hosted location selector – only your UI.
      parameters:
        - name: platform
          in: path
          required: true
          schema: 
            type: string
            enum: [facebook, instagram, linkedin, twitter, tiktok, youtube, threads, reddit, pinterest, bluesky, googlebusiness]
          description: Social media platform to connect
        - name: profileId
          in: query
          required: true
          schema: { type: string }
          description: Your Late profile ID (get from /v1/profiles)
        - name: redirect_url
          in: query
          schema: { type: string, format: uri }
          description: |
            Optional: Your custom redirect URL after connection completes.
            
            **Standard Mode:** Omit `headless=true` to use our hosted page selection UI.  
            After the user selects a Facebook Page, Late redirects here with:  
            `?connected=facebook&profileId=X&username=Y`
            
            **Headless Mode (Facebook, LinkedIn, Pinterest & Google Business Profile):**
            Pass `headless=true` as a query parameter on this endpoint (not inside `redirect_url`), e.g.:
            `GET /v1/connect/facebook?profileId=PROFILE_ID&redirect_url=https://yourapp.com/callback&headless=true`
            `GET /v1/connect/linkedin?profileId=PROFILE_ID&redirect_url=https://yourapp.com/callback&headless=true`
            `GET /v1/connect/pinterest?profileId=PROFILE_ID&redirect_url=https://yourapp.com/callback&headless=true`
            `GET /v1/connect/googlebusiness?profileId=PROFILE_ID&redirect_url=https://yourapp.com/callback&headless=true`

            After OAuth, the user is redirected directly to your `redirect_url` with OAuth data:
            - **Facebook:** `?profileId=X&tempToken=Y&userProfile=Z&connect_token=CT&platform=facebook&step=select_page`
            - **LinkedIn:** `?profileId=X&tempToken=Y&userProfile=Z&organizations=ORGS&connect_token=CT&platform=linkedin&step=select_organization`
              (organizations contains `id`, `urn`, `name` only - use `/v1/connect/linkedin/organizations` to fetch full details)
            - **Pinterest:** `?profileId=X&tempToken=Y&userProfile=Z&connect_token=CT&platform=pinterest&step=select_board`
            - **Google Business:** `?profileId=X&tempToken=Y&userProfile=Z&connect_token=CT&platform=googlebusiness&step=select_location`

            Then use the respective endpoints to build your custom UI:
            - Facebook: `/v1/connect/facebook/select-page` (GET to fetch, POST to save)
            - LinkedIn: `/v1/connect/linkedin/organizations` (GET to fetch logos), `/v1/connect/linkedin/select-organization` (POST to save)
            - Pinterest: `/v1/connect/pinterest/select-board` (GET to fetch, POST to save)
            - Google Business: `/v1/connect/googlebusiness/locations` (GET) and `/v1/connect/googlebusiness/select-location` (POST)
            
            Example: `https://yourdomain.com/integrations/callback`
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OAuth authorization URL to redirect user to
          content:
            application/json:
              schema:
                type: object
                properties:
                  authUrl: 
                    type: string
                    format: uri
                    description: URL to redirect your user to for OAuth authorization
                  state: 
                    type: string
                    description: State parameter for security (handled automatically)
              example:
                authUrl: "https://www.facebook.com/v21.0/dialog/oauth?client_id=..."
                state: "user123-profile456-1234567890-https://yourdomain.com/callback"
        '400':
          description: "Missing/invalid parameters (e.g., invalid profileId format)"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: "No access to profile, or BYOK required for AppSumo Twitter"
        '404':
          description: Profile not found
    post:
      tags: [Connect]
      summary: Complete OAuth token exchange manually (for server-side flows)
      parameters:
        - name: platform
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, state, profileId]
              properties:
                code: { type: string }
                state: { type: string }
                profileId: { type: string }
      responses:
        '200': { description: Account connected }
        '400': { description: Invalid params }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: BYOK required for AppSumo Twitter }
        '500': { description: Failed to connect account }

  /v1/connect/facebook/select-page:
    get:
      tags: [Connect]
      summary: List Facebook Pages after OAuth (Headless Mode)
      description: |
        **Headless Mode for Custom UI**
        
        After initiating Facebook OAuth via `/v1/connect/facebook`, you'll be redirected to 
        `/connect/facebook/select-page` with query params including `tempToken` and `userProfile`.
        
        For a **headless/whitelabeled flow**, extract these params from the URL and call this 
        endpoint to retrieve the list of Facebook Pages the user can manage. Then build your 
        own UI to let users select a page.
        
        **Note:** Use the `X-Connect-Token` header if you initiated the connection via API key 
        (rather than a browser session).
      parameters:
        - name: profileId
          in: query
          required: true
          schema: { type: string }
          description: Profile ID from your connection flow
        - name: tempToken
          in: query
          required: true
          schema: { type: string }
          description: Temporary Facebook access token from the OAuth callback redirect
      security:
        - bearerAuth: []
        - connectToken: []
      responses:
        '200':
          description: List of Facebook Pages available for connection
          content:
            application/json:
              schema:
                type: object
                properties:
                  pages:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, description: Facebook Page ID }
                        name: { type: string, description: Page name }
                        username: { type: string, description: Page username/handle (may be null) }
                        access_token: { type: string, description: Page-specific access token }
                        category: { type: string, description: Page category }
                        tasks: { type: array, items: { type: string }, description: User permissions for this page }
              example:
                pages:
                  - id: "123456789"
                    name: "My Brand Page"
                    username: "mybrand"
                    access_token: "EAAxxxxx..."
                    category: "Brand"
                    tasks: ["MANAGE", "CREATE_CONTENT"]
        '400': { description: Missing required parameters (profileId or tempToken) }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': 
          description: Failed to fetch pages (e.g., invalid token, insufficient permissions)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
    post:
      tags: [Connect]
      summary: Select a Facebook Page to complete the connection (Headless Mode)
      description: |
        **Complete the Headless Flow**
        
        After displaying your custom UI with the list of pages from the GET endpoint, call this 
        endpoint to finalize the connection with the user's selected page.
        
        The `userProfile` should be the decoded JSON object from the `userProfile` query param 
        in the OAuth callback redirect URL.
        
        **Note:** Use the `X-Connect-Token` header if you initiated the connection via API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [profileId, pageId, tempToken]
              properties:
                profileId: 
                  type: string
                  description: Profile ID from your connection flow
                pageId: 
                  type: string
                  description: The Facebook Page ID selected by the user
                tempToken: 
                  type: string
                  description: Temporary Facebook access token from OAuth
                userProfile: 
                  type: object
                  description: Decoded user profile object from the OAuth callback
                  properties:
                    id: { type: string }
                    name: { type: string }
                    profilePicture: { type: string }
                redirect_url: 
                  type: string
                  format: uri
                  description: Optional custom redirect URL to return to after selection
            example:
              profileId: "507f1f77bcf86cd799439011"
              pageId: "123456789"
              tempToken: "EAAxxxxx..."
              userProfile:
                id: "987654321"
                name: "John Doe"
                profilePicture: "https://..."
              redirect_url: "https://yourdomain.com/integrations/callback"
      security:
        - bearerAuth: []
        - connectToken: []
      responses:
        '200': 
          description: Facebook Page connected successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  redirect_url: 
                    type: string
                    description: Redirect URL if custom redirect_url was provided
                  account:
                    type: object
                    properties:
                      platform: { type: string, enum: [facebook] }
                      username: { type: string }
                      displayName: { type: string }
                      profilePicture: { type: string }
                      isActive: { type: boolean }
                      selectedPageName: { type: string }
              example:
                message: "Facebook page connected successfully"
                redirect_url: "https://yourdomain.com/integrations/callback?connected=facebook&profileId=507f1f77bcf86cd799439011&username=My+Brand+Page"
                account:
                  platform: "facebook"
                  username: "mybrand"
                  displayName: "My Brand Page"
                  profilePicture: "https://..."
                  isActive: true
                  selectedPageName: "My Brand Page"
        '400':
          description: "Missing required fields (profileId, pageId, or tempToken)"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: User does not have access to the specified profile
        '404':
          description: Selected page not found in available pages
        '500':
          description: Failed to save Facebook connection

  /v1/connect/googlebusiness/locations:
    get:
      tags: [Connect]
      summary: List Google Business Locations after OAuth (Headless Mode)
      description: |
        **Headless Mode for Custom UI**
        
        After initiating Google Business OAuth via `/v1/connect/googlebusiness?headless=true`, you'll be redirected 
        to your `redirect_url` with query params including `tempToken` and `userProfile`.
        
        For a **headless/whitelabeled flow**, extract these params from the URL and call this 
        endpoint to retrieve the list of Google Business locations the user can manage. Then build your 
        own UI to let users select a location.
        
        **Note:** Use the `X-Connect-Token` header if you initiated the connection via API key 
        (rather than a browser session).
      parameters:
        - name: profileId
          in: query
          required: true
          schema: { type: string }
          description: Profile ID from your connection flow
        - name: tempToken
          in: query
          required: true
          schema: { type: string }
          description: Temporary Google access token from the OAuth callback redirect
      security:
        - bearerAuth: []
        - connectToken: []
      responses:
        '200':
          description: List of Google Business locations available for connection
          content:
            application/json:
              schema:
                type: object
                properties:
                  locations:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, description: Location ID }
                        name: { type: string, description: Business name }
                        accountId: { type: string, description: Google Business Account ID }
                        accountName: { type: string, description: Account name }
                        address: { type: string, description: Business address }
                        category: { type: string, description: Business category }
              example:
                locations:
                  - id: "9281089117903930794"
                    name: "My Coffee Shop"
                    accountId: "accounts/113303573364907650416"
                    accountName: "My Business Account"
                    address: "123 Main St, City, Country"
                    category: "Coffee shop"
        '400': { description: Missing required parameters (profileId or tempToken) }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': 
          description: Failed to fetch locations (e.g., invalid token, insufficient permissions)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }

  /v1/connect/googlebusiness/select-location:
    post:
      tags: [Connect]
      summary: Select a Google Business location to complete the connection (Headless Mode)
      description: |
        **Complete the Headless Flow**
        
        After displaying your custom UI with the list of locations from the GET `/v1/connect/googlebusiness/locations` 
        endpoint, call this endpoint to finalize the connection with the user's selected location.
        
        The `userProfile` should be the decoded JSON object from the `userProfile` query param 
        in the OAuth callback redirect URL. It contains important token information (including refresh token).
        
        **Note:** Use the `X-Connect-Token` header if you initiated the connection via API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [profileId, locationId, tempToken]
              properties:
                profileId: 
                  type: string
                  description: Profile ID from your connection flow
                locationId: 
                  type: string
                  description: The Google Business location ID selected by the user
                tempToken: 
                  type: string
                  description: Temporary Google access token from OAuth
                userProfile: 
                  type: object
                  description: |
                    Decoded user profile object from the OAuth callback. **Important:** This contains 
                    the refresh token needed for token refresh. Always include this field.
                  properties:
                    id: { type: string }
                    name: { type: string }
                    refreshToken: { type: string, description: Google refresh token for long-lived access }
                    tokenExpiresIn: { type: integer, description: Token expiration time in seconds }
                    scope: { type: string, description: Granted OAuth scopes }
                redirect_url: 
                  type: string
                  format: uri
                  description: Optional custom redirect URL to return to after selection
            example:
              profileId: "507f1f77bcf86cd799439011"
              locationId: "9281089117903930794"
              tempToken: "ya29.xxxxx..."
              userProfile:
                id: "113303573364907650416"
                name: "John Doe"
                refreshToken: "1//0gxxxxx..."
                tokenExpiresIn: 3599
                scope: "https://www.googleapis.com/auth/business.manage"
              redirect_url: "https://yourdomain.com/integrations/callback"
      security:
        - bearerAuth: []
        - connectToken: []
      responses:
        '200': 
          description: Google Business location connected successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  redirect_url: 
                    type: string
                    description: Redirect URL if custom redirect_url was provided
                  account:
                    type: object
                    properties:
                      platform: { type: string, enum: [googlebusiness] }
                      username: { type: string }
                      displayName: { type: string }
                      isActive: { type: boolean }
                      selectedLocationName: { type: string }
                      selectedLocationId: { type: string }
              example:
                message: "Google Business location connected successfully"
                redirect_url: "https://yourdomain.com/integrations/callback?connected=googlebusiness&profileId=507f1f77bcf86cd799439011&username=My+Coffee+Shop"
                account:
                  platform: "googlebusiness"
                  username: "My Coffee Shop"
                  displayName: "My Coffee Shop"
                  isActive: true
                  selectedLocationName: "My Coffee Shop"
                  selectedLocationId: "9281089117903930794"
        '400':
          description: "Missing required fields (profileId, locationId, or tempToken)"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: User does not have access to the specified profile
        '404':
          description: Selected location not found in available locations
        '500':
          description: Failed to save Google Business connection

  /v1/accounts/{accountId}/gmb-reviews:
    get:
      tags: [GMB Reviews]
      summary: Get Google Business Profile reviews
      description: |
        Fetches reviews for a connected Google Business Profile account.
        
        Returns all reviews for the business location, including:
        - Reviewer information (name, profile photo)
        - Star rating (1-5)
        - Review comment/text
        - Business owner's reply (if any)
        - Review timestamps
        
        Use pagination via `nextPageToken` to fetch all reviews for locations with many reviews.
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
          description: The Late account ID (from /v1/accounts)
        - name: pageSize
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 50 }
          description: Number of reviews to fetch per page (max 50)
        - name: pageToken
          in: query
          schema: { type: string }
          description: Pagination token from previous response
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Reviews fetched successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  accountId: { type: string }
                  locationId: { type: string }
                  reviews:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, description: Review ID }
                        name: { type: string, description: Full resource name }
                        reviewer:
                          type: object
                          properties:
                            displayName: { type: string }
                            profilePhotoUrl: { type: string, nullable: true }
                            isAnonymous: { type: boolean }
                        rating: { type: integer, minimum: 1, maximum: 5, description: Numeric star rating }
                        starRating: { type: string, enum: [ONE, TWO, THREE, FOUR, FIVE], description: Google's string rating }
                        comment: { type: string, description: Review text }
                        createTime: { type: string, format: date-time }
                        updateTime: { type: string, format: date-time }
                        reviewReply:
                          type: object
                          nullable: true
                          properties:
                            comment: { type: string, description: Business owner reply }
                            updateTime: { type: string, format: date-time }
                  averageRating: { type: number, description: Overall average rating }
                  totalReviewCount: { type: integer, description: Total number of reviews }
                  nextPageToken: { type: string, nullable: true, description: Token for next page }
              example:
                success: true
                accountId: "64e1f0a9e2b5af0012ab34cd"
                locationId: "9281089117903930794"
                reviews:
                  - id: "AIe9_BGx1234567890"
                    name: "accounts/123456789/locations/9281089117903930794/reviews/AIe9_BGx1234567890"
                    reviewer:
                      displayName: "John Smith"
                      profilePhotoUrl: "https://lh3.googleusercontent.com/a/..."
                      isAnonymous: false
                    rating: 5
                    starRating: "FIVE"
                    comment: "Great service and friendly staff! Highly recommend."
                    createTime: "2024-01-15T10:30:00Z"
                    updateTime: "2024-01-15T10:30:00Z"
                    reviewReply:
                      comment: "Thank you for your kind words! We appreciate your support."
                      updateTime: "2024-01-16T08:00:00Z"
                  - id: "AIe9_BGx0987654321"
                    name: "accounts/123456789/locations/9281089117903930794/reviews/AIe9_BGx0987654321"
                    reviewer:
                      displayName: "Anonymous"
                      profilePhotoUrl: null
                      isAnonymous: true
                    rating: 4
                    starRating: "FOUR"
                    comment: "Good experience overall."
                    createTime: "2024-01-10T14:20:00Z"
                    updateTime: "2024-01-10T14:20:00Z"
                    reviewReply: null
                averageRating: 4.5
                totalReviewCount: 125
                nextPageToken: "CiAKHAoUMTIzNDU2Nzg5"
        '400':
          description: Invalid request - not a Google Business account or missing location
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
              example:
                error: "This endpoint is only available for Google Business Profile accounts"
        '401':
          description: Unauthorized or token expired
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
              example:
                error: "Access token expired. Please reconnect your Google Business Profile account."
        '403':
          description: Permission denied for this location
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
              example:
                error: "You do not have permission to access reviews for this location."
        '404': { $ref: '#/components/responses/NotFound' }
        '500':
          description: Failed to fetch reviews
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /v1/connect/linkedin/organizations:
    get:
      tags: [Connect]
      summary: Fetch full LinkedIn organization details (Headless Mode)
      description: |
        **Fetch Full Organization Details for Custom UI**
        
        After LinkedIn OAuth in headless mode, the redirect URL contains organization data with only
        `id`, `urn`, and `name` fields (additional details are excluded to prevent URL length issues with many organizations).
        
        Use this endpoint to fetch full organization details including logos, vanity names, websites, and more
        if you want to display them in your custom selection UI.
        
        **Note:** This endpoint requires no authentication - just the `tempToken` from the OAuth redirect.
        Details are fetched directly from LinkedIn's API in parallel for fast response times.
      parameters:
        - name: tempToken
          in: query
          required: true
          schema: { type: string }
          description: The temporary LinkedIn access token from the OAuth redirect
        - name: orgIds
          in: query
          required: true
          schema: { type: string }
          description: Comma-separated list of organization IDs to fetch details for (max 100)
          example: "12345678,87654321,11111111"
      responses:
        '200':
          description: Organization details fetched successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  organizations:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, description: Organization ID }
                        logoUrl: { type: string, format: uri, description: Logo URL (may be absent if no logo) }
                        vanityName: { type: string, description: Organization's vanity name/slug }
                        website: { type: string, format: uri, description: Organization's website URL }
                        industry: { type: string, description: Organization's primary industry }
                        description: { type: string, description: Organization's description }
              example:
                organizations:
                  - id: "12345678"
                    logoUrl: "https://media.licdn.com/dms/image/v2/..."
                    vanityName: "acme-corp"
                    website: "https://acme.com"
                    industry: "Technology"
                    description: "Leading provider of innovative solutions"
                  - id: "87654321"
                    logoUrl: "https://media.licdn.com/dms/image/v2/..."
                    vanityName: "example-inc"
                    website: "https://example.com"
                  - id: "11111111"
        '400':
          description: Missing required parameters or too many organization IDs
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
              example:
                error: "Missing tempToken parameter"
        '500':
          description: Failed to fetch organization details

  /v1/connect/linkedin/select-organization:
    post:
      tags: [Connect]
      summary: Select LinkedIn organization or personal account after OAuth
      description: |
        **Complete the LinkedIn Connection Flow**
        
        After OAuth, the user is redirected with `organizations` in the URL params (if they have org admin access).
        The organizations array contains `id`, `urn`, and `name` fields. Use this data to build your UI, 
        then call this endpoint to save the selection.
        
        Set `accountType` to `personal` to connect as the user's personal LinkedIn profile, or 
        `organization` to connect as a company page (requires `selectedOrganization` object).
        
        **Headless Mode:** Use the `X-Connect-Token` header if you initiated the connection via API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [profileId, tempToken, userProfile, accountType]
              properties:
                profileId: { type: string }
                tempToken: { type: string }
                userProfile: { type: object }
                accountType: { type: string, enum: [personal, organization] }
                selectedOrganization: { type: object }
                redirect_url: { type: string, format: uri }
            example:
              profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
              tempToken: "AQX..."
              userProfile:
                id: "abc123"
                username: "johndoe"
                displayName: "John Doe"
                profilePicture: "https://media.licdn.com/dms/image/v2/..."
              accountType: "organization"
              selectedOrganization:
                id: "12345678"
                urn: "urn:li:organization:12345678"
                name: "Acme Corporation"
              redirect_url: "https://yourapp.com/callback"
      responses:
        '200':
          description: LinkedIn account connected
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  account: { $ref: '#/components/schemas/SocialAccount' }
              example:
                message: "LinkedIn account connected successfully"
                accountType: "organization"
                accountName: "Acme Corporation"
                socialAccount:
                  id: "64e1f0a9e2b5af0012ab34cd"
                  platform: "linkedin"
                  username: "acme-corp"
                  displayName: "Acme Corporation"
                bulkRefresh:
                  updatedCount: 5
                  errors: 0
        '400': { description: Missing required fields }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { description: Failed to connect LinkedIn account }

  /v1/connect/pinterest/select-board:
    get:
      tags: [Connect]
      summary: List Pinterest Boards after OAuth (Headless Mode)
      description: |
        **Retrieve Pinterest Boards for Selection UI**

        After initiating Pinterest OAuth via `/v1/connect/pinterest` with `headless=true`, you'll be redirected to
        your `redirect_url` with query params including `tempToken` and `userProfile`.

        If you want to build your own fully-branded board selector (instead of Late's hosted UI), call this
        endpoint to retrieve the list of Pinterest Boards the user can post to. Then build your
        UI and call `POST /v1/connect/pinterest/select-board` to save the selection.

        **Authentication:** Use `X-Connect-Token` header with the `connect_token` from the redirect URL.
      parameters:
        - name: X-Connect-Token
          in: header
          required: true
          schema: { type: string }
          description: Short-lived connect token from the OAuth redirect
        - name: profileId
          in: query
          required: true
          schema: { type: string }
          description: Your Late profile ID
        - name: tempToken
          in: query
          required: true
          schema: { type: string }
          description: Temporary Pinterest access token from the OAuth callback redirect
      responses:
        '200':
          description: List of Pinterest Boards available for connection
          content:
            application/json:
              schema:
                type: object
                properties:
                  boards:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, description: Pinterest Board ID }
                        name: { type: string, description: Board name }
                        description: { type: string, description: Board description }
                        privacy: { type: string, description: Board privacy setting }
              example:
                boards:
                  - id: "123456789012345678"
                    name: "Marketing Ideas"
                    description: "Collection of marketing inspiration"
                    privacy: "PUBLIC"
                  - id: "234567890123456789"
                    name: "Product Photos"
                    description: "Product photography"
                    privacy: "PUBLIC"
        '400': { description: Missing required parameters }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: No access to profile }
        '500': { description: Failed to fetch boards }
    post:
      tags: [Connect]
      summary: Select a Pinterest Board to complete the connection (Headless Mode)
      description: |
        **Complete the Pinterest Connection Flow**

        After OAuth, use this endpoint to save the selected board and complete the Pinterest account connection.

        **Headless Mode:** Use the `X-Connect-Token` header if you initiated the connection via API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [profileId, boardId, tempToken]
              properties:
                profileId:
                  type: string
                  description: Your Late profile ID
                boardId:
                  type: string
                  description: The Pinterest Board ID selected by the user
                boardName:
                  type: string
                  description: The board name (for display purposes)
                tempToken:
                  type: string
                  description: Temporary Pinterest access token from OAuth
                userProfile:
                  type: object
                  description: User profile data from OAuth redirect
                refreshToken:
                  type: string
                  description: Pinterest refresh token (if available)
                expiresIn:
                  type: integer
                  description: Token expiration time in seconds
                redirect_url:
                  type: string
                  format: uri
                  description: Custom redirect URL after connection completes
            example:
              profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
              boardId: "123456789012345678"
              boardName: "Marketing Ideas"
              tempToken: "pina_..."
              userProfile:
                id: "user123"
                username: "mybrand"
                displayName: "My Brand"
                profilePicture: "https://i.pinimg.com/..."
              redirect_url: "https://yourapp.com/callback"
      responses:
        '200':
          description: Pinterest Board connected successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  redirect_url: { type: string, description: Redirect URL with connection params (if provided) }
                  account:
                    type: object
                    properties:
                      platform: { type: string, enum: [pinterest] }
                      username: { type: string }
                      displayName: { type: string }
                      profilePicture: { type: string }
                      isActive: { type: boolean }
                      defaultBoardName: { type: string }
              example:
                message: "Pinterest connected successfully with default board"
                redirect_url: "https://yourdomain.com/integrations/callback?connected=pinterest&profileId=507f1f77bcf86cd799439011&board=Marketing+Ideas"
                account:
                  platform: "pinterest"
                  username: "mybrand"
                  displayName: "My Brand"
                  profilePicture: "https://i.pinimg.com/..."
                  isActive: true
                  defaultBoardName: "Marketing Ideas"
        '400':
          description: Missing required fields
          content:
            application/json:
              example:
                error: "Missing required fields"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: No access to profile or profile limit exceeded
          content:
            application/json:
              examples:
                forbidden:
                  value: { error: "Forbidden" }
                limitExceeded:
                  value:
                    error: "Cannot connect to this profile. It exceeds your Pro plan limit of 5 profiles."
                    code: "PROFILE_LIMIT_EXCEEDED"
        '500':
          description: Failed to save Pinterest connection

  /v1/connect/bluesky/credentials:
    post:
      tags: [Connect]
      summary: Connect Bluesky using app password
      description: |
        Connect a Bluesky account using identifier (handle or email) and an app password.
        
        To get your userId for the state parameter, call `GET /v1/users` - the response includes a `currentUserId` field.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [identifier, appPassword, state]
              properties:
                identifier:
                  type: string
                  description: Your Bluesky handle (e.g. user.bsky.social) or email address
                appPassword:
                  type: string
                  description: App password generated from Bluesky Settings > App Passwords
                state:
                  type: string
                  description: |
                    Required state parameter formatted as `{userId}-{profileId}`.
                    - `userId`: Your Late user ID (get from `GET /v1/users` → `currentUserId`)
                    - `profileId`: The profile ID to connect the account to (get from `GET /v1/profiles`)
                  example: "6507a1b2c3d4e5f6a7b8c9d0-6507a1b2c3d4e5f6a7b8c9d1"
                redirectUri:
                  type: string
                  format: uri
                  description: Optional URL to redirect to after successful connection
            example:
              identifier: "yourhandle.bsky.social"
              appPassword: "xxxx-xxxx-xxxx-xxxx"
              state: "6507a1b2c3d4e5f6a7b8c9d0-6507a1b2c3d4e5f6a7b8c9d1"
              redirectUri: "https://yourapp.com/connected"
      responses:
        '200':
          description: Bluesky connected successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  account: { $ref: '#/components/schemas/SocialAccount' }
              example:
                message: "Bluesky connected successfully"
                account:
                  platform: "bluesky"
                  username: "yourhandle.bsky.social"
                  displayName: "Your Name"
                  isActive: true
                redirectUrl: "https://getlate.dev/dashboard/profiles/64f0.../accounts"
        '400': { description: Invalid request - missing fields or invalid state format }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { description: Internal error }

  /v1/connect/telegram:
    get:
      tags: [Connect]
      summary: Generate Telegram access code
      description: |
        Generate a unique access code for connecting a Telegram channel or group.

        **Connection Flow:**
        1. Call this endpoint to get an access code (valid for 15 minutes)
        2. Add the bot (@LateScheduleBot or your configured bot) as an administrator in your Telegram channel/group
        3. Open a private chat with the bot
        4. Send: `{CODE} @yourchannel` (e.g., `LATE-ABC123 @mychannel`)
        5. Poll `PATCH /v1/connect/telegram?code={CODE}` to check connection status

        **Alternative for private channels:** If your channel has no public username, forward any message from the channel to the bot along with the access code.
      parameters:
        - name: profileId
          in: query
          required: true
          schema: { type: string }
          description: The profile ID to connect the Telegram account to
      responses:
        '200':
          description: Access code generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                    description: The access code to send to the Telegram bot
                    example: "LATE-ABC123"
                  expiresAt:
                    type: string
                    format: date-time
                    description: When the code expires
                  expiresIn:
                    type: integer
                    description: Seconds until expiration
                    example: 900
                  botUsername:
                    type: string
                    description: The Telegram bot username to message
                    example: "LateScheduleBot"
                  instructions:
                    type: array
                    items: { type: string }
                    description: Step-by-step connection instructions
              example:
                code: "LATE-ABC123"
                expiresAt: "2024-01-15T12:30:00.000Z"
                expiresIn: 900
                botUsername: "LateScheduleBot"
                instructions:
                  - "1. Add @LateScheduleBot as an administrator in your channel/group"
                  - "2. Open a private chat with @LateScheduleBot"
                  - "3. Send: LATE-ABC123 @yourchannel (replace @yourchannel with your channel username)"
                  - "4. Wait for confirmation - the connection will appear in your dashboard"
                  - "Tip: If your channel has no public username, forward a message from it along with the code"
        '400': { description: Profile ID required or invalid format }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: No access to this profile }
        '404': { description: Profile not found }
        '500': { description: Internal error }
    post:
      tags: [Connect]
      summary: Direct Telegram connection (power users)
      description: |
        Connect a Telegram channel/group directly using the chat ID.

        This is an alternative to the access code flow for power users who know their Telegram chat ID.
        The bot must already be added as an administrator in the channel/group.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [chatId, profileId]
              properties:
                chatId:
                  type: string
                  description: |
                    The Telegram chat ID. Can be:
                    - Numeric ID (e.g., "-1001234567890")
                    - Username with @ prefix (e.g., "@mychannel")
                profileId:
                  type: string
                  description: The profile ID to connect the account to
            example:
              chatId: "-1001234567890"
              profileId: "6507a1b2c3d4e5f6a7b8c9d0"
      responses:
        '200':
          description: Telegram channel connected successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  account:
                    type: object
                    properties:
                      _id: { type: string }
                      platform: { type: string, enum: [telegram] }
                      username: { type: string }
                      displayName: { type: string }
                      isActive: { type: boolean }
                      chatType: { type: string, enum: [channel, group, supergroup, private] }
              example:
                message: "Telegram channel connected successfully"
                account:
                  _id: "64e1f0a9e2b5af0012ab34cd"
                  platform: "telegram"
                  username: "mychannel"
                  displayName: "My Channel"
                  isActive: true
                  chatType: "channel"
        '400': { description: Chat ID required, bot not admin, or cannot access chat }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: No access to this profile }
        '404': { description: Profile not found }
        '500': { description: Internal error }
    patch:
      tags: [Connect]
      summary: Check Telegram connection status
      description: |
        Poll this endpoint to check if a Telegram access code has been used to connect a channel/group.

        **Recommended polling interval:** 3 seconds

        **Status values:**
        - `pending`: Code is valid, waiting for user to complete connection
        - `connected`: Connection successful - channel/group is now linked
        - `expired`: Code has expired, generate a new one
      parameters:
        - name: code
          in: query
          required: true
          schema: { type: string }
          description: The access code to check status for
          example: "LATE-ABC123"
      responses:
        '200':
          description: Connection status
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    title: Pending
                    properties:
                      status: { type: string, enum: [pending] }
                      expiresAt: { type: string, format: date-time }
                      expiresIn: { type: integer, description: Seconds until expiration }
                  - type: object
                    title: Connected
                    properties:
                      status: { type: string, enum: [connected] }
                      chatId: { type: string }
                      chatTitle: { type: string }
                      chatType: { type: string, enum: [channel, group, supergroup] }
                      account:
                        type: object
                        properties:
                          _id: { type: string }
                          platform: { type: string }
                          username: { type: string }
                          displayName: { type: string }
                  - type: object
                    title: Expired
                    properties:
                      status: { type: string, enum: [expired] }
                      message: { type: string }
              examples:
                pending:
                  summary: Waiting for connection
                  value:
                    status: "pending"
                    expiresAt: "2024-01-15T12:30:00.000Z"
                    expiresIn: 542
                connected:
                  summary: Successfully connected
                  value:
                    status: "connected"
                    chatId: "-1001234567890"
                    chatTitle: "My Channel"
                    chatType: "channel"
                    account:
                      _id: "64e1f0a9e2b5af0012ab34cd"
                      platform: "telegram"
                      username: "mychannel"
                      displayName: "My Channel"
                expired:
                  summary: Code expired
                  value:
                    status: "expired"
                    message: "Access code has expired. Please generate a new one."
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Code not found }
        '500': { description: Internal error }

  /v1/accounts/{accountId}/facebook-page:
    put:
      tags: [Connect]
      summary: Update selected Facebook page for a connected account
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [selectedPageId]
              properties:
                selectedPageId: { type: string }
            example:
              selectedPageId: "123456789012345"
      responses:
        '200':
          description: Page updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  account: { $ref: '#/components/schemas/SocialAccount' }
              example:
                message: "Facebook page updated successfully"
                account:
                  _id: "64e1f0a9e2b5af0012ab34cd"
                  platform: "facebook"
                  username: "mybrand"
                  displayName: "My Brand Page"
                  isActive: true
        '400': { description: Page not in available pages }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found }

  /v1/accounts/{accountId}/linkedin-organizations:
    get:
      tags: [Connect]
      summary: Get available LinkedIn organizations for a connected account
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Organizations list
          content:
            application/json:
              schema:
                type: object
                properties:
                  organizations:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        vanityName: { type: string }
                        localizedName: { type: string }
              example:
                organizations:
                  - id: "12345678"
                    name: "Acme Corporation"
                    vanityName: "acme-corp"
                    localizedName: "Acme Corporation"
                  - id: "87654321"
                    name: "Acme Subsidiary"
                    vanityName: "acme-sub"
                    localizedName: "Acme Subsidiary"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found }

  /v1/accounts/{accountId}/linkedin-aggregate-analytics:
    get:
      tags: [Analytics]
      summary: Get aggregate analytics for a LinkedIn personal account
      description: |
        Returns aggregate analytics across ALL posts for a LinkedIn personal account.
        Uses LinkedIn's `memberCreatorPostAnalytics` API with `q=me` finder.
        
        **Important:** This endpoint only works for LinkedIn **personal** accounts. Organization accounts should use the standard `/v1/analytics` endpoint for per-post analytics.
        
        **Required Scope:** `r_member_postAnalytics`
        
        If the connected account doesn't have this scope, you'll receive a 403 error with instructions to reconnect.
        
        **Aggregation Options:**
        - `TOTAL` (default): Returns lifetime totals for all metrics
        - `DAILY`: Returns daily breakdown of metrics over time
        
        **Available Metrics:**
        - `IMPRESSION`: Number of times posts were displayed
        - `MEMBERS_REACHED`: Unique members who saw posts (NOT available with DAILY aggregation)
        - `REACTION`: Total reactions (likes, celebrates, etc.)
        - `COMMENT`: Total comments
        - `RESHARE`: Total reshares/reposts
        
        **Date Range Filtering:**
        Use `startDate` and `endDate` parameters to filter analytics to a specific time period.
        If omitted, returns lifetime analytics.
        
        **LinkedIn API Limitation:** The combination of `MEMBERS_REACHED` + `DAILY` aggregation is not supported by LinkedIn's API.
      parameters:
        - name: accountId
          in: path
          required: true
          description: The ID of the LinkedIn personal account
          schema: { type: string }
        - name: aggregation
          in: query
          required: false
          description: |
            Type of aggregation for the analytics data.
            - `TOTAL` (default): Returns single totals for each metric
            - `DAILY`: Returns daily breakdown of metrics
            
            Note: `MEMBERS_REACHED` metric is not available with `DAILY` aggregation.
          schema:
            type: string
            enum: [TOTAL, DAILY]
            default: TOTAL
        - name: startDate
          in: query
          required: false
          description: |
            Start date for analytics data in YYYY-MM-DD format.
            If provided without endDate, endDate defaults to today.
            If omitted entirely, returns lifetime analytics.
          schema:
            type: string
            format: date
          example: "2024-01-01"
        - name: endDate
          in: query
          required: false
          description: |
            End date for analytics data in YYYY-MM-DD format (exclusive).
            If provided without startDate, startDate defaults to 30 days before endDate.
          schema:
            type: string
            format: date
          example: "2024-01-31"
        - name: metrics
          in: query
          required: false
          description: |
            Comma-separated list of metrics to fetch. If omitted, fetches all available metrics.
            Valid values: IMPRESSION, MEMBERS_REACHED, REACTION, COMMENT, RESHARE
          schema:
            type: string
          example: "IMPRESSION,REACTION,COMMENT"
      responses:
        '200':
          description: Aggregate analytics data
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/LinkedInAggregateAnalyticsTotalResponse'
                  - $ref: '#/components/schemas/LinkedInAggregateAnalyticsDailyResponse'
              examples:
                totalAggregation:
                  summary: TOTAL aggregation (lifetime totals)
                  value:
                    accountId: "64abc123def456"
                    platform: "linkedin"
                    accountType: "personal"
                    username: "John Doe"
                    aggregation: "TOTAL"
                    dateRange: null
                    analytics:
                      impressions: 1250000
                      reach: 450000
                      reactions: 7500
                      comments: 2500
                      shares: 1200
                      engagementRate: 0.90
                    note: "Aggregate analytics across all posts on this LinkedIn personal account (lifetime totals)."
                    lastUpdated: "2025-01-15T10:30:00.000Z"
                totalWithDateRange:
                  summary: TOTAL aggregation with date range
                  value:
                    accountId: "64abc123def456"
                    platform: "linkedin"
                    accountType: "personal"
                    username: "John Doe"
                    aggregation: "TOTAL"
                    dateRange:
                      startDate: "2024-01-01"
                      endDate: "2024-01-31"
                    analytics:
                      impressions: 125000
                      reach: 45000
                      reactions: 750
                      comments: 250
                      shares: 120
                      engagementRate: 0.90
                    note: "Aggregate analytics for the specified date range."
                    lastUpdated: "2025-01-15T10:30:00.000Z"
                dailyAggregation:
                  summary: DAILY aggregation (time series)
                  value:
                    accountId: "64abc123def456"
                    platform: "linkedin"
                    accountType: "personal"
                    username: "John Doe"
                    aggregation: "DAILY"
                    dateRange:
                      startDate: "2024-05-04"
                      endDate: "2024-05-06"
                    analytics:
                      impressions:
                        - date: "2024-05-04"
                          count: 1500
                        - date: "2024-05-05"
                          count: 2300
                      reactions:
                        - date: "2024-05-04"
                          count: 10
                        - date: "2024-05-05"
                          count: 20
                      comments:
                        - date: "2024-05-04"
                          count: 3
                        - date: "2024-05-05"
                          count: 5
                      shares:
                        - date: "2024-05-04"
                          count: 2
                        - date: "2024-05-05"
                          count: 4
                    skippedMetrics:
                      - "MEMBERS_REACHED (not supported with DAILY aggregation)"
                    note: "Daily breakdown of analytics across all posts. MEMBERS_REACHED is not available with DAILY aggregation per LinkedIn API limitations."
                    lastUpdated: "2025-01-15T10:30:00.000Z"
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  code: { type: string }
                  validOptions: { type: array, items: { type: string } }
              examples:
                not_linkedin:
                  summary: Not a LinkedIn account
                  value:
                    error: "This endpoint only supports LinkedIn accounts"
                    code: "invalid_platform"
                organization:
                  summary: Organization account not supported
                  value:
                    error: "Aggregate analytics only available for LinkedIn personal accounts. Organization accounts can use per-post analytics via /v1/analytics."
                    code: "organization_not_supported"
                invalid_aggregation:
                  summary: Invalid aggregation type
                  value:
                    error: "Invalid aggregation type. Must be one of: TOTAL, DAILY"
                    code: "invalid_aggregation"
                    validOptions: ["TOTAL", "DAILY"]
                invalid_date:
                  summary: Invalid date format
                  value:
                    error: "Invalid date format. Use YYYY-MM-DD format."
                    code: "invalid_date_format"
                    example:
                      startDate: "2024-01-01"
                      endDate: "2024-01-31"
                invalid_metrics:
                  summary: Invalid metrics requested
                  value:
                    error: "Invalid metrics: INVALID_METRIC. Valid options: IMPRESSION, MEMBERS_REACHED, REACTION, COMMENT, RESHARE"
                    code: "invalid_metrics"
                    validOptions: ["IMPRESSION", "MEMBERS_REACHED", "REACTION", "COMMENT", "RESHARE"]
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402':
          description: Analytics add-on required
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  code: { type: string }
        '403':
          description: Missing required LinkedIn scope
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  code: { type: string, example: missing_scope }
                  requiredScope: { type: string, example: r_member_postAnalytics }
                  action: { type: string, example: reconnect }
              example:
                error: "Missing r_member_postAnalytics scope. Please reconnect your LinkedIn account to grant analytics permissions."
                code: "missing_scope"
                requiredScope: "r_member_postAnalytics"
                action: "reconnect"
        '404': { description: Account not found }

  /v1/accounts/{accountId}/linkedin-post-analytics:
    get:
      tags: [Analytics]
      summary: Get analytics for a specific LinkedIn post by URN
      description: |
        Returns analytics for a specific LinkedIn post using its URN.
        Works for both personal and organization accounts.
        
        This is useful for fetching analytics of posts that weren't published through Late,
        as long as you have the post URN.
        
        **For Personal Accounts:**
        - Uses `memberCreatorPostAnalytics` API + `memberCreatorVideoAnalytics` for video posts
        - Requires `r_member_postAnalytics` scope
        - Available metrics: impressions, reach, likes, comments, shares, video views (video posts only)
        - **Clicks are NOT available** for personal accounts
        
        **For Organization Accounts:**
        - Uses `organizationalEntityShareStatistics` API + `videoAnalytics` for video posts
        - Requires `r_organization_social` scope
        - Available metrics: impressions, reach, clicks, likes, comments, shares, video views (video posts only), engagement rate
      parameters:
        - name: accountId
          in: path
          required: true
          description: The ID of the LinkedIn account
          schema: { type: string }
        - name: urn
          in: query
          required: true
          description: The LinkedIn post URN
          schema: { type: string }
          example: "urn:li:share:7123456789012345678"
      responses:
        '200':
          description: Post analytics data
          content:
            application/json:
              schema:
                type: object
                properties:
                  accountId: { type: string }
                  platform: { type: string, example: linkedin }
                  accountType: { type: string, enum: [personal, organization] }
                  username: { type: string }
                  postUrn: { type: string }
                  analytics:
                    type: object
                    properties:
                      impressions: { type: integer, description: Times the post was shown }
                      reach: { type: integer, description: Unique members who saw the post }
                      likes: { type: integer, description: Reactions on the post }
                      comments: { type: integer, description: Comments on the post }
                      shares: { type: integer, description: Reshares of the post }
                      clicks: { type: integer, description: Clicks on the post (organization accounts only) }
                      views: { type: integer, description: Video views (video posts only) }
                      engagementRate: { type: number, description: Engagement rate as percentage }
                  lastUpdated: { type: string, format: date-time }
              example:
                accountId: "64abc123def456"
                platform: "linkedin"
                accountType: "personal"
                username: "John Doe"
                postUrn: "urn:li:share:7123456789012345678"
                analytics:
                  impressions: 5420
                  reach: 3200
                  likes: 156
                  comments: 23
                  shares: 12
                  clicks: 0
                  views: 1250
                  engagementRate: 5.17
                lastUpdated: "2025-01-15T10:30:00.000Z"
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  code: { type: string, enum: [missing_urn, invalid_urn, invalid_platform] }
              examples:
                missing_urn:
                  value:
                    error: "Missing required parameter: urn"
                    code: "missing_urn"
                    example: "urn:li:share:7123456789012345678 or urn:li:ugcPost:7123456789012345678"
                invalid_urn:
                  value:
                    error: "Invalid URN format. Must be urn:li:share:ID or urn:li:ugcPost:ID"
                    code: "invalid_urn"
                    providedUrn: "invalid-urn"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402':
          description: Analytics add-on required
        '403':
          description: Missing required LinkedIn scope
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  code: { type: string, example: missing_scope }
                  requiredScope: { type: string }
                  action: { type: string, example: reconnect }
        '404':
          description: Account or post not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  code: { type: string }
              examples:
                account_not_found:
                  value:
                    error: "Account not found"
                post_not_found:
                  value:
                    error: "Post not found. The URN may be invalid or the post may have been deleted."
                    code: "post_not_found"
                    postUrn: "urn:li:share:123"

  /v1/accounts/{accountId}/linkedin-organization:
    put:
      tags: [Connect]
      summary: Switch LinkedIn account type (personal/organization)
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accountType]
              properties:
                accountType: { type: string, enum: [personal, organization] }
                selectedOrganization: { type: object }
            example:
              accountType: "organization"
              selectedOrganization:
                id: "12345678"
                name: "Acme Corporation"
                vanityName: "acme-corp"
      responses:
        '200':
          description: Account updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  account: { $ref: '#/components/schemas/SocialAccount' }
              example:
                message: "LinkedIn account type updated successfully"
                account:
                  _id: "64e1f0a9e2b5af0012ab34cd"
                  platform: "linkedin"
                  username: "acme-corp"
                  displayName: "Acme Corporation"
                  isActive: true
        '400': { description: Invalid request }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found }

  /v1/accounts/{accountId}/linkedin-mentions:
    get:
      tags: [LinkedIn Mentions]
      summary: Resolve a LinkedIn profile or company URL to a URN for @mentions
      description: |
        Converts a LinkedIn profile URL (person) or company page URL (organization) to a URN that can be used to @mention them in posts.

        **Supports both:**
        - **Person mentions:** `linkedin.com/in/username` or just `username`
        - **Organization mentions:** `linkedin.com/company/company-name` or `company/company-name`

        **⚠️ Organization Admin Required for Person Mentions Only:**
        Person mentions require the connected LinkedIn account to have admin access to at least one LinkedIn Organization (Company Page).
        Organization mentions do NOT have this requirement - any LinkedIn account can tag companies.

        **IMPORTANT - Display Name Requirement:**
        For **person mentions** to be clickable, the display name must **exactly match** what appears on their LinkedIn profile.
        - Organization mentions automatically retrieve the company name from LinkedIn API
        - Person mentions require the exact name, so provide the `displayName` parameter

        **Mention Format:**
        Use the returned `mentionFormat` value directly in your post content:
        ```
        Great insights from @[Miquel Palet](urn:li:person:4qj5ox-agD) on this topic!
        Excited to partner with @[Microsoft](urn:li:organization:1035)!
        ```
      parameters:
        - name: accountId
          in: path
          required: true
          description: The LinkedIn account ID
          schema: { type: string }
        - name: url
          in: query
          required: true
          description: |
            LinkedIn profile URL, company URL, or vanity name.
            - Person: `miquelpalet`, `linkedin.com/in/miquelpalet`
            - Organization: `company/microsoft`, `linkedin.com/company/microsoft`
          schema: { type: string }
          examples:
            personVanityName:
              value: "miquelpalet"
              summary: Person - just the vanity name
            personFullUrl:
              value: "https://www.linkedin.com/in/miquelpalet"
              summary: Person - full LinkedIn URL
            orgShortUrl:
              value: "company/microsoft"
              summary: Organization - short format
            orgFullUrl:
              value: "https://www.linkedin.com/company/microsoft"
              summary: Organization - full LinkedIn URL
        - name: displayName
          in: query
          required: false
          description: |
            The exact display name as shown on LinkedIn.
            - **Person mentions:** Required for clickable mentions. If not provided, a name is derived from the vanity URL which may not match exactly.
            - **Organization mentions:** Optional. If not provided, the company name is automatically retrieved from LinkedIn.
          schema: { type: string }
          examples:
            personName:
              value: "Miquel Palet"
              summary: Exact name as shown on LinkedIn profile
            orgName:
              value: "Microsoft"
              summary: Company name (optional for orgs)
      responses:
        '200':
          description: URN resolved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  urn:
                    type: string
                    description: The LinkedIn URN (person or organization)
                    example: "urn:li:person:4qj5ox-agD"
                  type:
                    type: string
                    enum: [person, organization]
                    description: The type of entity (person or organization)
                    example: "person"
                  displayName:
                    type: string
                    description: Display name (provided, from API, or derived from vanity URL)
                    example: "Miquel Palet"
                  mentionFormat:
                    type: string
                    description: Ready-to-use mention format for post content
                    example: "@[Miquel Palet](urn:li:person:4qj5ox-agD)"
                  vanityName:
                    type: string
                    description: The vanity name/slug (only for organization mentions)
                    example: "microsoft"
                  warning:
                    type: string
                    description: Warning about clickable mentions (only present for person mentions if displayName was not provided)
                    example: "For clickable person mentions, provide the displayName parameter with the exact name as shown on their LinkedIn profile."
              examples:
                personWithDisplayName:
                  summary: Person mention with displayName (recommended)
                  value:
                    urn: "urn:li:person:4qj5ox-agD"
                    type: "person"
                    displayName: "Miquel Palet"
                    mentionFormat: "@[Miquel Palet](urn:li:person:4qj5ox-agD)"
                personWithoutDisplayName:
                  summary: Person mention without displayName (may not be clickable)
                  value:
                    urn: "urn:li:person:4qj5ox-agD"
                    type: "person"
                    displayName: "Miquelpalet"
                    mentionFormat: "@[Miquelpalet](urn:li:person:4qj5ox-agD)"
                    warning: "For clickable person mentions, provide the displayName parameter with the exact name as shown on their LinkedIn profile."
                organization:
                  summary: Organization mention
                  value:
                    urn: "urn:li:organization:1035"
                    type: "organization"
                    displayName: "Microsoft"
                    mentionFormat: "@[Microsoft](urn:li:organization:1035)"
                    vanityName: "microsoft"
        '400':
          description: Invalid request or no organization found (for person mentions)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
              examples:
                missingUrl:
                  value: { error: "url parameter is required" }
                noOrgForPersonMention:
                  value: { error: "No organization found. You need to be an admin of a LinkedIn organization to use person mentions. Organization mentions work without this requirement." }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: Person or organization not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
              examples:
                memberNotFound:
                  value: { error: "Member not found. Check the LinkedIn URL is correct." }
                orgNotFound:
                  value: { error: "Organization not found. Check the LinkedIn company URL is correct." }

  /v1/accounts/{accountId}/pinterest-boards:
    get:
      tags: [Connect]
      summary: List Pinterest boards for a connected account
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Boards list
          content:
            application/json:
              schema:
                type: object
                properties:
                  boards:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        description: { type: string }
                        privacy: { type: string }
              example:
                boards:
                  - id: "123456789012345678"
                    name: "Marketing Ideas"
                    description: "Collection of marketing inspiration"
                    privacy: "PUBLIC"
                  - id: "234567890123456789"
                    name: "Product Photos"
                    description: "Product photography"
                    privacy: "PUBLIC"
        '400': { description: Not a Pinterest account }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found }
    put:
      tags: [Connect]
      summary: Set default Pinterest board on the connection
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [defaultBoardId]
              properties:
                defaultBoardId: { type: string }
                defaultBoardName: { type: string }
            example:
              defaultBoardId: "123456789012345678"
              defaultBoardName: "Marketing Ideas"
      responses:
        '200':
          description: Default board set
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  account: { $ref: '#/components/schemas/SocialAccount' }
              example:
                message: "Default Pinterest board updated successfully"
                account:
                  _id: "64e1f0a9e2b5af0012ab34cd"
                  platform: "pinterest"
                  username: "mybrand"
                  displayName: "My Brand"
                  isActive: true
        '400': { description: Invalid request }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found }

  /v1/accounts/{accountId}/reddit-subreddits:
    get:
      tags: [Connect]
      summary: List Reddit subreddits for a connected account
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Subreddits list
          content:
            application/json:
              schema:
                type: object
                properties:
                  subreddits:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        displayName: { type: string }
                        subscribers: { type: integer }
                        public_description: { type: string }
              example:
                subreddits:
                  - name: "marketing"
                    displayName: "r/marketing"
                    subscribers: 456000
                    public_description: "A place to discuss marketing strategies"
                  - name: "socialmedia"
                    displayName: "r/socialmedia"
                    subscribers: 234000
                    public_description: "Social media marketing discussions"
        '400': { description: Not a Reddit account }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found }

    put:
      tags: [Connect]
      summary: Set default subreddit on the connection
      parameters:
        - name: accountId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [defaultSubreddit]
              properties:
                defaultSubreddit: { type: string }
            example:
              defaultSubreddit: "marketing"
      responses:
        '200':
          description: Default subreddit set
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  account: { $ref: '#/components/schemas/SocialAccount' }
              example:
                message: "Default subreddit updated successfully"
                account:
                  _id: "64e1f0a9e2b5af0012ab34cd"
                  platform: "reddit"
                  username: "u/myuser"
                  displayName: "My Reddit Account"
                  isActive: true
        '400': { description: Invalid request }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found }




  /v1/queue/slots:
    get:
      tags: [Queue]
      summary: Get queue schedules for a profile
      description: |
        Retrieve queue schedules for a profile. Each profile can have multiple queues.
        - Without `all=true`: Returns the default queue (or specific queue if queueId provided)
        - With `all=true`: Returns all queues for the profile
      parameters:
        - name: profileId
          in: query
          required: true
          schema: { type: string }
          description: Profile ID to get queues for
        - name: queueId
          in: query
          required: false
          schema: { type: string }
          description: Specific queue ID to retrieve (optional)
        - name: all
          in: query
          required: false
          schema: { type: string, enum: ['true'] }
          description: Set to 'true' to list all queues for the profile
      responses:
        '200':
          description: Queue schedule(s) retrieved
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: Single queue response (default behavior)
                    properties:
                      exists: { type: boolean }
                      schedule:
                        $ref: '#/components/schemas/QueueSchedule'
                      nextSlots:
                        type: array
                        items: { type: string, format: date-time }
                  - type: object
                    description: All queues response (when all=true)
                    properties:
                      queues:
                        type: array
                        items:
                          $ref: '#/components/schemas/QueueSchedule'
                      count: { type: integer }
              examples:
                singleQueue:
                  summary: Single queue response
                  value:
                    exists: true
                    schedule:
                      _id: "64f0a1b2c3d4e5f6a7b8c9d1"
                      profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
                      name: "Morning Posts"
                      timezone: "America/New_York"
                      slots:
                        - dayOfWeek: 1
                          time: "09:00"
                        - dayOfWeek: 3
                          time: "09:00"
                        - dayOfWeek: 5
                          time: "10:00"
                      active: true
                      isDefault: true
                    nextSlots:
                      - "2024-11-04T09:00:00-05:00"
                      - "2024-11-06T09:00:00-05:00"
                allQueues:
                  summary: All queues response (all=true)
                  value:
                    queues:
                      - _id: "64f0a1b2c3d4e5f6a7b8c9d1"
                        name: "Morning Posts"
                        isDefault: true
                        timezone: "America/New_York"
                        slots: [{ dayOfWeek: 1, time: "09:00" }]
                        active: true
                      - _id: "64f0a1b2c3d4e5f6a7b8c9d2"
                        name: "Evening Content"
                        isDefault: false
                        timezone: "America/New_York"
                        slots: [{ dayOfWeek: 1, time: "18:00" }]
                        active: true
                    count: 2
        '400': { description: Missing profileId }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Profile not found }
    post:
      tags: [Queue]
      summary: Create a new queue for a profile
      description: |
        Create an additional queue for a profile. The first queue created becomes the default.
        Subsequent queues are non-default unless explicitly set.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [profileId, name, timezone, slots]
              properties:
                profileId: { type: string, description: Profile ID }
                name: { type: string, description: "Queue name (e.g., Evening Posts)" }
                timezone: { type: string, description: IANA timezone }
                slots:
                  type: array
                  items:
                    $ref: '#/components/schemas/QueueSlot'
                active: { type: boolean, default: true }
            example:
              profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
              name: "Evening Posts"
              timezone: "America/New_York"
              slots:
                - dayOfWeek: 1
                  time: "18:00"
                - dayOfWeek: 3
                  time: "18:00"
                - dayOfWeek: 5
                  time: "18:00"
              active: true
      responses:
        '201':
          description: Queue created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  schedule:
                    $ref: '#/components/schemas/QueueSchedule'
                  nextSlots:
                    type: array
                    items: { type: string, format: date-time }
        '400': { description: Invalid request or validation error }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Profile not found }
    put:
      tags: [Queue]
      summary: Create or update a queue schedule
      description: |
        Create a new queue or update an existing one.
        - Without queueId: Creates or updates the default queue
        - With queueId: Updates the specific queue
        - With setAsDefault=true: Makes this queue the default for the profile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [profileId, timezone, slots]
              properties:
                profileId: { type: string }
                queueId: { type: string, description: Queue ID to update (optional) }
                name: { type: string, description: Queue name }
                timezone: { type: string }
                slots:
                  type: array
                  items:
                    $ref: '#/components/schemas/QueueSlot'
                active: { type: boolean, default: true }
                setAsDefault: { type: boolean, description: Make this queue the default }
                reshuffleExisting:
                  type: boolean
                  default: false
                  description: Whether to reschedule existing queued posts to match new slots
            example:
              profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
              queueId: "64f0a1b2c3d4e5f6a7b8c9d1"
              name: "Morning Posts"
              timezone: "America/New_York"
              slots:
                - dayOfWeek: 1
                  time: "09:00"
                - dayOfWeek: 3
                  time: "09:00"
                - dayOfWeek: 5
                  time: "10:00"
              active: true
              setAsDefault: false
      responses:
        '200':
          description: Queue schedule updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  schedule:
                    $ref: '#/components/schemas/QueueSchedule'
                  nextSlots:
                    type: array
                    items: { type: string, format: date-time }
                  reshuffledCount: { type: integer }
              example:
                success: true
                schedule:
                  _id: "64f0a1b2c3d4e5f6a7b8c9d1"
                  profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
                  name: "Morning Posts"
                  timezone: "America/New_York"
                  slots:
                    - dayOfWeek: 1
                      time: "09:00"
                    - dayOfWeek: 3
                      time: "09:00"
                    - dayOfWeek: 5
                      time: "10:00"
                  active: true
                  isDefault: true
                nextSlots:
                  - "2024-11-04T09:00:00-05:00"
                  - "2024-11-06T09:00:00-05:00"
                reshuffledCount: 0
        '400': { description: Invalid request }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Profile not found }
    delete:
      tags: [Queue]
      summary: Delete a queue schedule
      description: |
        Delete a queue from a profile. Requires queueId to specify which queue to delete.
        If deleting the default queue, another queue will be promoted to default.
      parameters:
        - name: profileId
          in: query
          required: true
          schema: { type: string }
        - name: queueId
          in: query
          required: true
          schema: { type: string }
          description: Queue ID to delete
      responses:
        '200':
          description: Queue schedule deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  deleted: { type: boolean }
              example:
                success: true
                deleted: true
        '400': { description: Missing profileId or queueId }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /v1/queue/preview:
    get:
      tags: [Queue]
      summary: Preview upcoming queue slots for a profile
      parameters:
        - name: profileId
          in: query
          required: true
          schema: { type: string }
        - name: count
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        '200':
          description: Queue slots preview
          content:
            application/json:
              schema:
                type: object
                properties:
                  profileId: { type: string }
                  count: { type: integer }
                  slots:
                    type: array
                    items: { type: string, format: date-time }
              example:
                profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
                count: 10
                slots:
                  - "2024-11-04T09:00:00-05:00"
                  - "2024-11-04T14:00:00-05:00"
                  - "2024-11-06T09:00:00-05:00"
                  - "2024-11-08T10:00:00-05:00"
                  - "2024-11-11T09:00:00-05:00"
                  - "2024-11-11T14:00:00-05:00"
                  - "2024-11-13T09:00:00-05:00"
                  - "2024-11-15T10:00:00-05:00"
                  - "2024-11-18T09:00:00-05:00"
                  - "2024-11-18T14:00:00-05:00"
        '400': { description: Invalid parameters }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Profile or queue schedule not found }
  /v1/queue/next-slot:
    get:
      tags: [Queue]
      summary: Get the next available queue slot for a profile
      description: |
        Returns the next available posting slot, taking into account already scheduled posts
        to avoid conflicts. Useful for scheduling posts via queue without manual time selection.

        If no queueId is specified, uses the profile's default queue.
      parameters:
        - name: profileId
          in: query
          required: true
          schema: { type: string }
        - name: queueId
          in: query
          required: false
          schema: { type: string }
          description: Specific queue ID (optional, defaults to profile's default queue)
      responses:
        '200':
          description: Next available slot
          content:
            application/json:
              schema:
                type: object
                properties:
                  profileId: { type: string }
                  nextSlot: { type: string, format: date-time }
                  timezone: { type: string }
                  queueId: { type: string, description: Queue ID this slot belongs to }
                  queueName: { type: string, description: Queue name }
              example:
                profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
                nextSlot: "2024-11-04T09:00:00-05:00"
                timezone: "America/New_York"
                queueId: "64f0a1b2c3d4e5f6a7b8c9d1"
                queueName: "Morning Posts"
        '400':
          description: Invalid parameters or inactive queue
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: "Profile or queue schedule not found, or no available slots"
  /v1/profiles/{profileId}/clone-connection:
    post:
      tags: [Connect]
      summary: Clone an existing connection to this profile with optional targeting
      description: |
        Reuse an existing OAuth connection from another profile in this profile, 
        optionally targeting different pages (Facebook) or organizations (LinkedIn).
        Perfect for managing multiple brands with the same underlying social media account.
      parameters:
        - name: profileId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sourceAccountId]
              properties:
                sourceAccountId:
                  type: string
                  description: ID of existing connection to clone
                targetPageId:
                  type: string
                  description: Facebook page ID to target (optional, for Facebook)
                targetPageName:
                  type: string
                  description: Facebook page name (optional, for Facebook)
                targetPageAccessToken:
                  type: string
                  description: Facebook page access token (optional, for Facebook)
                targetOrganizationId:
                  type: string
                  description: LinkedIn organization ID (optional, for LinkedIn)
                targetOrganizationUrn:
                  type: string
                  description: LinkedIn organization URN (optional, for LinkedIn)
                targetOrganizationName:
                  type: string
                  description: LinkedIn organization name (optional, for LinkedIn)
                targetAccountType:
                  type: string
                  enum: [personal, organization]
                  description: LinkedIn account type (optional, for LinkedIn)
            examples:
              cloneTwitter:
                summary: Clone a Twitter connection
                value:
                  sourceAccountId: "64e1f0a9e2b5af0012ab34cd"
              cloneFacebookWithPage:
                summary: Clone Facebook with different page
                value:
                  sourceAccountId: "64e1f0a9e2b5af0012ab34cd"
                  targetPageId: "987654321098765"
                  targetPageName: "My Other Brand"
              cloneLinkedInOrganization:
                summary: Clone LinkedIn with organization
                value:
                  sourceAccountId: "64e1f0a9e2b5af0012ab34cd"
                  targetOrganizationId: "87654321"
                  targetOrganizationUrn: "urn:li:organization:87654321"
                  targetOrganizationName: "Acme Subsidiary"
                  targetAccountType: "organization"
      responses:
        '200':
          description: Connection cloned successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  connection: { $ref: '#/components/schemas/SocialAccount' }
              example:
                message: "Connection cloned successfully"
                connection:
                  _id: "64e1f0a9e2b5af0012ab34ce"
                  platform: "twitter"
                  profileId: "64f0a1b2c3d4e5f6a7b8c9d1"
                  username: "@acmecorp"
                  displayName: "Acme Corporation"
                  isActive: true
        '400': { description: Invalid request or source account }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Source account not found }
        '500': { description: Failed to clone connection }

  # ============================================
  # Webhooks API (Multi-Webhook System)
  # ============================================
  /v1/webhooks/settings:
    get:
      tags: [Webhooks]
      summary: List all webhooks
      description: Retrieve all configured webhooks for the authenticated user. Supports up to 10 webhooks per user.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Webhooks retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Webhook'
              example:
                webhooks:
                  - _id: "507f1f77bcf86cd799439011"
                    name: "My Production Webhook"
                    url: "https://example.com/webhook"
                    events: ["post.published", "post.failed"]
                    isActive: true
                    lastFiredAt: "2024-01-15T10:30:00Z"
                    failureCount: 0
                  - _id: "507f1f77bcf86cd799439012"
                    name: "Slack Notifications"
                    url: "https://hooks.slack.com/services/xxx"
                    events: ["post.failed", "account.disconnected"]
                    isActive: true
                    failureCount: 0
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Webhooks]
      summary: Create a new webhook
      description: |
        Create a new webhook configuration. Maximum 10 webhooks per user.

        **Note:** Webhooks are automatically disabled after 10 consecutive delivery failures.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Webhook name (max 50 characters)
                  maxLength: 50
                url:
                  type: string
                  format: uri
                  description: Webhook endpoint URL (must be HTTPS in production)
                secret:
                  type: string
                  description: Secret key for HMAC-SHA256 signature verification
                events:
                  type: array
                  items:
                    type: string
                    enum: [post.scheduled, post.published, post.failed, post.partial, account.disconnected]
                  description: Events to subscribe to
                isActive:
                  type: boolean
                  description: Enable or disable webhook delivery
                customHeaders:
                  type: object
                  additionalProperties:
                    type: string
                  description: Custom headers to include in webhook requests
            examples:
              createWebhook:
                summary: Create webhook with all events
                value:
                  name: "My Production Webhook"
                  url: "https://example.com/webhook"
                  secret: "your-secret-key"
                  events: ["post.scheduled", "post.published", "post.failed", "post.partial", "account.disconnected"]
                  isActive: true
      responses:
        '200':
          description: Webhook created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  webhook:
                    $ref: '#/components/schemas/Webhook'
        '400': { description: Validation error or maximum webhooks reached }
        '401': { $ref: '#/components/responses/Unauthorized' }
    put:
      tags: [Webhooks]
      summary: Update a webhook
      description: |
        Update an existing webhook configuration. All fields except `_id` are optional - only provided fields will be updated.

        **Note:** Webhooks are automatically disabled after 10 consecutive delivery failures.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - _id
              properties:
                _id:
                  type: string
                  description: Webhook ID to update (required)
                name:
                  type: string
                  description: Webhook name (max 50 characters)
                  maxLength: 50
                url:
                  type: string
                  format: uri
                  description: Webhook endpoint URL (must be HTTPS in production)
                secret:
                  type: string
                  description: Secret key for HMAC-SHA256 signature verification
                events:
                  type: array
                  items:
                    type: string
                    enum: [post.scheduled, post.published, post.failed, post.partial, account.disconnected]
                  description: Events to subscribe to
                isActive:
                  type: boolean
                  description: Enable or disable webhook delivery
                customHeaders:
                  type: object
                  additionalProperties:
                    type: string
                  description: Custom headers to include in webhook requests
            examples:
              updateWebhook:
                summary: Update webhook URL and events
                value:
                  _id: "507f1f77bcf86cd799439011"
                  url: "https://new-example.com/webhook"
                  events: ["post.published", "post.failed"]
              toggleWebhook:
                summary: Enable/disable webhook
                value:
                  _id: "507f1f77bcf86cd799439011"
                  isActive: false
      responses:
        '200':
          description: Webhook updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  webhook:
                    $ref: '#/components/schemas/Webhook'
        '400': { description: Validation error or missing webhook ID }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Webhook not found }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      description: Permanently delete a webhook configuration.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: query
          required: true
          description: Webhook ID to delete
          schema:
            type: string
      responses:
        '200':
          description: Webhook deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
        '400': { description: Webhook ID required }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/webhooks/test:
    post:
      tags: [Webhooks]
      summary: Send test webhook
      description: |
        Send a test webhook to verify your endpoint is configured correctly.
        The test payload includes `event: "webhook.test"` to distinguish it from real events.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - webhookId
              properties:
                webhookId:
                  type: string
                  description: ID of the webhook to test
            example:
              webhookId: "507f1f77bcf86cd799439011"
      responses:
        '200':
          description: Test webhook sent successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  message: { type: string }
              example:
                success: true
                message: "Test webhook sent successfully"
        '400': { description: Webhook ID required }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500':
          description: Test webhook failed to deliver
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  message: { type: string }
              example:
                success: false
                message: "Test webhook failed"

  /v1/webhooks/logs:
    get:
      tags: [Webhooks]
      summary: Get webhook delivery logs
      description: |
        Retrieve webhook delivery history. Logs are automatically deleted after 7 days.
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          description: Maximum number of logs to return (max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: status
          in: query
          description: Filter by delivery status
          schema:
            type: string
            enum: [success, failed]
        - name: event
          in: query
          description: Filter by event type
          schema:
            type: string
            enum: [post.scheduled, post.published, post.failed, post.partial, account.disconnected, webhook.test]
        - name: webhookId
          in: query
          description: Filter by webhook ID
          schema:
            type: string
      responses:
        '200':
          description: Webhook logs retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  logs:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookLog'
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/logs:
    get:
      tags: [Logs]
      summary: Get publishing logs
      description: |
        Retrieve publishing logs for all posts. Logs show detailed information about each
        publishing attempt including API requests, responses, and timing data.

        **Filtering:**
        - Filter by status (success, failed, pending, skipped)
        - Filter by platform (instagram, twitter, linkedin, etc.)
        - Filter by action (publish, retry, rate_limit_pause, etc.)

        **Retention:** Logs are automatically deleted after 7 days.
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          description: Filter by log status
          schema:
            type: string
            enum: [success, failed, pending, skipped, all]
        - name: platform
          in: query
          description: Filter by platform
          schema:
            type: string
            enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, all]
        - name: action
          in: query
          description: Filter by action type
          schema:
            type: string
            enum: [publish, retry, media_upload, rate_limit_pause, token_refresh, cancelled, all]
        - name: days
          in: query
          description: Number of days to look back (max 7)
          schema:
            type: integer
            minimum: 1
            maximum: 7
            default: 7
        - name: limit
          in: query
          description: Maximum number of logs to return (max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: skip
          in: query
          description: Number of logs to skip (for pagination)
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Publishing logs retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  logs:
                    type: array
                    items:
                      $ref: '#/components/schemas/PostLog'
                  pagination:
                    type: object
                    properties:
                      total:
                        type: integer
                        description: Total number of logs matching the query
                      limit:
                        type: integer
                      skip:
                        type: integer
                      pages:
                        type: integer
                        description: Total number of pages
                      hasMore:
                        type: boolean
              example:
                logs:
                  - _id: "675f1c0a9e2b5af0012ab34cd"
                    postId:
                      _id: "65f1c0a9e2b5af0012ab34cd"
                      content: "Check out our new feature!"
                      status: "published"
                    userId: "64e1f0a9e2b5af0012ab34de"
                    platform: "instagram"
                    accountId: "64e1f0a9e2b5af0012ab34ef"
                    accountUsername: "@acmecorp"
                    action: "publish"
                    status: "success"
                    statusCode: 200
                    endpoint: "graph.facebook.com/me/media_publish"
                    request:
                      contentPreview: "Check out our new feature!"
                      mediaCount: 1
                      mediaTypes: ["image"]
                      mediaUrls: ["https://storage.getlate.dev/abc123.jpg"]
                    response:
                      platformPostId: "17895695668004550"
                      platformPostUrl: "https://www.instagram.com/p/ABC123/"
                    durationMs: 2340
                    attemptNumber: 1
                    createdAt: "2024-11-01T10:00:05Z"
                pagination:
                  total: 150
                  limit: 50
                  skip: 0
                  pages: 3
                  hasMore: true
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/logs/{logId}:
    get:
      tags: [Logs]
      summary: Get a single log entry
      description: |
        Retrieve detailed information about a specific log entry, including full request
        and response bodies for debugging.
      security:
        - bearerAuth: []
      parameters:
        - name: logId
          in: path
          required: true
          description: The log entry ID
          schema:
            type: string
      responses:
        '200':
          description: Log entry retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  log:
                    $ref: '#/components/schemas/PostLogDetail'
              example:
                log:
                  _id: "675f1c0a9e2b5af0012ab34cd"
                  postId:
                    _id: "65f1c0a9e2b5af0012ab34cd"
                    content: "Check out our new feature!"
                    status: "published"
                    scheduledFor: "2024-11-01T10:00:00Z"
                    mediaItems:
                      - type: "image"
                        url: "https://storage.getlate.dev/abc123.jpg"
                  userId: "64e1f0a9e2b5af0012ab34de"
                  platform: "instagram"
                  accountId:
                    _id: "64e1f0a9e2b5af0012ab34ef"
                    platform: "instagram"
                    username: "acmecorp"
                    displayName: "Acme Corporation"
                  accountUsername: "acmecorp"
                  action: "publish"
                  status: "success"
                  statusCode: 200
                  endpoint: "graph.facebook.com/me/media_publish"
                  request:
                    contentPreview: "Check out our new feature!"
                    mediaCount: 1
                    mediaTypes: ["image"]
                    mediaUrls: ["https://storage.getlate.dev/abc123.jpg"]
                    rawBody: '{"image_url":"https://storage.getlate.dev/abc123.jpg","caption":"Check out our new feature!"}'
                  response:
                    platformPostId: "17895695668004550"
                    platformPostUrl: "https://www.instagram.com/p/ABC123/"
                    rawBody: '{"id":"17895695668004550"}'
                  durationMs: 2340
                  attemptNumber: 1
                  createdAt: "2024-11-01T10:00:05Z"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Forbidden - not authorized to view this log
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/posts/{postId}/logs:
    get:
      tags: [Logs]
      summary: Get logs for a specific post
      description: |
        Retrieve all publishing logs for a specific post. Shows the complete history
        of publishing attempts for that post across all platforms.
      security:
        - bearerAuth: []
      parameters:
        - name: postId
          in: path
          required: true
          description: The post ID
          schema:
            type: string
        - name: limit
          in: query
          description: Maximum number of logs to return (max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: Post logs retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  logs:
                    type: array
                    items:
                      $ref: '#/components/schemas/PostLog'
                  count:
                    type: integer
                    description: Number of logs returned
                  postId:
                    type: string
              example:
                logs:
                  - _id: "675f1c0a9e2b5af0012ab34cd"
                    postId: "65f1c0a9e2b5af0012ab34cd"
                    userId: "64e1f0a9e2b5af0012ab34de"
                    platform: "instagram"
                    accountUsername: "@acmecorp"
                    action: "publish"
                    status: "success"
                    statusCode: 200
                    durationMs: 2340
                    createdAt: "2024-11-01T10:00:05Z"
                  - _id: "675f1c0a9e2b5af0012ab34ce"
                    postId: "65f1c0a9e2b5af0012ab34cd"
                    userId: "64e1f0a9e2b5af0012ab34de"
                    platform: "twitter"
                    accountUsername: "@acme"
                    action: "publish"
                    status: "failed"
                    statusCode: 429
                    response:
                      errorMessage: "Rate limit exceeded"
                      errorCode: "RATE_LIMITED"
                    durationMs: 150
                    attemptNumber: 1
                    createdAt: "2024-11-01T10:00:03Z"
                count: 2
                postId: "65f1c0a9e2b5af0012ab34cd"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Forbidden - not authorized to view this post
        '404': { $ref: '#/components/responses/NotFound' }








