openapi: 3.0.3

info:
  title: Nuelink Public API
  version: 1.2.0
  description: |
    The Nuelink Public API provides token-authenticated access to profiles, brands,
    media, RSS/feed automations, collections, channels, posts, the weekly
    queue schedule, and published-post results.

    ## Media
    Media is never sent as base64. Upload a file with `POST /brands/{brand_id}/media`
    (`multipart/form-data`) and reference the returned `id`, or pass a public
    `url` in a post's `media` item and Nuelink downloads it. Each post media item
    takes exactly one of `id` or `url`.

    ## Sensitive actions
    Destructive operations (currently `DELETE /brands/{brand_id}/posts/{post_id}`)
    are refused with HTTP 403 unless the account owner enabled "Allow AI to
    perform sensitive actions" in the dashboard API settings. These operations
    carry the `x-mcp-annotations.destructiveHint: true` extension so MCP clients
    can ask for confirmation.

    ## Authentication
    Send an API token as `Authorization: Bearer <token>`. For compatibility, the
    same token may be sent as the `api_key` query parameter, but the header is
    strongly recommended because URLs are commonly retained in logs and browser
    history. All requests and responses use JSON except media uploads, which use
    `multipart/form-data`.

    ## Errors
    Request validation failures return HTTP 422 with Laravel's validation error
    object. Domain errors use the same `message` and `errors` fields plus
    `status: error`. Unknown server failures are normalized to a JSON HTTP 500
    response after authentication.

    ## Rate limits
    The API route group is limited to 60 requests per minute per client IP.
    Resource endpoints have an additional public-API limit of 30 requests per
    minute. `/auth` and `/me` are exempt from the additional 30-request limit.
    Rate-limited resource responses expose `X-RateLimit-Limit`,
    `X-RateLimit-Remaining`, and `X-RateLimit-Reset`; the reset value is the
    number of seconds until another request is available. When the route-level
    limiter rejects a request instead, `X-RateLimit-Limit` is `60` and
    `X-RateLimit-Reset` is a Unix timestamp. A rejected request returns HTTP 429
    and may also include `Retry-After` from the route-level limiter.
  contact:
    name: Nuelink Support
    url: https://nuelink.com/contact
  license:
    name: Proprietary
    url: https://nuelink.com/terms

servers:
  - url: https://app.nuelink.com/api/public/v1
    description: Production

security:
  - bearerAuth: []
  - apiKeyQuery: []

tags:
  - name: Profile
    description: Validate a token and retrieve its user profile.
  - name: Brands
    description: Brands accessible to the authenticated user.
  - name: Media
    description: List and upload brand media.
  - name: Automations
    description: List and create feed automations.
  - name: Collections
    description: List and create content collections and queue slots.
  - name: Channels
    description: List social channels connected to a brand.
  - name: Posts
    description: List, create, reschedule, re-queue, draft, and delete posts.
  - name: Published Posts
    description: Posts already published to channels, with their engagement results.
  - name: Schedule
    description: The weekly queue schedule of a brand.

