openapi: 3.1.0
info:
  title: DoroSocial Public API
  version: 2026-07-04
  summary: First-party API for paid-user automation, AI agents, scripts, integrations, and MCP clients.
  description: |
    This is the supported customer-facing API contract. It intentionally includes only
    `/api/v1/public/*` routes and uses DoroSocial API tokens.

    Do not use the internal FastAPI `/api/v1/openapi.json` output as the public API
    contract. Internal browser/admin routes can change without notice.
servers:
  - url: https://doro.social
    description: Production
  - url: http://localhost:8000
    description: Local development
security:
  - ApiTokenAuth: []
tags:
  - name: Context
    description: Brand and account context available to the token owner.
  - name: Content
    description: Source content items used to create posts.
  - name: Media
    description: Multipart media upload flow.
  - name: Posts
    description: Public post creation, scheduling, listing, and status.
paths:
  /api/v1/public/context:
    get:
      tags: [Context]
      summary: Read default account and brand context.
      description: Requires `brand:read`.
      security:
        - ApiTokenAuth: []
      responses:
        "200":
          description: Token owner context.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicContext"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/public/brands:
    get:
      tags: [Context]
      summary: List brands available to the token owner.
      description: Requires `brand:read`.
      security:
        - ApiTokenAuth: []
      responses:
        "200":
          description: Available brands.
          content:
            application/json:
              schema:
                type: object
                required: [brands]
                properties:
                  brands:
                    type: array
                    items:
                      $ref: "#/components/schemas/Brand"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/public/content:
    post:
      tags: [Content]
      summary: Create a content item.
      description: |
        Requires `content:create`.

        If the account owns one active brand, `brand_id` may be omitted. If the
        account owns multiple active brands, pass `brand_id`.
      security:
        - ApiTokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateContentRequest"
            example:
              brand_id: 42
              title: Summer launch announcement
              content_type: TEXT
              status: DRAFT
              description: Short-form campaign copy for the launch.
              external_links:
                landing_page: https://example.com/summer
      responses:
        "200":
          description: Created content item.
          content:
            application/json:
              schema:
                type: object
                required: [content]
                properties:
                  content:
                    $ref: "#/components/schemas/Content"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"
  /api/v1/public/media/uploads/multipart/start:
    post:
      tags: [Media]
      summary: Start a multipart media upload.
      description: Requires `media:upload`.
      security:
        - ApiTokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StartMultipartUploadRequest"
      responses:
        "200":
          description: Upload session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MultipartStartResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/public/media/uploads/multipart/sign-parts:
    post:
      tags: [Media]
      summary: Sign multipart upload parts.
      description: Requires `media:upload`.
      security:
        - ApiTokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SignMultipartPartsRequest"
      responses:
        "200":
          description: Signed upload part URLs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MultipartSignPartsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/public/media/uploads/multipart/complete:
    post:
      tags: [Media]
      summary: Complete a multipart upload and attach media to content.
      description: Requires `media:upload` and `content:create`.
      security:
        - ApiTokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CompleteMultipartUploadRequest"
      responses:
        "200":
          description: Content item with uploaded media attached.
          content:
            application/json:
              schema:
                type: object
                required: [content]
                properties:
                  content:
                    $ref: "#/components/schemas/Content"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/public/posts:
    post:
      tags: [Posts]
      summary: Create a draft or scheduled post.
      description: |
        Requires `posts:create`. If `scheduled_for` is present, also requires
        `posts:schedule`.

        Public API language uses "post"; internally DoroSocial may still store this
        as a deployment.
      security:
        - ApiTokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePostRequest"
            examples:
              draft:
                summary: Draft LinkedIn post
                value:
                  brand_id: 42
                  content_id: 1001
                  platform: LI
                  text: Launch copy for LinkedIn.
              scheduledX:
                summary: Scheduled X post using a selected account
                value:
                  brand_id: 42
                  content_id: 1002
                  platform: TW
                  text: Launch copy for X.
                  scheduled_for: "2026-07-08T16:00:00Z"
                  account_ids:
                    x_account_id: 55
      responses:
        "200":
          description: Created post.
          content:
            application/json:
              schema:
                type: object
                required: [post]
                properties:
                  post:
                    $ref: "#/components/schemas/Post"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"
    get:
      tags: [Posts]
      summary: List posts.
      description: Requires `posts:read`.
      security:
        - ApiTokenAuth: []
      parameters:
        - $ref: "#/components/parameters/BrandId"
        - name: content_id
          in: query
          schema:
            type: integer
        - name: platform
          in: query
          schema:
            $ref: "#/components/schemas/Platform"
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/PostStatus"
        - name: skip
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: Posts.
          content:
            application/json:
              schema:
                type: object
                required: [posts, total, page, size, pages]
                properties:
                  posts:
                    type: array
                    items:
                      $ref: "#/components/schemas/Post"
                  total:
                    type: integer
                  page:
                    type: integer
                  size:
                    type: integer
                  pages:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/public/posts/{post_id}:
    get:
      tags: [Posts]
      summary: Read one post.
      description: Requires `posts:read`.
      security:
        - ApiTokenAuth: []
      parameters:
        - $ref: "#/components/parameters/PostId"
        - $ref: "#/components/parameters/BrandId"
      responses:
        "200":
          description: Post.
          content:
            application/json:
              schema:
                type: object
                required: [post]
                properties:
                  post:
                    $ref: "#/components/schemas/Post"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/public/posts/{post_id}/status:
    get:
      tags: [Posts]
      summary: Read post publishing status.
      description: Requires `posts:read`.
      security:
        - ApiTokenAuth: []
      parameters:
        - $ref: "#/components/parameters/PostId"
        - $ref: "#/components/parameters/BrandId"
      responses:
        "200":
          description: Post status.
          content:
            application/json:
              schema:
                type: object
                required: [post]
                properties:
                  post:
                    $ref: "#/components/schemas/Post"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
