openapi: 3.1.0
info:
  title: VidSyndicate Public API
  version: 1.0.0
  description: Create and distribute social posts through channels connected to an authenticated VidSyndicate account.
servers:
  - url: https://vidsyndicate.com
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Account
  - name: Channels
  - name: Campaigns
paths:
  /api/v1/me:
    get:
      tags: [Account]
      operationId: getCurrentAccount
      summary: Get the authenticated VidSyndicate account
      description: Verifies the API key and returns basic account and plan information.
      responses:
        '200':
          description: Authenticated account information.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/Account'
              example:
                data:
                  id: 42
                  name: Example Creator
                  email: creator@example.com
                  api_access: true
                  plan: Starter
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/capabilities:
    get:
      tags: [Account]
      operationId: getAccountCapabilities
      summary: Get account API capabilities and limits
      description: Returns publishing modes, connected platforms, generation availability, current post usage, and the API request limit for the authenticated account.
      responses:
        '200':
          description: Current account capabilities.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/Capabilities'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/connected-channels:
    get:
      tags: [Channels]
      operationId: listConnectedChannels
      summary: List connected publishing channels
      description: Returns only channels owned by the authenticated user in their current VidSyndicate workspace. Use the returned numeric IDs in campaign channel_ids.
      responses:
        '200':
          description: Connected channels for the authenticated account.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConnectedChannel'
              example:
                data:
                  - id: 44
                    platform: pinterest
                    name: Example Pinterest Account
                    handle: example
                    status: connected
                    connected_at: '2026-08-17T14:30:00+00:00'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/connected-channels/{id}/boards:
    get:
      tags: [Channels]
      operationId: listPinterestBoards
      summary: List boards for a connected Pinterest channel
      description: Returns Pinterest boards for a Pinterest channel owned by the authenticated account. Use a returned board ID as pinterest_board_id when publishing to Pinterest.
      parameters:
        - name: id
          in: path
          required: true
          description: Numeric Pinterest channel ID returned by listConnectedChannels.
          schema:
            type: integer
            minimum: 1
          example: 44
      responses:
        '200':
          description: Pinterest boards for the selected connected channel.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PinterestBoard'
              example:
                data:
                  - id: '123456789012345678'
                    name: Theme Parks
                    privacy: PUBLIC
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Pinterest must be reconnected before boards can be listed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/campaigns:
    post:
      tags: [Campaigns]
      operationId: createCampaign
      summary: Create a draft, immediate, or scheduled social campaign
      description: Creates a post in the authenticated account. Non-draft posts require at least one owned connected channel and either an uploaded image or generate_image set to true. Pinterest publishing also requires a verified board ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignJsonInput'
            examples:
              draft:
                summary: Draft for review
                value:
                  title: API draft for review
                  content: Review this post in VidSyndicate.
                  link_url: https://example.com/article
                  publish_mode: draft
                  generate_ai_content: false
              scheduled:
                summary: Scheduled post with a generated image
                value:
                  title: Scheduled post
                  content: Publish this at the requested time.
                  link_url: https://example.com/article
                  publish_mode: scheduled
                  scheduled_at: '2030-08-20T14:00:00-04:00'
                  channel_ids: [44]
                  pinterest_board_id: '123456789012345678'
                  generate_ai_content: false
                  generate_image: true
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CampaignMultipartInput'
      responses:
        '201':
          description: Campaign created.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/Campaign'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          description: Uploaded request exceeds the server request-size limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: VidSyndicate API key
      description: API key generated in Dashboard > Account > API Access. Keys begin with vsk_ and are shown in full only when created.
  schemas:
    Account:
      type: object
      required: [id, name, email, api_access, plan]
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
        api_access:
          type: boolean
        plan:
          type: string
    ConnectedChannel:
      type: object
      required: [id, platform, name, status]
      properties:
        id:
          type: integer
        platform:
          type: string
        name:
          type: string
        handle:
          type: [string, 'null']
        status:
          type: string
          enum: [connected, expired]
        connected_at:
          type: [string, 'null']
          format: date-time
    PinterestBoard:
      type: object
      required: [id, name, privacy]
      properties:
        id:
          type: string
        name:
          type: string
        privacy:
          type: string
    Capabilities:
      type: object
      required: [plan, publishing_modes, connected_platforms, features, post_usage, rate_limit]
      properties:
        plan:
          type: object
          required: [name, key]
          properties:
            name:
              type: string
            key:
              type: string
              enum: [free, starter, pro, studio]
        publishing_modes:
          type: array
          items:
            type: string
            enum: [draft, publish_now, scheduled]
        connected_platforms:
          type: array
          items:
            type: string
        features:
          type: object
          required: [content_generation, image_generation, image_upload, pinterest_boards]
          properties:
            content_generation:
              type: boolean
            image_generation:
              type: boolean
            image_upload:
              type: boolean
            pinterest_boards:
              type: boolean
        post_usage:
          type: object
          required: [used, limit, remaining, scope]
          properties:
            used:
              type: integer
              minimum: 0
            limit:
              type: [integer, 'null']
              minimum: 0
            remaining:
              type: [integer, 'null']
              minimum: 0
            scope:
              type: string
              enum: [stored, user, studio]
        rate_limit:
          type: object
          required: [requests, period_seconds]
          properties:
            requests:
              type: integer
              minimum: 1
            period_seconds:
              type: integer
              const: 60
    CampaignJsonInput:
      type: object
      required: [title, content, publish_mode]
      properties:
        title:
          type: string
          maxLength: 255
        content:
          type: string
          maxLength: 100000
        excerpt:
          type: [string, 'null']
          maxLength: 5000
        source_url:
          type: [string, 'null']
          format: uri
          maxLength: 2048
          description: Backward-compatible alias used as link_url when link_url is omitted.
        link_url:
          type: [string, 'null']
          format: uri
          maxLength: 2048
          description: HTTP or HTTPS source/CTA URL. It is stored and appended to content once when not already present.
        generate_image:
          type: boolean
          default: false
        publish_mode:
          type: string
          enum: [draft, publish_now, scheduled]
        scheduled_at:
          type: [string, 'null']
          format: date-time
          description: Required and must be in the future when publish_mode is scheduled.
        channel_ids:
          type: array
          uniqueItems: true
          items:
            type: integer
          description: Connected channel IDs owned by the authenticated user. At least one is required for non-draft publishing.
        generate_ai_content:
          type: boolean
          default: false
        pinterest_board_id:
          type: [string, 'null']
          maxLength: 100
        pinterest_title:
          type: [string, 'null']
          maxLength: 100
        pinterest_description:
          type: [string, 'null']
          maxLength: 500
        pinterest_link:
          type: [string, 'null']
          format: uri
          maxLength: 2048
        pinterest_alt_text:
          type: [string, 'null']
          maxLength: 500
    CampaignMultipartInput:
      allOf:
        - $ref: '#/components/schemas/CampaignJsonInput'
        - type: object
          properties:
            image:
              type: string
              format: binary
              description: JPG, JPEG, PNG, or WebP image up to 10 MB. Cannot be combined with generate_image=true.
    Campaign:
      type: object
      required: [id, title, content, status, media, channels]
      properties:
        id:
          type: integer
        title:
          type: string
        content:
          type: string
        link_url:
          type: [string, 'null']
          format: uri
        status:
          type: string
          enum: [draft, scheduled]
          description: A publish_now request enters the existing publishing pipeline with scheduled status.
        scheduled_at:
          type: [string, 'null']
          format: date-time
        media:
          type: object
          required: [has_image, has_video]
          properties:
            has_image:
              type: boolean
            has_video:
              type: boolean
        pinterest_board_id:
          type: [string, 'null']
        channels:
          type: array
          items:
            type: object
            required: [id, platform, status]
            properties:
              id:
                type: integer
              platform:
                type: string
              status:
                type: string
                enum: [draft, scheduled]
        created_at:
          type: [string, 'null']
          format: date-time
    Error:
      type: object
      required: [message]
      properties:
        message:
          type: string
    ValidationError:
      type: object
      required: [message, errors]
      properties:
        message:
          type: string
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
  responses:
    Unauthenticated:
      description: API key is missing, invalid, revoked, or regenerated.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Unauthenticated.
    Forbidden:
      description: The authenticated account is not authorized to use the requested API capability.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: The selected resource was not found for the authenticated account.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ValidationError:
      description: Request validation, ownership validation, plan limits, or Pinterest requirements prevented the operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError'
    RateLimited:
      description: The API key exceeded its plan-based per-minute request limit.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until another request should be attempted.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