paths:
  /auth:
    get:
      tags: [Profile]
      summary: Validate an API token
      description: |
        Alias of `/me`. Returns the profile associated with the supplied token.
        This operation is subject only to the route-level limit of 60 requests
        per minute per client IP.
      operationId: validateApiToken
      responses:
        '200':
          $ref: '#/components/responses/ProfileSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RouteRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /me:
    get:
      tags: [Profile]
      summary: Get the authenticated profile
      description: |
        Returns the user represented by the API token. This operation is subject
        only to the route-level limit of 60 requests per minute per client IP.
      operationId: getCurrentProfile
      responses:
        '200':
          $ref: '#/components/responses/ProfileSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RouteRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands:
    get:
      tags: [Brands]
      summary: List accessible brands
      description: Returns active brand memberships, newest brand first.
      operationId: listBrands
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Brands retrieved successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandListResponse'
              example:
                status: success
                data:
                  - id: 42
                    title: Acme Marketing
                    description: Social channels for the Acme team
                    timezone: Africa/Casablanca
                    queueStatus: ACTIVE
                    createdAt: '2026-09-15T10:20:30.000000Z'
                    updatedAt: '2026-10-01T14:05:00.000000Z'
                pagination:
                  currentPage: 1
                  perPage: 25
                  total: 1
                  lastPage: 1
                  nextPageUrl: null
                  prevPageUrl: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/media:
    parameters:
      - $ref: '#/components/parameters/BrandId'
    get:
      tags: [Media]
      summary: List brand media
      description: |
        Returns visible media from supported Nuelink sources, newest first.
        `APPLICATION` includes all records marked as documents; `DOCUMENT`
        restricts those records to the supported office/document MIME types.
      operationId: listBrandMedia
      parameters:
        - $ref: '#/components/parameters/MediaType'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Media retrieved successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaListResponse'
              example:
                status: success
                data:
                  - id: 9b8c7d6e5f4a.jpg
                    name: product-launch.jpg
                    type: image/jpeg
                    size: 248512
                    createdAt: '2026-10-01T11:00:00.000000Z'
                    updatedAt: '2026-10-01T11:00:00.000000Z'
                pagination:
                  currentPage: 1
                  perPage: 25
                  total: 1
                  lastPage: 1
                  nextPageUrl: null
                  prevPageUrl: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      tags: [Media]
      summary: Upload brand media
      description: |
        Uploads one JPEG, PNG, BMP, MP4, MOV, or PDF file as a multipart file
        (base64 payloads are not supported). Use the returned `id` in a post's
        `media` item, or skip uploading and pass a public `url` when creating a
        post. The controller allows
        at most 100 MiB (104,857,600 bytes); infrastructure or PHP upload limits
        may reject a request earlier.
      operationId: uploadBrandMedia
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [media]
              properties:
                media:
                  type: string
                  format: binary
                  description: JPEG, PNG, BMP, MP4, MOV, or PDF; maximum 100 MiB.
            encoding:
              media:
                contentType: image/jpeg, image/png, image/bmp, video/mp4, video/quicktime, application/pdf
      responses:
        '201':
          description: Media uploaded successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaUploadResponse'
              example:
                status: success
                data:
                  id: 9b8c7d6e5f4a.jpg
                  type: image/jpeg
                  size: 248512
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/automations:
    parameters:
      - $ref: '#/components/parameters/BrandId'
    get:
      tags: [Automations]
      summary: List feed automations
      description: Returns only `FEED` automations for the brand, newest first.
      operationId: listBrandAutomations
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Automations retrieved successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationListResponse'
              example:
                status: success
                data:
                  - id: 315
                    name: Company blog
                    collectionId: 77
                    status: ACTIVE
                    type: RSS
                    feedUrl: https://example.com/feed.xml
                    runCount: 18
                    importAsType: LINK
                    lastRun: 1791190800
                    title: '{{title}}'
                    caption: '{{description}}\n\n{{link}}'
                    createdAt: '2026-09-20T09:00:00.000000Z'
                    updatedAt: '2026-10-05T08:00:00.000000Z'
                pagination:
                  currentPage: 1
                  perPage: 25
                  total: 1
                  lastPage: 1
                  nextPageUrl: null
                  prevPageUrl: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandMembershipNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      tags: [Automations]
      summary: Create a feed automation
      description: |
        Creates an RSS/feed importer for a collection in the selected brand. The
        feed URL is fetched and parsed before creation. When `loadOldPosts` is
        true, importing is queued asynchronously; otherwise only future feed
        items are considered. `title` and `caption` templates support the placeholders understood
        by the selected feed type.
      operationId: createBrandAutomation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutomationCreateRequest'
            examples:
              rssFeed:
                summary: Import a company RSS feed
                value:
                  name: Company blog
                  collectionId: 77
                  type: RSS
                  feedUrl: https://example.com/feed.xml
                  importAsType: LINK
                  title: '{{title}}'
                  caption: '{{description}}\n\n{{link}}'
                  loadOldPosts: false
                  addPostsAsDraft: true
                  refreshRate: 6
              youtubeFeed:
                summary: Import a YouTube feed as video posts
                value:
                  name: Product videos
                  collectionId: 77
                  type: YOUTUBE
                  feedUrl: https://www.youtube.com/feeds/videos.xml?channel_id=UC123456789
                  importAsType: VIDEO
                  loadOldPosts: true
                  addPostsAsDraft: false
                  refreshRate: 12
      responses:
        '201':
          description: Automation created successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationCreateResponse'
              example:
                status: success
                data:
                  id: 315
                  name: Company blog
                  collectionId: 77
                  status: ACTIVE
                  type: RSS
                  feedUrl: https://example.com/feed.xml
                  runCount: 0
                  importAsType: LINK
                  lastRun: 0
                  title: '{{title}}'
                  caption: '{{description}}\n\n{{link}}'
                  createdAt: '2026-10-05T12:00:00.000000Z'
                  updatedAt: '2026-10-05T12:00:00.000000Z'
        '400':
          description: The feed could not be fetched or parsed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: error
                message: Failed to fetch or parse the feed. Please check the URL and try again.
                errors:
                  feedUrl: Failed to fetch or parse the feed. Please check the URL and try again.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PlanLimitReached'
        '404':
          $ref: '#/components/responses/ResourceNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/collections:
    parameters:
      - $ref: '#/components/parameters/BrandId'
    get:
      tags: [Collections]
      summary: List collections
      description: |
        Returns collections newest first. Each collection includes its `status`
        (`ACTIVE` or `PAUSED`), evergreen settings, weekly queue times (in the
        brand `timezone`, ordered Monday to Sunday), post counts, and its accessible
        active or inactive channels; Pinterest account-container records and
        unavailable channels are omitted.
      operationId: listBrandCollections
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Collections retrieved successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectionListResponse'
              example:
                status: success
                data:
                  - id: 77
                    title: Evergreen content
                    description: Reusable product tips
                    status: ACTIVE
                    evergreen:
                      enabled: true
                      maxRepublish: 2
                    maxRepublish: 2
                    timezone: Africa/Casablanca
                    createdAt: '2026-09-15T10:20:30.000000Z'
                    updatedAt: '2026-10-01T14:05:00.000000Z'
                    channels:
                      - id: 501
                        name: Acme on LinkedIn
                        status: ACTIVE
                        type: LinkedIn
                    queues:
                      - id: 910
                        day: MON
                        time: '09:30'
                        date: MON 09:30
                        status: PENDING
                    postsCount:
                      queued: 12
                      scheduled: 1
                      drafts: 3
                      published: 40
                pagination:
                  currentPage: 1
                  perPage: 25
                  total: 1
                  lastPage: 1
                  nextPageUrl: null
                  prevPageUrl: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandMembershipNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      tags: [Collections]
      summary: Create a collection
      description: |
        Creates a collection, optionally attaches brand channels, and creates
        weekly queue slots in the brand timezone. Queue slots use `D H:i` values
        such as `Mon 09:30`. Verified users may create at most 10 slots per day;
        unverified users may create at most 5. Duplicate or overlapping slots are
        not rejected by this endpoint.
      operationId: createBrandCollection
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CollectionCreateRequest'
            examples:
              withSchedule:
                summary: Collection with channels and weekly queue slots
                value:
                  title: Evergreen content
                  description: Reusable product tips
                  maxRepublish: 2
                  channels: [501, 502]
                  queues:
                    - Mon 09:30
                    - Wed 14:00
                    - Fri 18:15
              emptyCollection:
                summary: Collection without channels or queue slots
                value:
                  title: Draft ideas
                  description: Work in progress
      responses:
        '201':
          description: Collection created successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectionCreateResponse'
              example:
                status: success
                data:
                  id: 77
                  title: Evergreen content
                  description: Reusable product tips
                  status: ACTIVE
                  evergreen:
                    enabled: true
                    maxRepublish: 2
                  maxRepublish: 2
                  timezone: Africa/Casablanca
                  createdAt: '2026-10-05T12:00:00.000000Z'
                  updatedAt: '2026-10-05T12:00:00.000000Z'
                  channels:
                    - id: 501
                      name: Acme on LinkedIn
                      status: ACTIVE
                      type: LinkedIn
                  queues:
                    - id: 910
                      day: MON
                      time: '09:30'
                      date: MON 09:30
                      status: PENDING
        '400':
          $ref: '#/components/responses/InvalidCollectionConfiguration'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PlanLimitReached'
        '404':
          $ref: '#/components/responses/BrandMembershipNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/channels:
    parameters:
      - $ref: '#/components/parameters/BrandId'
    get:
      tags: [Channels]
      summary: List brand channels
      description: |
        Returns active and inactive channels newest first. Other channel statuses
        and Pinterest account-container records are omitted from `data`. Pagination
        totals are calculated before status filtering, so `data` can contain fewer
        than `perPage` entries and can be empty while `total` is non-zero.
      operationId: listBrandChannels
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Channels retrieved successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelListResponse'
              example:
                status: success
                data:
                  - id: 501
                    name: Acme on LinkedIn
                    status: ACTIVE
                    type: LinkedIn
                    createdAt: '2026-09-15T10:20:30.000000Z'
                    updatedAt: '2026-10-01T14:05:00.000000Z'
                pagination:
                  currentPage: 1
                  perPage: 25
                  total: 1
                  lastPage: 1
                  nextPageUrl: null
                  prevPageUrl: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/collections/{collection_id}/posts:
    parameters:
      - $ref: '#/components/parameters/BrandId'
      - $ref: '#/components/parameters/CollectionId'
    get:
      tags: [Posts]
      summary: List collection posts
      description: |
        Use `view=QUEUE|SCHEDULED|DRAFT|PUBLISHED` to list a single kind of post.
        Returns posts newest first by default. Media objects contain Nuelink media identifiers.
        For polls, `body` is null and decoded poll fields are returned in `poll`.
        All filters are optional and combined with AND. Creation date filters use
        UTC and include their boundary values.
        Sorting is deterministic: when `sort_by` is not `id`, the post ID is used
        as a secondary key in the same direction.
      operationId: listCollectionPosts
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PostView'
        - $ref: '#/components/parameters/PostStatus'
        - $ref: '#/components/parameters/PostType'
        - $ref: '#/components/parameters/PostingType'
        - $ref: '#/components/parameters/CreatedFrom'
        - $ref: '#/components/parameters/CreatedTo'
        - $ref: '#/components/parameters/PostSortBy'
        - $ref: '#/components/parameters/SortOrder'
      responses:
        '200':
          description: Posts retrieved successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostListResponse'
              examples:
                scheduledImagePost:
                  summary: Scheduled image post
                  value:
                    status: success
                    data:
                      - id: 8801
                        collectionId: 77
                        title: Product launch
                        body: Our new product is live.
                        media:
                          - id: 9b8c7d6e5f4a.jpg
                        postType: IMAGE
                        postingType: SCHEDULE
                        status: PENDING
                        postDate: '2026-10-10 14:00:00'
                        createdAt: '2026-10-05T12:00:00.000000Z'
                        updatedAt: '2026-10-05T12:00:00.000000Z'
                    pagination:
                      currentPage: 1
                      perPage: 25
                      total: 1
                      lastPage: 1
                      nextPageUrl: null
                      prevPageUrl: null
                pollPost:
                  summary: Poll post
                  value:
                    status: success
                    data:
                      - id: 8802
                        collectionId: 77
                        title: Feature poll
                        body: null
                        media: []
                        postType: POLL
                        postingType: NOW
                        status: PENDING
                        postDate: null
                        createdAt: '2026-10-05T12:00:00.000000Z'
                        updatedAt: '2026-10-05T12:00:00.000000Z'
                        poll:
                          caption: Vote for the next feature.
                          question: What should we build next?
                          options: [Analytics, Mobile app, Integrations]
                          period: 3d
                          correctOptionIndex: null
                          explanation: null
                    pagination:
                      currentPage: 1
                      perPage: 25
                      total: 1
                      lastPage: 1
                      nextPageUrl: null
                      prevPageUrl: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ResourceNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      tags: [Posts]
      summary: Create a collection post
      description: |
        Creates a text, media, story, thread, document, or poll post. Supply either
        `caption` or `poll`. A media item must contain exactly one of `id` (an
        existing media identifier in this brand) or `url` (downloaded by Nuelink).

        `SCHEDULE` requires `scheduledAt` in the brand's local timezone and at
        least 10 minutes in the future; it is stored in UTC. `IMMEDIATE` is
        returned by list operations as posting type `NOW`. A caption containing
        triple-newline separators becomes a thread with at most 10 segments.
        Creation can also fail when brand, collection, scheduled-post, or plan
        limits are reached; a brand may contain at most 15,000 posts.
      operationId: createCollectionPost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostCreateRequest'
            examples:
              scheduledMediaPost:
                summary: Schedule an existing image
                value:
                  title: Product launch
                  caption: Our new product is live. Learn more at the link below.
                  publishMode: SCHEDULE
                  scheduledAt: '2026-10-10 15:00:00'
                  media:
                    - id: 9b8c7d6e5f4a.jpg
                  alt: Product package on a blue background
                  link: https://example.com/products/new
                  comment:
                    delay: 10
                    comment: Full details are available on our website.
                  platforms:
                    instagram:
                      collab: partner_account
                      location:
                        id: '123456789'
                        name: Casablanca
                      shareToFeed: true
                    youtube:
                      tags: [product, launch]
              poll:
                summary: Queue a poll
                value:
                  title: Feature poll
                  caption: Vote for the next feature.
                  publishMode: QUEUE
                  poll:
                    question: What should we build next?
                    options: [Analytics, Mobile app, Integrations]
                    period: 3d
              externalVideo:
                summary: Publish a remotely hosted video immediately
                value:
                  caption: Watch our latest walkthrough.
                  publishMode: IMMEDIATE
                  media:
                    - url: https://cdn.example.com/videos/walkthrough.mp4
                  platforms:
                    tiktok:
                      sendToInbox: false
                      autoAddMusic: true
                    youtube:
                      playlists:
                        - channelId: 501
                          playlistIds: [PL123456789]
      responses:
        '201':
          description: Post created successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostCreateResponse'
              example:
                status: success
                data:
                  id: 8801
                  message: Post created successfully
        '400':
          $ref: '#/components/responses/InvalidPostMedia'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenOrLimit'
        '404':
          $ref: '#/components/responses/ResourceNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/schedule:
    parameters:
      - $ref: '#/components/parameters/BrandId'
    get:
      tags: [Schedule]
      summary: Get the weekly queue schedule
      description: |
        Returns every weekly queue time of the brand, grouped by day and ordered by
        time. Times are in the brand `timezone`. When `queueStatus` is `PAUSED`
        nothing is published from the queues.
      operationId: getBrandSchedule
      responses:
        '200':
          description: Schedule retrieved successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandScheduleResponse'
              example:
                status: success
                data:
                  timezone: Africa/Casablanca
                  queueStatus: ACTIVE
                  schedule:
                    MON:
                      - time: '09:30'
                        queueId: 910
                        collectionId: 77
                        collectionTitle: Evergreen content
                        collectionStatus: ACTIVE
                    TUE: []
                    WED: []
                    THU: []
                    FRI: []
                    SAT: []
                    SUN: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandMembershipNotFound'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/posts:
    parameters:
      - $ref: '#/components/parameters/BrandId'
    get:
      tags: [Posts]
      summary: List brand posts
      description: |
        Lists posts across all collections of the brand. Accepts the same filters as
        the collection posts endpoint plus `collection_id`. Use
        `view=QUEUE|SCHEDULED|DRAFT|PUBLISHED` to separate queued posts, scheduled
        posts, drafts, and published posts.
      operationId: listBrandPosts
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: collection_id
          in: query
          required: false
          description: Only return posts of this collection.
          schema:
            type: integer
            format: int64
            minimum: 1
        - $ref: '#/components/parameters/PostView'
        - $ref: '#/components/parameters/PostStatus'
        - $ref: '#/components/parameters/PostType'
        - $ref: '#/components/parameters/PostingType'
        - $ref: '#/components/parameters/CreatedFrom'
        - $ref: '#/components/parameters/CreatedTo'
        - $ref: '#/components/parameters/PostSortBy'
        - $ref: '#/components/parameters/SortOrder'
      responses:
        '200':
          description: Posts retrieved successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostListResponse'
              example:
                status: success
                data:
                  - id: 8803
                    collectionId: 77
                    title: Weekly tip
                    body: Tip of the week.
                    media: []
                    postType: TEXT
                    postingType: QUEUE
                    status: PENDING
                    postDate: null
                    createdAt: '2026-10-05T12:00:00.000000Z'
                    updatedAt: '2026-10-05T12:00:00.000000Z'
                pagination:
                  currentPage: 1
                  perPage: 25
                  total: 1
                  lastPage: 1
                  nextPageUrl: null
                  prevPageUrl: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ResourceNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/posts/{post_id}:
    parameters:
      - $ref: '#/components/parameters/BrandId'
      - $ref: '#/components/parameters/PostId'
    patch:
      tags: [Posts]
      summary: Reschedule, re-queue, or draft a post
      description: |
        Limited edit of a draft, queued, or scheduled post. Published posts cannot
        be edited (HTTP 409). Content is not editable through this endpoint.

        * `publishMode: DRAFT` turns the post into a draft.
        * `publishMode: SCHEDULE` with `scheduledAt` (brand timezone, at least 10
          minutes ahead) reschedules the post.
        * `publishMode: QUEUE` puts the post in its collection queue.
        * `queuePosition: FRONT|BACK` re-orders a queued post, alone or together
          with `publishMode: QUEUE`. Raw priorities are intentionally not exposed.
      operationId: updateBrandPost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostUpdateRequest'
            examples:
              reschedule:
                value:
                  publishMode: SCHEDULE
                  scheduledAt: '2026-10-12 09:00:00'
              moveToFront:
                value:
                  queuePosition: FRONT
              draft:
                value:
                  publishMode: DRAFT
              requeueAtFront:
                summary: Put a draft in the queue and publish it next
                value:
                  publishMode: QUEUE
                  queuePosition: FRONT
      responses:
        '200':
          description: Post updated successfully. Returns the updated post.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostUpdateResponse'
              example:
                status: success
                data:
                  id: 8801
                  collectionId: 77
                  title: Product launch
                  body: Our new product is live.
                  media: []
                  postType: TEXT
                  postingType: SCHEDULE
                  status: PENDING
                  postDate: '2026-10-12 08:00:00'
                  createdAt: '2026-10-05T12:00:00.000000Z'
                  updatedAt: '2026-10-05T14:00:00.000000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenOrLimit'
        '404':
          $ref: '#/components/responses/ResourceNotFound'
        '409':
          description: The post is already published and cannot be edited.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: error
                message: Only draft, queued or scheduled posts can be edited
                errors:
                  post_id: Only draft, queued or scheduled posts can be edited
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      tags: [Posts]
      summary: Delete a post
      description: |
        Permanently deletes a post. This is a sensitive action: it is refused with
        HTTP 403 unless "Allow AI to perform sensitive actions" is enabled in the
        dashboard API settings.
      operationId: deleteBrandPost
      x-mcp-annotations:
        destructiveHint: true
      responses:
        '200':
          description: Post deleted successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostDeleteResponse'
              example:
                status: success
                data:
                  id: 8801
                  message: Post deleted successfully
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Sensitive actions are not enabled for this account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: error
                message: Deleting posts through the API is disabled. Enable sensitive actions for AI in your API settings to allow it.
                errors:
                  permissions: Deleting posts through the API is disabled. Enable sensitive actions for AI in your API settings to allow it.
        '404':
          $ref: '#/components/responses/ResourceNotFound'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/published-posts:
    parameters:
      - $ref: '#/components/parameters/BrandId'
    get:
      tags: [Published Posts]
      summary: List published posts with results
      description: |
        Lists what was actually sent to channels, one entry per channel, with the
        engagement `results` (likes, comments, shares) so performance can be
        reviewed. Defaults to `status=PUBLISHED`; newest first.
      operationId: listPublishedPosts
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: status
          in: query
          schema:
            type: string
            enum: [PUBLISHED, FAILED, RETRYING, SKIPPED, SENT]
            default: PUBLISHED
        - name: collection_id
          in: query
          schema:
            type: integer
            format: int64
        - name: channel_id
          in: query
          schema:
            type: integer
            format: int64
        - name: post_id
          in: query
          description: The Nuelink post these were published from.
          schema:
            type: integer
            format: int64
        - name: post_type
          in: query
          schema:
            type: string
        - name: search
          in: query
          description: Case-insensitive match against the published text.
          schema:
            type: string
            maxLength: 255
        - name: published_from
          in: query
          description: Include results published at or after this UTC timestamp.
          schema:
            type: string
            pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
          example: '2026-10-01 00:00:00'
        - name: published_to
          in: query
          description: Include results published at or before this UTC timestamp; must not precede `published_from`.
          schema:
            type: string
            pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
          example: '2026-10-31 23:59:59'
        - name: sort_by
          in: query
          schema:
            type: string
            enum: [id, created_at, likes, comments, shares]
            default: id
        - $ref: '#/components/parameters/SortOrder'
      responses:
        '200':
          description: Published posts retrieved successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublishedPostListResponse'
              example:
                status: success
                data:
                  - id: 120045
                    postId: 8801
                    collectionId: 77
                    channel:
                      id: 501
                      name: Acme on LinkedIn
                      type: LinkedIn
                    postType: IMAGE
                    body: Our new product is live.
                    url: https://www.linkedin.com/feed/update/urn:li:share:123
                    status: PUBLISHED
                    message: null
                    publishedAt: '2026-10-10T14:00:00.000000Z'
                    results:
                      likes: 42
                      comments: 5
                      shares: 3
                pagination:
                  currentPage: 1
                  perPage: 25
                  total: 1
                  lastPage: 1
                  nextPageUrl: null
                  prevPageUrl: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandMembershipNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/PublicApiRateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API token
      description: 'Preferred. Use `Authorization: Bearer <token>`.'
    apiKeyQuery:
      type: apiKey
      in: query
      name: api_key
      description: Compatibility fallback. Prefer bearer authentication.

  parameters:
    BrandId:
      name: brand_id
      in: path
      required: true
      description: Numeric ID of a brand accessible to the authenticated user.
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 42
    CollectionId:
      name: collection_id
      in: path
      required: true
      description: Numeric ID of a collection belonging to the selected brand.
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 77
    Page:
      name: page
      in: query
      required: false
      description: One-based result page.
      schema:
        type: integer
        minimum: 1
        default: 1
      example: 1
    PerPage:
      name: per_page
      in: query
      required: false
      description: Number of records per page.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
      example: 25
    MediaType:
      name: type
      in: query
      required: false
      description: Optional media category filter.
      schema:
        type: string
        enum: [IMAGE, VIDEO, GIF, APPLICATION, DOCUMENT, CSV]
      example: IMAGE
    PostView:
      name: view
      in: query
      required: false
      description: |
        Shortcut filter. `QUEUE` returns queued posts, `SCHEDULED` posts with a fixed
        date, `DRAFT` drafts, and `PUBLISHED` posts that were published.
      schema:
        type: string
        enum: [QUEUE, SCHEDULED, DRAFT, PUBLISHED]
      example: QUEUE
    PostId:
      name: post_id
      in: path
      required: true
      description: Numeric ID of a post belonging to the selected brand.
      schema:
        type: integer
        format: int64
        minimum: 1
      example: 8801
    PostStatus:
      name: status
      in: query
      required: false
      description: Return only posts with this lifecycle status.
      schema:
        type: string
        enum: [PENDING, DRAFT, PUBLISHED]
      example: PENDING
    PostType:
      name: post_type
      in: query
      required: false
      description: Return only posts with this content type.
      schema:
        type: string
        enum: [TEXT, IMAGE, VIDEO, MULTIMEDIA, SHORT, DOCUMENT, THREAD, SLIDES, REEL, TIKTOK, STORY, POLL, ENGAGE_COMMENT]
      example: IMAGE
    PostingType:
      name: posting_type
      in: query
      required: false
      description: Return only posts with this publishing mode.
      schema:
        type: string
        enum: [NOW, SCHEDULE, QUEUE, DRAFT]
      example: SCHEDULE
    CreatedFrom:
      name: created_from
      in: query
      required: false
      description: Include posts created at or after this UTC timestamp.
      schema:
        type: string
        pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
      example: '2026-10-01 00:00:00'
    CreatedTo:
      name: created_to
      in: query
      required: false
      description: Include posts created at or before this UTC timestamp. Must not precede `created_from` when both are supplied.
      schema:
        type: string
        pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
      example: '2026-10-31 23:59:59'
    PostSortBy:
      name: sort_by
      in: query
      required: false
      description: Field used to order matching posts.
      schema:
        type: string
        enum: [id, title, post_date, created_at, updated_at]
        default: id
      example: post_date
    SortOrder:
      name: sort_order
      in: query
      required: false
      description: Sort direction.
      schema:
        type: string
        enum: [asc, desc]
        default: desc
      example: asc

  headers:
    RateLimitLimit:
      description: Maximum requests available in the public-API window.
      schema:
        type: integer
        example: 30
    RateLimitRemaining:
      description: Requests remaining in the current public-API window.
      schema:
        type: integer
        minimum: 0
        example: 29
    RateLimitReset:
      description: Seconds until another public-API request is available.
      schema:
        type: integer
        minimum: 0
        example: 12
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        minimum: 1
        example: 12
    RouteRateLimitLimit:
      description: Maximum requests in the route-level window.
      schema:
        type: integer
        example: 60
    RouteRateLimitRemaining:
      description: Requests remaining in the current route-level window.
      schema:
        type: integer
        minimum: 0
        example: 59
    RouteRateLimitReset:
      description: Unix timestamp at which the route-level limit resets. Present on rejected requests.
      schema:
        type: integer
        format: int64
        example: 1791201660
    RejectedRateLimitLimit:
      description: |
        Size of the window that rejected the request: `30` when the public-API
        limit rejected it, `60` when the route-level limit did.
      schema:
        type: integer
        example: 30
    RejectedRateLimitRemaining:
      description: Requests remaining in the window that rejected the request.
      schema:
        type: integer
        minimum: 0
        example: 0
    RejectedRateLimitReset:
      description: |
        When another request is available. When the public-API limit rejected the
        request, this is the number of seconds to wait; when the route-level limit
        did, it is the Unix timestamp at which that window resets. Both windows
        last one minute, so a value above `60` is a timestamp. Prefer
        `Retry-After` when it is present.
      schema:
        type: integer
        format: int64
        minimum: 0
        example: 12

  responses:
    ProfileSuccess:
      description: Profile retrieved successfully.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RouteRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RouteRateLimitRemaining'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProfileResponse'
          example:
            status: success
            data:
              id: 45b4d6e9171f8c55c6a0319f85fbed2c
              name: Alex Morgan
              joinedAt: '2025-06-20T10:15:30.000000Z'
              timezone: Africa/Casablanca
    Unauthorized:
      description: Token missing, invalid, expired, or not associated with a user.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          examples:
            missingToken:
              value:
                status: error
                message: Token is required
                errors:
                  authorization: Token is required
            invalidToken:
              value:
                status: error
                message: Invalid Token
                errors:
                  authorization: Invalid Token
            expiredToken:
              value:
                status: error
                message: Token has expired
                errors:
                  authorization: Token has expired
    ValidationFailed:
      description: One or more values failed validation or a domain validation rule rejected the request.
      content:
        application/json:
          schema:
            anyOf:
              - $ref: '#/components/schemas/ValidationError'
              - $ref: '#/components/schemas/ApiError'
          examples:
            frameworkValidation:
              value:
                message: The given data was invalid.
                errors:
                  per_page:
                    - The per page must not be greater than 100.
            domainValidation:
              value:
                status: error
                message: Scheduled post date must be at least 10 minutes in the future
                errors:
                  scheduledAt: Scheduled post date must be at least 10 minutes in the future
    BrandNotFound:
      description: Brand not found or inaccessible to the authenticated user.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          example:
            status: error
            message: Brand not found
            errors:
              brand_id: Brand not found
    BrandMembershipNotFound:
      description: Brand not found or the authenticated user is not a member.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          example:
            status: error
            message: Brand not found or you are not a member of the selected brand
            errors:
              brand_id: Brand not found or you are not a member of the selected brand
    ResourceNotFound:
      description: A referenced brand, collection, channel, or media item was not found or is inaccessible.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          examples:
            collection:
              value:
                status: error
                message: Collection not found for this brand
                errors:
                  collection_id: Collection not found for this brand
            media:
              value:
                status: error
                message: Media with id missing.jpg not found
                errors:
                  media: Media with id missing.jpg not found
    PlanLimitReached:
      description: A plan, brand, collection, automation, or scheduled-post limit was reached.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          example:
            status: error
            message: Collections limit reached for this brand
            errors:
              limits: Collections limit reached for this brand
    ForbiddenOrLimit:
      description: The user is not a brand member or an applicable account/plan limit was reached.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          examples:
            notMember:
              value:
                status: error
                message: You are not a member of this brand
                errors:
                  brand_id: You are not a member of this brand
            postLimit:
              value:
                status: error
                message: You have reached your limit for scheduled and queued posts for this brand
                errors:
                  limits: You have reached your limit for scheduled and queued posts for this brand
    InvalidCollectionConfiguration:
      description: Channels or queue slots are invalid for this brand or account.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          examples:
            foreignChannel:
              value:
                status: error
                message: One or more channels do not belong to the specified brand
                errors:
                  channels: One or more channels do not belong to the specified brand
            tooManyQueueSlots:
              value:
                status: error
                message: You can only schedule 10 posts per collection a day
                errors:
                  queues: You can only schedule 10 posts per collection a day
    InvalidPostMedia:
      description: External media could not be downloaded.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          example:
            status: error
            message: 'Failed to download media from url: https://cdn.example.com/missing.jpg'
            errors:
              media: 'Failed to download media from url: https://cdn.example.com/missing.jpg'
    PublicApiRateLimited:
      description: |
        The public-API or route-level request limit was exceeded. The
        `X-RateLimit-*` values describe whichever limit rejected the request.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RejectedRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RejectedRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RejectedRateLimitReset'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            anyOf:
              - $ref: '#/components/schemas/ThrottleError'
              - type: object
                required: [message]
                properties:
                  message:
                    type: string
          examples:
            publicApiLimit:
              value:
                message: Too many requests. Please try again later.
                errors:
                  rate_limit: Too many requests. Please try again later.
            routeLevelLimit:
              value:
                message: Too Many Attempts.
    RouteRateLimited:
      description: The route-level limit of 60 requests per minute per client IP was exceeded.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RouteRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RouteRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RouteRateLimitReset'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            type: object
            required: [message]
            properties:
              message:
                type: string
          example:
            message: Too Many Attempts.
    InternalServerError:
      description: An unexpected authenticated request failure occurred.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          example:
            status: error
            message: Internal Server Error
            errors:
              server: Internal Server Error

  schemas:
    ApiError:
      type: object
      required: [status, message, errors]
      properties:
        status:
          type: string
          enum: [error]
        message:
          type: string
        errors:
          type: object
          additionalProperties:
            type: string
    ValidationError:
      type: object
      required: [message, errors]
      properties:
        message:
          type: string
          example: The given data was invalid.
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
    ThrottleError:
      type: object
      required: [message, errors]
      properties:
        message:
          type: string
        errors:
          type: object
          required: [rate_limit]
          properties:
            rate_limit:
              type: string
    Pagination:
      type: object
      required: [currentPage, perPage, total, lastPage, nextPageUrl, prevPageUrl]
      properties:
        currentPage:
          type: integer
          minimum: 1
        perPage:
          type: integer
          minimum: 1
          maximum: 100
        total:
          type: integer
          minimum: 0
        lastPage:
          type: integer
          minimum: 1
        nextPageUrl:
          type: string
          format: uri
          nullable: true
        prevPageUrl:
          type: string
          format: uri
          nullable: true
    Timestamp:
      type: string
      format: date-time
      example: '2026-10-05T12:00:00.000000Z'
    ProfileResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/Profile'
    Profile:
      type: object
      required: [id, name, joinedAt, timezone]
      properties:
        id:
          type: string
          description: Stable, opaque hash of the internal user ID.
          example: 45b4d6e9171f8c55c6a0319f85fbed2c
        name:
          type: string
          example: Alex Morgan
        joinedAt:
          $ref: '#/components/schemas/Timestamp'
        timezone:
          type: string
          nullable: true
          description: User timezone identifier or UUID as stored by Nuelink.
          example: Africa/Casablanca
    Brand:
      type: object
      required: [id, title, description, timezone, queueStatus, createdAt, updatedAt]
      properties:
        id:
          type: integer
          format: int64
        title:
          type: string
        description:
          type: string
          nullable: true
        timezone:
          type: string
          nullable: true
          example: Africa/Casablanca
        queueStatus:
          type: string
          enum: [ACTIVE, PAUSED]
          description: When `PAUSED`, no queued post is published for any collection of the brand.
        createdAt:
          $ref: '#/components/schemas/Timestamp'
        updatedAt:
          $ref: '#/components/schemas/Timestamp'
    BrandListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/Brand'
        pagination:
          $ref: '#/components/schemas/Pagination'
    Media:
      type: object
      required: [id, name, type, size, createdAt, updatedAt]
      properties:
        id:
          type: string
          description: Public media identifier used when creating posts.
        name:
          type: string
          nullable: true
        type:
          type: string
          description: MIME type.
          example: image/jpeg
        size:
          type: integer
          format: int64
          minimum: 0
          description: File size in bytes.
        createdAt:
          $ref: '#/components/schemas/Timestamp'
        updatedAt:
          $ref: '#/components/schemas/Timestamp'
    MediaListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/Media'
        pagination:
          $ref: '#/components/schemas/Pagination'
    MediaUploadResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          required: [id, type, size]
          properties:
            id:
              type: string
            type:
              type: string
              example: image/jpeg
            size:
              type: integer
              format: int64
              minimum: 0
    Automation:
      type: object
      required:
        - id
        - name
        - collectionId
        - status
        - type
        - feedUrl
        - runCount
        - importAsType
        - lastRun
        - title
        - caption
        - createdAt
        - updatedAt
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
          description: Name of the automation.
        collectionId:
          type: integer
          format: int64
        status:
          type: string
          example: ACTIVE
        type:
          $ref: '#/components/schemas/FeedType'
        feedUrl:
          type: string
          format: uri
        runCount:
          type: integer
          minimum: 0
        importAsType:
          type: string
          enum: [LINK, IMAGE, VIDEO, CAROUSEL]
          nullable: true
        lastRun:
          type: integer
          format: int64
          minimum: 0
          description: Unix timestamp; 0 means the automation has not run.
        title:
          type: string
          nullable: true
          description: Title template applied to each imported post.
        caption:
          type: string
          nullable: true
          description: Caption template applied to each imported post.
        createdAt:
          $ref: '#/components/schemas/Timestamp'
        updatedAt:
          $ref: '#/components/schemas/Timestamp'
    AutomationListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/Automation'
        pagination:
          $ref: '#/components/schemas/Pagination'
    AutomationCreateResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/Automation'
    FeedType:
      type: string
      enum:
        - YOUTUBE
        - RSS
        - ATOM
        - MEDIUM
        - SHOPIFY
        - WORDPRESS
        - WOOCOMMERCE
        - ETSY
        - GHOST
        - SUBSTACK
        - ANCHOR
        - TRANSISTOR
        - CAPTIVATE
        - SOUNDCLOUD
        - BLOGGER
        - WIX
        - SQUARESPACEBLOG
        - SQUARESPACESHOP
        - WEEBLY
        - TUMBLR
        - SHOPIFY2
        - WOOCOMMERCE2
        - PRESTASHOP
        - BUZZSPROUT
        - RSSCOM
        - CASTOS
        - PODCASTCO
    AutomationCreateRequest:
      type: object
      required: [name, collectionId, type, feedUrl, importAsType]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Name of the automation.
        collectionId:
          type: integer
          format: int64
          minimum: 1
          description: Existing collection in the path brand.
        type:
          $ref: '#/components/schemas/FeedType'
        feedUrl:
          type: string
          format: uri
        importAsType:
          type: string
          enum: [LINK, IMAGE, VIDEO, CAROUSEL]
        title:
          type: string
          maxLength: 255
          nullable: true
          default: '{{title}}'
          description: Title template applied to each imported post.
        caption:
          type: string
          maxLength: 255
          nullable: true
          description: Caption template applied to each imported post.
        loadOldPosts:
          type: boolean
          nullable: true
          default: false
        addPostsAsDraft:
          type: boolean
          nullable: true
          default: false
        refreshRate:
          type: integer
          enum: [1, 6, 12, 24]
          nullable: true
          default: 24
          description: Refresh interval in hours.
    ChannelSummary:
      type: object
      required: [id, name, status, type]
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
        type:
          type: string
          description: Human-readable platform name.
          enum:
            - Facebook
            - Instagram
            - Pinterest
            - LinkedIn
            - X
            - TikTok
            - YouTube
            - Google Business
            - Telegram
            - Threads
            - Bluesky
            - Mastodon
            - Social Media
    Channel:
      allOf:
        - $ref: '#/components/schemas/ChannelSummary'
        - type: object
          required: [createdAt, updatedAt]
          properties:
            createdAt:
              $ref: '#/components/schemas/Timestamp'
            updatedAt:
              $ref: '#/components/schemas/Timestamp'
    ChannelListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/Channel'
        pagination:
          $ref: '#/components/schemas/Pagination'
    Collection:
      type: object
      required: [id, title, description, status, evergreen, maxRepublish, timezone, createdAt, updatedAt, channels, queues]
      properties:
        id:
          type: integer
          format: int64
        title:
          type: string
        description:
          type: string
          nullable: true
        status:
          type: string
          enum: [ACTIVE, PAUSED]
          description: Paused collections do not publish queued posts.
        evergreen:
          type: object
          required: [enabled, maxRepublish]
          properties:
            enabled:
              type: boolean
              description: Whether published posts are added back to the queue.
            maxRepublish:
              type: integer
              description: "`0` disables evergreen, `-1` republishes forever, and `N` re-adds posts after N weeks."
        maxRepublish:
          type: integer
          description: Same value as `evergreen.maxRepublish`.
        timezone:
          type: string
          nullable: true
          description: Timezone of the queue times.
        createdAt:
          $ref: '#/components/schemas/Timestamp'
        updatedAt:
          $ref: '#/components/schemas/Timestamp'
        channels:
          type: array
          items:
            $ref: '#/components/schemas/ChannelSummary'
        queues:
          type: array
          description: Weekly queue times, ordered Monday to Sunday.
          items:
            $ref: '#/components/schemas/QueueSlot'
        postsCount:
          type: object
          description: Only returned by the list endpoint.
          properties:
            queued:
              type: integer
            scheduled:
              type: integer
            drafts:
              type: integer
            published:
              type: integer
    CollectionListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/Collection'
        pagination:
          $ref: '#/components/schemas/Pagination'
    CollectionCreateRequest:
      type: object
      required: [title]
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          maxLength: 1000
          nullable: true
        maxRepublish:
          type: integer
          minimum: 0
          nullable: true
          default: 0
        channels:
          type: array
          nullable: true
          default: []
          description: Existing channel IDs from the path brand.
          items:
            type: integer
            format: int64
        queues:
          type: array
          nullable: true
          default: []
          description: Weekly slots in the brand timezone, formatted `D H:i`.
          items:
            type: string
            pattern: '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) ([01][0-9]|2[0-3]):[0-5][0-9]$'
            example: Mon 09:30
    QueueSlot:
      type: object
      required: [id, day, time, date, status]
      properties:
        id:
          type: integer
          format: int64
        day:
          type: string
          enum: [MON, TUE, WED, THU, FRI, SAT, SUN]
        time:
          type: string
          pattern: '^([01][0-9]|2[0-3]):[0-5][0-9]$'
          example: '09:30'
          description: Local time in the brand timezone.
        date:
          type: string
          pattern: '^(MON|TUE|WED|THU|FRI|SAT|SUN) ([01][0-9]|2[0-3]):[0-5][0-9]$'
          example: MON 09:30
        status:
          type: string
          enum: [PENDING, PUBLISHED]
    CreatedCollection:
      allOf:
        - $ref: '#/components/schemas/Collection'
        - type: object
          properties: {}
    CollectionCreateResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/CreatedCollection'
    MediaReference:
      type: object
      required: [id]
      properties:
        id:
          type: string
    PollResponse:
      type: object
      required: [caption, question, options, period, correctOptionIndex, explanation]
      properties:
        caption:
          type: string
          nullable: true
        question:
          type: string
          nullable: true
        options:
          type: array
          nullable: true
          minItems: 2
          maxItems: 4
          items:
            type: string
        period:
          type: string
          enum: [1d, 3d, 7d]
          nullable: true
        correctOptionIndex:
          type: integer
          minimum: 0
          nullable: true
        explanation:
          type: string
          nullable: true
    Post:
      type: object
      required:
        - id
        - collectionId
        - title
        - body
        - media
        - postType
        - postingType
        - status
        - postDate
        - createdAt
        - updatedAt
      properties:
        id:
          type: integer
          format: int64
        collectionId:
          type: integer
          format: int64
        title:
          type: string
          nullable: true
        body:
          type: string
          nullable: true
        media:
          type: array
          items:
            $ref: '#/components/schemas/MediaReference'
        postType:
          type: string
          description: Derived by the server from media, poll, story, and thread inputs.
          enum: [TEXT, IMAGE, VIDEO, MULTIMEDIA, SHORT, DOCUMENT, THREAD, SLIDES, REEL, TIKTOK, STORY, POLL, ENGAGE_COMMENT]
        postingType:
          type: string
          description: Immediate posts are represented as `NOW` in responses.
          enum: [NOW, SCHEDULE, QUEUE, DRAFT]
        status:
          type: string
          example: PENDING
        postDate:
          type: string
          nullable: true
          description: Scheduled UTC timestamp in `Y-m-d H:i:s` format.
          pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
        createdAt:
          $ref: '#/components/schemas/Timestamp'
        updatedAt:
          $ref: '#/components/schemas/Timestamp'
        poll:
          $ref: '#/components/schemas/PollResponse'
    PostListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/Post'
        pagination:
          $ref: '#/components/schemas/Pagination'
    BrandScheduleResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          required: [timezone, queueStatus, schedule]
          properties:
            timezone:
              type: string
              nullable: true
            queueStatus:
              type: string
              enum: [ACTIVE, PAUSED]
            schedule:
              type: object
              description: Keys are `MON` to `SUN`.
              additionalProperties:
                type: array
                items:
                  type: object
                  required: [time, queueId, collectionId, collectionTitle, collectionStatus]
                  properties:
                    time:
                      type: string
                      example: '09:30'
                    queueId:
                      type: integer
                      format: int64
                    collectionId:
                      type: integer
                      format: int64
                    collectionTitle:
                      type: string
                    collectionStatus:
                      type: string
                      enum: [ACTIVE, PAUSED]
    PostUpdateRequest:
      type: object
      description: |
        At least one of `publishMode` or `queuePosition` is required. `scheduledAt`
        is required when `publishMode` is `SCHEDULE`, and not allowed otherwise.
      properties:
        publishMode:
          type: string
          enum: [DRAFT, SCHEDULE, QUEUE]
        scheduledAt:
          type: string
          description: Brand-local `Y-m-d H:i:s`; required with `SCHEDULE`, otherwise not allowed.
          pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
        queuePosition:
          type: string
          enum: [FRONT, BACK]
          description: Move a queued post to the front or the back of its collection queue.
      allOf:
        - anyOf:
            - required: [publishMode]
            - required: [queuePosition]
        - anyOf:
            - required: [publishMode, scheduledAt]
              properties:
                publishMode:
                  enum: [SCHEDULE]
            - not:
                required: [scheduledAt]
              properties:
                publishMode:
                  enum: [DRAFT, QUEUE]
    PostUpdateResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [success]
        data:
          $ref: '#/components/schemas/Post'
    PostDeleteResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          required: [id, message]
          properties:
            id:
              type: integer
              format: int64
            message:
              type: string
    PublishedPost:
      type: object
      required: [id, postId, collectionId, channel, postType, body, url, status, message, publishedAt, results]
      properties:
        id:
          type: integer
          format: int64
        postId:
          type: integer
          format: int64
          nullable: true
        collectionId:
          type: integer
          format: int64
          nullable: true
        channel:
          type: object
          properties:
            id:
              type: integer
              format: int64
              nullable: true
            name:
              type: string
              nullable: true
            type:
              type: string
              nullable: true
        postType:
          type: string
          nullable: true
        body:
          type: string
          nullable: true
        url:
          type: string
          nullable: true
          description: Link to the post on the platform.
        status:
          type: string
        message:
          type: string
          nullable: true
        publishedAt:
          $ref: '#/components/schemas/Timestamp'
        results:
          type: object
          required: [likes, comments, shares]
          properties:
            likes:
              type: integer
            comments:
              type: integer
            shares:
              type: integer
    PublishedPostListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublishedPost'
        pagination:
          $ref: '#/components/schemas/Pagination'
    PollCreate:
      type: object
      required: [question, options, period]
      properties:
        question:
          type: string
          maxLength: 280
        options:
          type: array
          minItems: 2
          maxItems: 4
          items:
            type: string
            maxLength: 30
        correctOptionIndex:
          type: integer
          minimum: 0
          nullable: true
          description: Zero-based option index. The endpoint does not verify that it is within the options array.
        explanation:
          type: string
          maxLength: 512
          nullable: true
        period:
          type: string
          enum: [1d, 3d, 7d]
    PostMediaInput:
      description: Exactly one of `id` or `url` is required.
      oneOf:
        - type: object
          required: [id]
          properties:
            id:
              type: string
              minLength: 1
              description: Existing media identifier belonging to the path brand.
        - type: object
          required: [url]
          properties:
            url:
              type: string
              format: uri
              maxLength: 1000
              description: Public URL downloaded by Nuelink during the request.
    DelayedComment:
      type: object
      required: [delay, comment]
      properties:
        delay:
          type: integer
          minimum: 0
          description: Delay value consumed by Nuelink's comment scheduler.
        comment:
          type: string
          minLength: 1
          maxLength: 1000
    PlatformLocation:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
        name:
          type: string
    InstagramOptions:
      type: object
      properties:
        collab:
          type: string
          maxLength: 1000
          nullable: true
        location:
          type: object
          allOf:
            - $ref: '#/components/schemas/PlatformLocation'
          nullable: true
        trialReel:
          type: string
          enum: [MANUAL, SS_PERFORMANCE]
          nullable: true
        shareToFeed:
          type: boolean
          nullable: true
    FacebookOptions:
      type: object
      properties:
        location:
          type: object
          allOf:
            - $ref: '#/components/schemas/PlatformLocation'
          nullable: true
    TikTokOptions:
      type: object
      properties:
        sendToInbox:
          type: boolean
          nullable: true
        autoAddMusic:
          type: boolean
          nullable: true
    YouTubePlaylistSelection:
      type: object
      required: [channelId, playlistIds]
      properties:
        channelId:
          type: integer
          format: int64
          minimum: 1
          description: YouTube channel ID belonging to the path brand.
        playlistIds:
          type: array
          items:
            type: string
            minLength: 1
    YouTubeOptions:
      type: object
      properties:
        tags:
          type: array
          nullable: true
          items:
            type: string
            maxLength: 255
        playlists:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/YouTubePlaylistSelection'
    GoogleBusinessOptions:
      type: object
      properties:
        uploadToPhotosSection:
          type: boolean
          nullable: true
    PostPlatformOptions:
      type: object
      nullable: true
      properties:
        instagram:
          type: object
          allOf:
            - $ref: '#/components/schemas/InstagramOptions'
          nullable: true
        facebook:
          type: object
          allOf:
            - $ref: '#/components/schemas/FacebookOptions'
          nullable: true
        tiktok:
          type: object
          allOf:
            - $ref: '#/components/schemas/TikTokOptions'
          nullable: true
        youtube:
          type: object
          allOf:
            - $ref: '#/components/schemas/YouTubeOptions'
          nullable: true
        googlemybusiness:
          type: object
          allOf:
            - $ref: '#/components/schemas/GoogleBusinessOptions'
          nullable: true
    PostCreateRequest:
      type: object
      required: [publishMode]
      description: |
        Either `caption` or `poll` is required, and `null` counts as missing.
        `scheduledAt` is required when `publishMode` is `SCHEDULE`. Platform
        options are stored for compatible channels; channel-specific publishing
        constraints may apply later.
      properties:
        title:
          type: string
          maxLength: 255
          nullable: true
        caption:
          type: string
          minLength: 1
          maxLength: 3000
          nullable: true
        poll:
          type: object
          allOf:
            - $ref: '#/components/schemas/PollCreate'
          nullable: true
        alt:
          type: string
          maxLength: 255
          nullable: true
        link:
          type: string
          format: uri
          maxLength: 1000
          nullable: true
          description: Used for Google Business's LEARN_MORE button and Pinterest destination link.
        publishMode:
          type: string
          enum: [IMMEDIATE, SCHEDULE, QUEUE, DRAFT]
        scheduledAt:
          type: string
          nullable: true
          description: Brand-local time, format `Y-m-d H:i:s`; required for `SCHEDULE` and at least 10 minutes in the future.
          pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
          example: '2026-10-10 15:00:00'
        media:
          type: array
          nullable: true
          default: []
          items:
            $ref: '#/components/schemas/PostMediaInput'
        postAsStory:
          type: boolean
          nullable: true
          default: false
        autoThreadText:
          type: boolean
          nullable: true
          default: false
          description: Enables Nuelink's automatic text splitting behavior.
        comment:
          type: object
          allOf:
            - $ref: '#/components/schemas/DelayedComment'
          nullable: true
        platforms:
          $ref: '#/components/schemas/PostPlatformOptions'
      anyOf:
        - required: [caption]
          properties:
            caption:
              type: string
        - required: [poll]
          properties:
            poll:
              type: object
      allOf:
        - anyOf:
            - properties:
                publishMode:
                  enum: [IMMEDIATE, QUEUE, DRAFT]
            - required: [scheduledAt]
              properties:
                scheduledAt:
                  type: string
    PostCreateResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          required: [id, message]
          properties:
            id:
              type: integer
              format: int64
            message:
              type: string
              enum: [Post created successfully]