components:
  securitySchemes:
    ApiTokenAuth:
      type: http
      scheme: bearer
      bearerFormat: DoroSocial API token
      description: "API tokens are created in DoroSocial Settings > API tokens, shown only once, and passed in the `Authorization: Bearer <token>` header."
  parameters:
    BrandId:
      name: brand_id
      in: query
      required: false
      schema:
        type: integer
      description: Required when the token owner has more than one active brand.
    PostId:
      name: post_id
      in: path
      required: true
      schema:
        type: integer
  responses:
    Unauthorized:
      description: Missing, invalid, revoked, or expired API token.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            detail:
              code: api_token_invalid
              message: API token is invalid.
            status_code: 401
    Forbidden:
      description: Scope, plan, verification, brand ownership, platform, scheduling, privacy, media, or rate-limit rule failed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            scope:
              value:
                success: false
                detail:
                  code: api_scope_required
                  required_scope: posts:schedule
                  message: This API token does not include the required scope.
                status_code: 403
            brand:
              value:
                success: false
                detail:
                  code: brand_required
                  message: This account has multiple brands. Pass brand_id.
                  brands:
                    - id: 42
                      name: DevPro One
                status_code: 403
            limit:
              value:
                success: false
                detail:
                  code: daily_channel_post_limit_reached
                  platform: TW
                  channel_key: x:55
                  message: This channel has reached its daily posting limit.
                status_code: 403
    NotFound:
      description: Resource does not exist or does not belong to the token owner and brand.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ValidationError:
      description: Invalid request body or query parameters.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    Brand:
      type: object
      required: [id, name, is_active]
      properties:
        id:
          type: integer
        name:
          type: string
        is_active:
          type: boolean
    Account:
      type: object
      required: [id, email]
      properties:
        id:
          type: integer
        email:
          type: string
          format: email
    PublicContext:
      type: object
      required: [account, brands, default_brand_id, scopes]
      properties:
        account:
          $ref: "#/components/schemas/Account"
        default_brand_id:
          type: integer
          nullable: true
        brands:
          type: array
          items:
            $ref: "#/components/schemas/Brand"
        scopes:
          type: array
          items:
            type: string
    Content:
      type: object
      required: [id, brand_id, title, content_type, status]
      properties:
        id:
          type: integer
        brand_id:
          type: integer
        title:
          type: string
        content_type:
          type: string
          enum: [TEXT, IMAGE, VIDEO, CAROUSEL]
        status:
          type: string
        created_at:
          type: string
          format: date-time
          nullable: true
    Post:
      type: object
      required: [id, brand_id, content_id, platform, status]
      properties:
        id:
          type: integer
        brand_id:
          type: integer
          nullable: true
        content_id:
          type: integer
        platform:
          $ref: "#/components/schemas/Platform"
        status:
          $ref: "#/components/schemas/PostStatus"
        title:
          type: string
          nullable: true
        text:
          type: string
          nullable: true
        scheduled_for:
          type: string
          format: date-time
          nullable: true
        published_at:
          type: string
          format: date-time
          nullable: true
        external_url:
          type: string
          nullable: true
        error:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
    Platform:
      type: string
      enum:
        - YT_LONG
        - YT_SHORT
        - TT
        - IG_FEED
        - IG_REEL
        - IG_CAROUSEL
        - IG_STORY
        - LI
        - FB
        - THREADS
        - TW
        - BSKY
        - PIN
    PostStatus:
      type: string
      enum: [draft, scheduled, publishing, published, failed]
    CreateContentRequest:
      type: object
      required: [title, content_type]
      properties:
        brand_id:
          type: integer
        title:
          type: string
          minLength: 1
          maxLength: 500
        content_type:
          type: string
          maxLength: 50
          enum: [TEXT, IMAGE, VIDEO, CAROUSEL]
        status:
          type: string
          maxLength: 50
          default: DRAFT
        description:
          type: string
        external_links:
          oneOf:
            - type: object
              additionalProperties:
                type: string
            - type: array
              items:
                type: object
                additionalProperties:
                  type: string
        tags:
          type: array
          maxItems: 20
          items:
            type: string
        master_asset_path:
          type: string
          maxLength: 500
        hashtags:
          type: array
          items:
            type: string
        duration_in_seconds:
          type: integer
          minimum: 0
        orientation:
          type: string
          maxLength: 20
    CreatePostRequest:
      type: object
      required: [content_id, platform]
      properties:
        brand_id:
          type: integer
        content_id:
          type: integer
        platform:
          $ref: "#/components/schemas/Platform"
        title:
          type: string
          maxLength: 500
        text:
          type: string
        status:
          type: string
          maxLength: 50
          description: Omit this. Draft requests create `DRAFT`; scheduled requests create `SCHEDULED`.
        scheduled_for:
          type: string
          format: date-time
        privacy:
          type: string
          maxLength: 64
        account_ids:
          type: object
          description: Connected account IDs, such as `x_account_id` or `youtube_account_id`.
          additionalProperties:
            type: integer
        pinned_comment:
          type: string
        youtube_tags:
          type: string
        hashtags:
          type: array
          items:
            type: string
    StartMultipartUploadRequest:
      type: object
      required: [filename, file_size_bytes]
      properties:
        filename:
          type: string
        file_size_bytes:
          type: integer
          minimum: 1
        content_type:
          type: string
          nullable: true
        brand_id:
          type: integer
        content_id:
          type: integer
        part_size_bytes:
          type: integer
          minimum: 1
        expected_parts:
          type: integer
          minimum: 1
    SignMultipartPartsRequest:
      type: object
      required: [object_key, upload_id, session_token, part_numbers]
      properties:
        object_key:
          type: string
        upload_id:
          type: string
        session_token:
          type: string
        part_numbers:
          type: array
          items:
            type: integer
    MultipartStartResponse:
      type: object
      required: [object_key, upload_id, session_token, part_size_bytes, max_parts]
      properties:
        object_key:
          type: string
        upload_id:
          type: string
        session_token:
          type: string
        part_size_bytes:
          type: integer
        max_parts:
          type: integer
    MultipartSignPartsResponse:
      type: object
      required: [urls]
      properties:
        urls:
          type: object
          additionalProperties:
            type: string
    CompleteMultipartUploadRequest:
      type: object
      required: [object_key, upload_id, session_token, parts, title, content_type]
      properties:
        object_key:
          type: string
        upload_id:
          type: string
        session_token:
          type: string
        parts:
          type: array
          items:
            type: object
            required: [part_number, etag]
            properties:
              part_number:
                type: integer
              etag:
                type: string
        title:
          type: string
          minLength: 1
          maxLength: 500
        content_type:
          type: string
        status:
          type: string
          default: DRAFT
        brand_id:
          type: integer
        content_id:
          type: integer
        orientation:
          type: string
        creation_method:
          type: string
        duration_in_seconds:
          type: integer
          minimum: 0
        external_links:
          type: array
          items:
            type: object
            additionalProperties:
              type: string
        tags:
          type: array
          items:
            type: string
        auto_transcribe:
          type: boolean
          default: false
    ErrorResponse:
      type: object
      required: [success, detail, status_code]
      properties:
        success:
          type: boolean
          const: false
        detail:
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
        status_code:
          type: integer
