openapi: 3.1.0
info:
  title: Nuelink External API
  version: 1.0.1
  license:
    name: Proprietary
    identifier: LicenseRef-Nuelink-Proprietary
  summary: Public external API for profile, brands, channels, collections, automations, media, and posts.
  description: |
    Production-ready OpenAPI specification generated from the Postman collection and Laravel implementation.

    Scope covered by this document:
    - Endpoints present in the Postman collection `Nuelink External API v1.0.1.postman_collection.json`
    - Route mappings from `routes/api.php`
    - Validation, business-rule rejections, and middleware behavior from controllers/middleware in `app/Http/Controllers/Api/Public/*` and `app/Http/Middleware/*`

    Authentication and middleware behavior:
    - All documented endpoints use bearer authentication through the `Authorization` header.
    - Custom middleware forces JSON response format.
    - Global per-token/per-user rate limit is 30 requests/minute for these routes.

    Important implementation notes:
    - Validation failures return Laravel-style 422 payloads: `{ message, errors }`.
    - Business-rule failures return custom payloads: `{ status: "error", message, errors }`.
    - If internal handler returns 500, middleware normalizes body to a generic internal server error payload.
    - Creation endpoints do not currently guarantee idempotency. Clients MUST NOT automatically retry a timed-out mutation. Reconcile with the corresponding list endpoint and request support guidance before retrying.

servers:
  - url: https://app.nuelink.com/api/public/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Profile
    description: Authenticated account profile operations.
  - name: Brands
    description: Brand discovery operations.
  - name: Channels
    description: Connected social channel discovery operations.
  - name: Collections
    description: Collection discovery and creation operations.
  - name: Automations
    description: Feed automation discovery and creation operations.
  - name: Media
    description: Media discovery and upload operations.
  - name: Posts
    description: Post discovery and creation operations.

paths:
  /me:
    get:
      tags: [Profile]
      operationId: getMe
      summary: Get current profile
      description: Returns current authenticated user profile mapped to external API format.
      responses:
        '200':
          description: Profile fetched successfully
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
              examples:
                success:
                  value:
                    status: success
                    data:
                      id: SAMPLE_USER_ID
                      name: Sample User
                      joinedAt: '2026-01-01T00:00:00.000000Z'
                      timezone: Europe/London
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands:
    get:
      tags: [Brands]
      operationId: listBrands
      summary: List brands available to current user
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Brands fetched successfully
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandsListResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/channels:
    get:
      tags: [Channels]
      operationId: listChannels
      summary: List channels in a brand
      parameters:
        - $ref: '#/components/parameters/BrandId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Channels fetched successfully
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelsListResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          description: Brand not found or inaccessible
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                brandNotFound:
                  value:
                    status: error
                    message: Brand not found
                    errors:
                      brand_id: Brand not found
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/collections:
    get:
      tags: [Collections]
      operationId: listCollections
      summary: List collections in a brand
      parameters:
        - $ref: '#/components/parameters/BrandId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Collections fetched successfully
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectionsListResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          description: Brand not found or user is not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                brandNotFound:
                  value:
                    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
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      tags: [Collections]
      operationId: createCollection
      summary: Create collection
      parameters:
        - $ref: '#/components/parameters/BrandId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCollectionRequest'
            examples:
              basic:
                value:
                  title: Example Collection
                  description: Example collection description
                  maxRepublish: 3
                  channels: [10001, 10002, 10003]
                  queues: [Mon 09:00, Wed 14:30, Fri 18:00, Sun 10:00]
      responses:
        '201':
          description: Collection created
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectionCreateResponse'
        '400':
          description: Invalid channels/queue business rules
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                channelsNotInBrand:
                  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
                queueLimit:
                  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
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          description: Plan or limit restriction
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                limits:
                  value:
                    status: error
                    message: Collections limit reached for this brand
                    errors:
                      limits: Collections limit reached for this brand
        '404':
          description: Brand not found or user is not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/automations:
    get:
      tags: [Automations]
      operationId: listAutomations
      summary: List feed automations
      parameters:
        - $ref: '#/components/parameters/BrandId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Automations fetched successfully
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationsListResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          description: Brand not found or user is not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      tags: [Automations]
      operationId: createAutomation
      summary: Create feed automation
      description: Creates a feed-based automation (type must be `FEED`).
      parameters:
        - $ref: '#/components/parameters/BrandId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAutomationRequest'
            examples:
              rss:
                value:
                  collectionId: 720
                  feedUrl: https://www.nasa.gov/rss/dyn/breaking_news.rss
                  importAsType: IMAGE
                  subType: RSS
                  title: NASA Breaking News
                  type: FEED
                  dynamicTitle: '{{title}}'
                  dynamicBody: '{title} {link}'
      responses:
        '201':
          description: Automation created
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationCreateResponse'
        '400':
          description: Feed URL parsing/fetching failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                feedFailed:
                  value:
                    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/UnauthorizedError'
        '403':
          description: Plan or limit restriction
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                automationLimit:
                  value:
                    status: error
                    message: Automations limit reached, please upgrade your plan to add more automations
                    errors:
                      limits: Automations limit reached, please upgrade your plan to add more automations
        '404':
          description: Brand or collection not found/inaccessible
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/media:
    get:
      tags: [Media]
      operationId: listMedia
      summary: List media in a brand
      parameters:
        - $ref: '#/components/parameters/BrandId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum: [IMAGE, VIDEO, GIF, APPLICATION, DOCUMENT, CSV]
      responses:
        '200':
          description: Media fetched successfully
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaListResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          description: Brand not found or user is not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      tags: [Media]
      operationId: uploadMedia
      summary: Upload media file
      description: |
        Uploads one media file.
        Allowed mime/extensions: `jpg,jpeg,png,bmp,webp,mp4,mov,pdf`. PDF is LinkedIn-only (document posts).
        Maximum size enforced by implementation: 100 MB.
      parameters:
        - $ref: '#/components/parameters/BrandId'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [media]
              properties:
                media:
                  type: string
                  format: binary
      responses:
        '201':
          description: Media uploaded
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaUploadResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          description: Brand not found or user is not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brand_id}/collections/{collection_id}/posts:
    get:
      tags: [Posts]
      operationId: listPosts
      summary: List posts in a collection
      parameters:
        - $ref: '#/components/parameters/BrandId'
        - $ref: '#/components/parameters/CollectionId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Posts fetched successfully
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostsListResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '404':
          description: Brand/collection not found or no membership
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                collectionNotFound:
                  value:
                    status: error
                    message: Collection not found for this brand
                    errors:
                      collection_id: Collection not found for this brand
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      tags: [Posts]
      operationId: createPost
      summary: Create post
      description: |
        Creates a post in the collection.

        Rules enforced by implementation include:
        - `publishMode` is required and must be one of `IMMEDIATE`, `SCHEDULE`, `QUEUE`, `DRAFT`.
        - If `publishMode=SCHEDULE`, `scheduledAt` is required (`Y-m-d H:i:s`) and must be at least 10 minutes in the future (UTC-adjusted).
        - `caption` is required unless `poll` object is provided.
        - `media[*]` items must provide exactly one of `id` or `url`.
        - `poll.period` supports `1d|3d|7d`.
        - Brand maximum total posts is 15,000.

      parameters:
        - $ref: '#/components/parameters/BrandId'
        - $ref: '#/components/parameters/CollectionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePostRequest'
            examples:
              scheduledPoll:
                value:
                  title: Example Post Title
                  caption: This is an example caption for a social media post.
                  poll:
                    question: Which feature would you like to see next?
                    options: [Feature One, Feature Two]
                    correctOptionIndex: 0
                    explanation: Example explanation for the selected option.
                    period: 1d
                  alt: Example alternative text for media accessibility
                  link: https://example.com/post
                  publishMode: SCHEDULE
                  scheduledAt: '2026-12-01 10:00:00'
                  postAsStory: false
                  media:
                    - id: media_123
                  platforms:
                    instagram:
                      collab: example_collaborator_username
                      location:
                        id: example_location_id
                        name: Example Location
                      trialReel: MANUAL
                      shareToFeed: true
                    facebook:
                      location:
                        id: example_location_id
                        name: Example Location
                    tiktok:
                      sendToInbox: true
                      autoAddMusic: false
                    youtube:
                      tags: [example, social media]
                    googlemybusiness:
                      uploadToPhotosSection: true
                  comment:
                    delay: 60
                    comment: Example automated comment text.
      responses:
        '201':
          description: Post created
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostCreateResponse'
        '400':
          description: Invalid business-state or downstream media/feed checks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                scheduledTooSoon:
                  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
                invalidMediaDownload:
                  value:
                    status: error
                    message: Failed to download media from url
                    errors:
                      media: Failed to download media from url
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          description: Plan/limit restriction
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
              examples:
                queueLimit:
                  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
                maxPostsBrand:
                  value:
                    status: error
                    message: This brand has reached the maximum total number of posts allowed (15,000).
                    errors:
                      limits: This brand has reached the maximum total number of posts allowed (15,000).
        '404':
          description: Brand/collection/channel/media not found or inaccessible
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Token

  headers:
    XRateLimitLimit:
      description: Maximum requests allowed in the current rate-limit window.
      schema:
        type: integer
        example: 30
    XRateLimitRemaining:
      description: Remaining requests in current rate-limit window.
      schema:
        type: integer
        example: 29
    XRateLimitReset:
      description: Seconds until window reset.
      schema:
        type: integer
        example: 60

  parameters:
    BrandId:
      name: brand_id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
    CollectionId:
      name: collection_id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
    Page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPage:
      name: per_page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25

  responses:
    UnauthorizedError:
      description: Missing, invalid, or expired bearer token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          examples:
            tokenRequired:
              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

    ValidationError:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'
          examples:
            invalidPerPage:
              value:
                message: The given data was invalid.
                errors:
                  per_page:
                    - The per page must be at least 1.

    RateLimitError:
      description: Too many requests
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitErrorResponse'
          examples:
            exceeded:
              value:
                message: Too many requests. Please try again later.
                errors:
                  rate_limit: Too many requests. Please try again later.

    InternalServerError:
      description: Internal server error (normalized by middleware)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          examples:
            generic:
              value:
                status: error
                message: Internal Server Error
                errors:
                  server: Internal Server Error

  schemas:
    Pagination:
      type: object
      required: [currentPage, perPage, total, lastPage, nextPageUrl, prevPageUrl]
      properties:
        currentPage:
          type: integer
        perPage:
          type: integer
        total:
          type: integer
        lastPage:
          type: integer
        nextPageUrl:
          type: [string, 'null']
          format: uri
        prevPageUrl:
          type: [string, 'null']
          format: uri

    ApiErrorResponse:
      type: object
      required: [status, message, errors]
      properties:
        status:
          type: string
          const: error
        message:
          type: string
        errors:
          type: object
          additionalProperties:
            type: string

    ValidationErrorResponse:
      type: object
      required: [message, errors]
      properties:
        message:
          type: string
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string

    RateLimitErrorResponse:
      type: object
      required: [message, errors]
      properties:
        message:
          type: string
        errors:
          type: object
          properties:
            rate_limit:
              type: string

    Me:
      type: object
      required: [id, name, joinedAt, timezone]
      properties:
        id:
          type: string
          description: External hashed identifier derived from internal user id.
        name:
          type: string
        joinedAt:
          type: string
          format: date-time
        timezone:
          type: string

    MeResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          const: success
        data:
          $ref: '#/components/schemas/Me'

    Brand:
      type: object
      required: [id, title, description, timezone, createdAt, updatedAt]
      properties:
        id:
          type: integer
        title:
          type: string
        description:
          type: [string, 'null']
        timezone:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    BrandsListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          const: success
        data:
          type: array
          items:
            $ref: '#/components/schemas/Brand'
        pagination:
          $ref: '#/components/schemas/Pagination'

    Channel:
      type: object
      required: [id, name, status, type, createdAt, updatedAt]
      properties:
        id:
          type: integer
        name:
          type: string
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
        type:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ChannelsListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          const: success
        data:
          type: array
          items:
            $ref: '#/components/schemas/Channel'
        pagination:
          $ref: '#/components/schemas/Pagination'

    CollectionChannel:
      type: object
      required: [id, name, status, type]
      properties:
        id:
          type: integer
        name:
          type: string
        status:
          type: string
          enum: [ACTIVE, INACTIVE]
        type:
          type: string

    Collection:
      type: object
      required: [id, title, description, createdAt, updatedAt, channels]
      properties:
        id:
          type: integer
        title:
          type: string
        description:
          type: [string, 'null']
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        channels:
          type: array
          items:
            $ref: '#/components/schemas/CollectionChannel'

    QueueItem:
      type: object
      required: [id, date, status]
      properties:
        id:
          type: integer
        date:
          type: string
          description: Weekly queue slot in format `DDD HH:mm` uppercased by server.
          pattern: '^[A-Z]{3} (?:[01][0-9]|2[0-3]):[0-5][0-9]$'
        status:
          type: string
          enum: [PENDING, PUBLISHED]

    CollectionsListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          const: success
        data:
          type: array
          items:
            $ref: '#/components/schemas/Collection'
        pagination:
          $ref: '#/components/schemas/Pagination'

    CreateCollectionRequest:
      type: object
      required: [title]
      properties:
        title:
          type: string
          maxLength: 255
        description:
          type: [string, 'null']
          maxLength: 1000
        maxRepublish:
          type: integer
          minimum: 0
        channels:
          type: array
          items:
            type: integer
            minimum: 1
        queues:
          type: array
          items:
            type: string
            description: Queue slot format.
            pattern: '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:[01][0-9]|2[0-3]):[0-5][0-9]$'

    CollectionCreateResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          const: success
        data:
          type: object
          required: [id, title, description, maxRepublish, createdAt, updatedAt, channels, queues]
          properties:
            id:
              type: integer
            title:
              type: string
            description:
              type: [string, 'null']
            maxRepublish:
              type: integer
            createdAt:
              type: string
              format: date-time
            updatedAt:
              type: string
              format: date-time
            channels:
              type: array
              items:
                $ref: '#/components/schemas/CollectionChannel'
            queues:
              type: array
              items:
                $ref: '#/components/schemas/QueueItem'

    Automation:
      type: object
      required: [id, title, description, collectionId, status, subType, feedUrl, runCount, importAsType, lastRun, dynamicTitle, dynamicBody, createdAt, updatedAt]
      properties:
        id:
          type: integer
        title:
          type: string
        description:
          type: [string, 'null']
        collectionId:
          type: integer
        status:
          type: [string, 'null']
        subType:
          type: string
        feedUrl:
          type: string
          format: uri
        runCount:
          type: [integer, 'null']
        importAsType:
          type: string
          enum: [LINK, IMAGE, VIDEO, CAROUSEL]
        lastRun:
          type: integer
          description: Unix timestamp
        dynamicTitle:
          type: [string, 'null']
        dynamicBody:
          type: [string, 'null']
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    AutomationsListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          const: success
        data:
          type: array
          items:
            $ref: '#/components/schemas/Automation'
        pagination:
          $ref: '#/components/schemas/Pagination'

    CreateAutomationRequest:
      type: object
      required: [title, collectionId, type, subType, feedUrl, importAsType]
      properties:
        title:
          type: string
          maxLength: 255
        description:
          type: [string, 'null']
        collectionId:
          type: integer
          minimum: 1
        type:
          type: string
          enum: [FEED]
        subType:
          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
        feedUrl:
          type: string
          format: uri
        importAsType:
          type: string
          enum: [LINK, IMAGE, VIDEO, CAROUSEL]
        dynamicTitle:
          type: [string, 'null']
          maxLength: 255
        dynamicBody:
          type: [string, 'null']
          maxLength: 255
        loadOldPosts:
          type: boolean
        addPostsAsDraft:
          type: boolean
        refreshRate:
          type: integer
          enum: [24, 12, 6, 1]

    AutomationCreateResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          const: success
        data:
          $ref: '#/components/schemas/Automation'

    MediaItem:
      type: object
      required: [id, name, type, size, createdAt, updatedAt]
      properties:
        id:
          description: Opaque media identifier accepted by post creation as media[].id.
          type: string
        name:
          type: string
        type:
          type: string
        size:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    MediaListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          const: success
        data:
          type: array
          items:
            $ref: '#/components/schemas/MediaItem'
        pagination:
          $ref: '#/components/schemas/Pagination'

    MediaUploadResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          const: success
        data:
          type: object
          required: [id, type, size]
          properties:
            id:
              type: string
              description: Opaque media identifier accepted by post creation as media[].id.
            type:
              type: string
            size:
              type: integer

    PostMediaReference:
      type: object
      required: [id]
      properties:
        id:
          type: string

    PollPayload:
      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, 'null']
          minimum: 0
        explanation:
          type: [string, 'null']
          maxLength: 512
        period:
          type: string
          enum: [1d, 3d, 7d]

    PostListItem:
      type: object
      required: [id, collectionId, title, body, media, postType, postingType, status, postDate, createdAt, updatedAt]
      properties:
        id:
          type: integer
        collectionId:
          type: integer
        title:
          type: [string, 'null']
        body:
          type: [string, 'null']
        media:
          type: array
          items:
            $ref: '#/components/schemas/PostMediaReference'
        postType:
          type: string
        postingType:
          type: string
        status:
          type: string
        postDate:
          type: [string, 'null']
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        poll:
          type: object
          properties:
            caption:
              type: [string, 'null']
            question:
              type: [string, 'null']
            options:
              type: [array, 'null']
              items:
                type: string
            period:
              type: [string, 'null']
              enum: [1d, 3d, 7d]
            correctOptionIndex:
              type: [integer, 'null']
            explanation:
              type: [string, 'null']

    PostsListResponse:
      type: object
      required: [status, data, pagination]
      properties:
        status:
          type: string
          const: success
        data:
          type: array
          items:
            $ref: '#/components/schemas/PostListItem'
        pagination:
          $ref: '#/components/schemas/Pagination'

    MediaInputItem:
      oneOf:
        - type: object
          required: [id]
          properties:
            id:
              type: string
          additionalProperties: false
        - type: object
          required: [url]
          properties:
            url:
              type: string
              format: uri
              maxLength: 1000
          additionalProperties: false

    InstagramLocation:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
        name:
          type: string

    YoutubePlaylistInput:
      type: object
      required: [channelId, playlistIds]
      properties:
        channelId:
          type: integer
          minimum: 1
        playlistIds:
          type: array
          items:
            type: string

    CreatePostRequest:
      type: object
      required: [publishMode]
      properties:
        title:
          type: [string, 'null']
          maxLength: 255
        caption:
          type: [string, 'null']
          minLength: 1
          maxLength: 3000
          description: Required when `poll` is not provided.
        poll:
          $ref: '#/components/schemas/PollPayload'
        alt:
          type: [string, 'null']
          maxLength: 255
        link:
          type: [string, 'null']
          format: uri
          maxLength: 1000
        publishMode:
          type: string
          enum: [IMMEDIATE, SCHEDULE, QUEUE, DRAFT]
        scheduledAt:
          type: [string, 'null']
          description: Required when `publishMode=SCHEDULE`; format `Y-m-d H:i:s`.
          pattern: '^\d{4}-\d{2}-\d{2} (?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$'
        media:
          type: array
          items:
            $ref: '#/components/schemas/MediaInputItem'
        postAsStory:
          type: boolean
        platforms:
          type: object
          properties:
            instagram:
              type: object
              properties:
                collab:
                  type: [string, 'null']
                  maxLength: 1000
                location:
                  $ref: '#/components/schemas/InstagramLocation'
                trialReel:
                  type: [string, 'null']
                  enum: [MANUAL, SS_PERFORMANCE]
                shareToFeed:
                  type: boolean
            facebook:
              type: object
              properties:
                location:
                  $ref: '#/components/schemas/InstagramLocation'
            tiktok:
              type: object
              properties:
                sendToInbox:
                  type: boolean
                autoAddMusic:
                  type: boolean
            youtube:
              type: object
              properties:
                tags:
                  type: array
                  items:
                    type: string
                    maxLength: 255
                playlists:
                  type: array
                  items:
                    $ref: '#/components/schemas/YoutubePlaylistInput'
            googlemybusiness:
              type: object
              properties:
                uploadToPhotosSection:
                  type: boolean
        comment:
          type: object
          properties:
            delay:
              type: integer
              minimum: 0
            comment:
              type: string
              maxLength: 1000

    PostCreateResponse:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          const: success
        data:
          type: object
          required: [id, message]
          properties:
            id:
              type: integer
            message:
              type: string
              const: Post created successfully
