# This file is rendered from src/openapi/spec.ts (the single source of truth)
# via src/openapi/to-yaml.ts. Do not hand-edit — regenerate with:
#   node scripts/generate-openapi-yaml.mjs
# after changing src/openapi/spec.ts. The JSON served by GET /api/openapi and
# the YAML served by GET /api/openapi?format=yaml come from the same
# in-memory object at request time, so they can never drift from this file.
openapi: 3.1.0
info:
  title: OneSocial API
  version: 1.0.0
  summary: "Schedule and publish social posts across Bluesky, Telegram, Discord, and Mastodon, plus analytics, inbox, contacts, broadcasts, sequences, automations, and a paid Ads Manager across Meta, Google, TikTok, LinkedIn, Pinterest, and X."
  description: "One REST API (and a parity MCP server at /api/mcp) to connect social accounts, schedule/publish posts, track analytics, manage a unified inbox (comments/mentions/reviews/DMs), maintain a contacts CRM with custom fields, run broadcasts/sequences/comment-to-DM automations, and run a paid Ads Manager (ad accounts, campaigns, ads, audiences, tracking tags) across Meta, Google, TikTok, LinkedIn, Pinterest, and X. All endpoints are versioned under /api/v1 (except the D1 media file server at /api/v1/media/file/{tenantId}/{filename}, which is a plain unauthenticated static-file GET), authenticate via an `sk_`-prefixed API key as an HTTP Bearer token, and respond with the envelope `{ data }` for single resources or `{ data, page, limit, total }` for lists. Errors are `{ error: { code, message } }` with a matching HTTP status. Scopes are a closed allowlist (see `@/lib/scopes`): a key needs the exact scope string for an action (or `*`/`admin` for full access) — an empty scopes array grants nothing. A handful of read endpoints (comments, mentions, reviews, broadcasts, sequences, automations lists) do not yet enforce their `:read` scope — any authenticated key can call them; this is called out per-operation below. Ad-network credentials are entirely optional server-side config (`<NETWORK>_ADS_*` env vars); every Ads Manager endpoint is fully exercisable with zero ad-network credentials configured — unconfigured live calls fail closed and the local DB state is persisted regardless (see the AdAccounts/Ads/AdCampaigns/AdAudiences/TrackingTags operations below for specifics)."
  license:
    name: Proprietary
servers:
  - url: /api
    description: Relative to whichever origin this spec was fetched from.
  - url: "http://localhost:3000/api"
    description: Local development.
security:
  - bearerAuth: []
tags:
  - name: Profiles
    description: Per-tenant containers for accounts and queue slots.
  - name: Accounts
    description: Connected social accounts and account groups.
  - name: Connect
    description: OAuth and direct-credential flows for connecting an account to a platform.
  - name: Posts
    description: "Create, schedule, publish, and retry posts across platforms."
  - name: Queue
    description: Recurring auto-schedule slots.
  - name: Media
    description: Pass-through media storage (no transcoding).
  - name: Webhooks
    description: Outbound event subscriptions and delivery history.
  - name: ApiKeys
    description: sk_ API key lifecycle.
  - name: User
    description: The currently authenticated actor.
  - name: Usage
    description: Per-tenant usage counters.
  - name: Analytics
    description: "Per-day post/account metric rollups, daily series, and best-time-to-post suggestions."
  - name: Comments
    description: "Inbound comments on connected accounts' native posts, and replies to them."
  - name: Mentions
    description: "Inbound @-mentions across connected accounts."
  - name: Reviews
    description: Platform reviews (e.g. Google Business style) and replies to them.
  - name: Messages
    description: DM conversations and messages across connected accounts.
  - name: Contacts
    description: "CRM contact records, their channels, and bulk import."
  - name: CustomFields
    description: Tenant-defined custom field schemas for Contact.customFields.
  - name: Broadcasts
    description: One-shot campaign messages sent to many contacts.
  - name: Sequences
    description: Multi-step drip campaigns with delay/message/broadcast steps.
  - name: Automations
    description: Comment-to-DM automation rules and their trigger logs.
  - name: AdAccounts
    description: "Per-network connected ad accounts (Meta, Google, TikTok, LinkedIn, Pinterest, X)."
  - name: AdCampaigns
    description: "Ad campaigns: objective, budget, and pause/resume state."
  - name: Ads
    description: "Individual ads, including boosted posts, lead forms, targeting search, and conversion forwarding."
  - name: AdAudiences
    description: Custom/lookalike/saved audiences and hashed-member uploads.
  - name: TrackingTags
    description: "Pixels, Conversions API tags, and gtag tags, plus their recorded events."
paths:
  "/v1/profiles":
    get:
      tags:
        - Profiles
      summary: List profiles
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Paginated list of profiles.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Profile"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
    post:
      tags:
        - Profiles
      summary: Create a profile
      description: "Requires scope `profiles:write`."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                description:
                  type:
                    - string
                    - "null"
                  maxLength: 2000
      responses:
        "201":
          description: Profile created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Profile"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/profiles/{id}":
    get:
      tags:
        - Profiles
      summary: Get a profile
      parameters:
        - name: id
          in: path
          required: true
          description: Profile id.
          schema:
            type: string
      responses:
        "200":
          description: The profile.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Profile"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Profiles
      summary: Update a profile
      description: "Requires scope `profiles:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Profile id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                description:
                  type:
                    - string
                    - "null"
                  maxLength: 2000
      responses:
        "200":
          description: Updated profile.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Profile"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - Profiles
      summary: Delete a profile
      description: "Requires scope `profiles:write`. Cascades to the profile's accounts and queue slots."
      parameters:
        - name: id
          in: path
          required: true
          description: Profile id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/accounts":
    get:
      tags:
        - Accounts
      summary: List accounts
      description: "Accounts are created via the connect flow, not directly."
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: profileId
          in: query
          schema:
            type: string
        - name: platform
          in: query
          schema:
            "$ref": "#/components/schemas/Platform"
        - name: status
          in: query
          schema:
            "$ref": "#/components/schemas/AccountStatus"
      responses:
        "200":
          description: Paginated list of accounts.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Account"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
  "/v1/accounts/{id}":
    get:
      tags:
        - Accounts
      summary: Get an account
      parameters:
        - name: id
          in: path
          required: true
          description: Account id.
          schema:
            type: string
      responses:
        "200":
          description: The account.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Account"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Accounts
      summary: Update an account
      description: "Requires scope `accounts:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Account id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              properties:
                profileId:
                  type: string
                  description: Move the account to a different profile owned by this tenant.
                username:
                  type:
                    - string
                    - "null"
                  minLength: 1
                  maxLength: 200
                displayName:
                  type:
                    - string
                    - "null"
                  minLength: 1
                  maxLength: 200
      responses:
        "200":
          description: Updated account.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Account"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - Accounts
      summary: Disconnect an account
      description: "Requires scope `accounts:write`. Soft-disconnect: clears stored credentials and flips status to disconnected; PostTarget history is preserved."
      parameters:
        - name: id
          in: path
          required: true
          description: Account id.
          schema:
            type: string
      responses:
        "200":
          description: Disconnected account.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Account"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/accounts/{id}/health":
    get:
      tags:
        - Accounts
      summary: Get account health
      description: "DB-derived health snapshot (does not make a live call to the platform): connection status, whether we still hold encrypted credentials, and the last recorded error."
      parameters:
        - name: id
          in: path
          required: true
          description: Account id.
          schema:
            type: string
      responses:
        "200":
          description: Health snapshot.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AccountHealth"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/account-groups":
    get:
      tags:
        - Accounts
      summary: List account groups
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: profileId
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated list of account groups.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/AccountGroup"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
    post:
      tags:
        - Accounts
      summary: Create an account group
      description: "Requires scope `account-groups:write`. Bundles accounts under a profile for bulk targeting."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - profileId
                - name
              properties:
                profileId:
                  type: string
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                accountIds:
                  type: array
                  items:
                    type: string
                  maxItems: 500
      responses:
        "201":
          description: Account group created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AccountGroup"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/account-groups/{id}":
    get:
      tags:
        - Accounts
      summary: Get an account group
      parameters:
        - name: id
          in: path
          required: true
          description: Account group id.
          schema:
            type: string
      responses:
        "200":
          description: The account group.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AccountGroup"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Accounts
      summary: Update an account group
      description: "Requires scope `account-groups:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Account group id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                accountIds:
                  type: array
                  items:
                    type: string
                  maxItems: 500
      responses:
        "200":
          description: Updated account group.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AccountGroup"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - Accounts
      summary: Delete an account group
      description: "Requires scope `account-groups:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Account group id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/connect/authorize":
    post:
      tags:
        - Connect
      summary: Start connecting an account
      description: "Requires scope `accounts:connect`. For platform=mastodon this dynamically registers an OAuth app with the given instance and returns a real authUrl; for every other platform it returns mode='manual' with the fields to collect and POST to /connect/manual."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - platform
                - profileId
              properties:
                platform:
                  "$ref": "#/components/schemas/Platform"
                profileId:
                  type: string
                instance:
                  type: string
                  description: "Mastodon instance host (e.g. \"mastodon.social\"). Required only when platform=mastodon."
      responses:
        "200":
          description: Connect flow details.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    description: "For platform=mastodon, mode is 'oauth' and authUrl is a real Mastodon authorization URL to redirect the user to (completes at GET /connect/callback). For every other platform, mode is 'manual' — collect `fields` from the user and POST them to /connect/manual."
                    properties:
                      platform:
                        "$ref": "#/components/schemas/Platform"
                      mode:
                        type: string
                        enum:
                          - oauth
                          - manual
                      authUrl:
                        type:
                          - string
                          - "null"
                        format: uri
                      instance:
                        type: string
                      manualEndpoint:
                        type: string
                        examples:
                          - /api/v1/connect/manual
                      fields:
                        type: array
                        items:
                          type: string
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/connect/manual":
    post:
      tags:
        - Connect
      summary: Connect an account with direct credentials
      description: "Requires scope `accounts:connect`. Bypasses OAuth: validates the given credentials via the platform's PublisherAdapter.connectAccount() and persists them AES-256-GCM encrypted. Upserts on (tenantId, profileId, platform, platformAccountId)."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - platform
                - profileId
                - credentials
              description: "Shape of `credentials` is discriminated by `platform`."
              properties:
                platform:
                  "$ref": "#/components/schemas/Platform"
                profileId:
                  type: string
                credentials:
                  oneOf:
                    - type: object
                      title: BlueskyCredentials
                      required:
                        - identifier
                        - appPassword
                      properties:
                        identifier:
                          type: string
                          description: Bluesky handle or DID.
                        appPassword:
                          type: string
                        service:
                          type: string
                          format: uri
                          description: "Defaults to https://bsky.social."
                    - type: object
                      title: TelegramCredentials
                      required:
                        - botToken
                        - chatId
                      properties:
                        botToken:
                          type: string
                        chatId:
                          type: string
                    - type: object
                      title: DiscordCredentials
                      required:
                        - webhookUrl
                      properties:
                        webhookUrl:
                          type: string
                          format: uri
                    - type: object
                      title: MastodonCredentials
                      required:
                        - instance
                        - accessToken
                      properties:
                        instance:
                          type: string
                        accessToken:
                          type: string
      responses:
        "200":
          description: Existing account reconnected.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Account"
        "201":
          description: New account connected.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Account"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/connect/callback":
    get:
      tags:
        - Connect
      summary: Mastodon OAuth callback
      description: "Redirect target for the Mastodon OAuth flow started by POST /connect/authorize. Not called with a Bearer sk_ key — tenant/profile authorization is instead proven by successfully decrypting the `state` query param, which was minted server-side after the original caller passed authenticate()+requireScope('accounts:connect')."
      security: []
      parameters:
        - name: code
          in: query
          required: true
          schema:
            type: string
        - name: state
          in: query
          required: true
          schema:
            type: string
        - name: error
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Existing account reconnected.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Account"
        "201":
          description: New account connected.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Account"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/posts":
    get:
      tags:
        - Posts
      summary: List posts
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: status
          in: query
          schema:
            "$ref": "#/components/schemas/PostStatus"
        - name: source
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated list of posts.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Post"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
    post:
      tags:
        - Posts
      summary: Create (and optionally schedule/publish) a post
      description: "Requires scope `posts:write`. One PostTarget is created per accountId. If `publishNow` is true the post is claimed by the next cron tick immediately; if `scheduledFor` is set it publishes at that time; otherwise it is saved as a draft. Publishing always goes through the scheduler (claimDuePosts/publishPost) — never directly through adapters."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - content
                - accountIds
              properties:
                content:
                  type: string
                  minLength: 1
                accountIds:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Accounts (owned by this tenant) to publish this post to — one PostTarget is created per account.
                mediaAssetIds:
                  type: array
                  items:
                    type: string
                  description: Previously uploaded MediaAsset ids.
                scheduledFor:
                  type: string
                  format: date-time
                  description: Omit together with publishNow=false to save as a draft.
                publishNow:
                  type: boolean
                  default: false
                source:
                  type: string
                  default: api
      responses:
        "201":
          description: Post created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Post"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/posts/{id}":
    get:
      tags:
        - Posts
      summary: Get a post
      parameters:
        - name: id
          in: path
          required: true
          description: Post id.
          schema:
            type: string
      responses:
        "200":
          description: "The post, including its targets and media."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Post"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Posts
      summary: Update a post
      description: "Requires scope `posts:write`. Only posts in status draft or scheduled may be updated."
      parameters:
        - name: id
          in: path
          required: true
          description: Post id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              description: Only posts in status draft or scheduled may be updated.
              properties:
                content:
                  type: string
                  minLength: 1
                accountIds:
                  type: array
                  items:
                    type: string
                  minItems: 1
                mediaAssetIds:
                  type: array
                  items:
                    type: string
                scheduledFor:
                  type:
                    - string
                    - "null"
                  format: date-time
      responses:
        "200":
          description: Updated post.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Post"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
    delete:
      tags:
        - Posts
      summary: Delete a post
      description: "Requires scope `posts:write`. Cascades to its PostTarget and PostMedia rows."
      parameters:
        - name: id
          in: path
          required: true
          description: Post id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/posts/{id}/retry":
    post:
      tags:
        - Posts
      summary: "Retry a post's failed targets"
      description: "Requires scope `posts:write`. Calls retryPost() from the scheduler — only re-attempts PostTarget rows currently in status failed; published targets are never re-sent."
      parameters:
        - name: id
          in: path
          required: true
          description: Post id.
          schema:
            type: string
      responses:
        "200":
          description: "Post after retry, with updated target statuses."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Post"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/posts/validate":
    post:
      tags:
        - Posts
      summary: Pre-flight validate content against target platforms
      description: "Any authenticated API key (no extra scope). Read-only: reports whether `content` fits each target platform's character limit and whether the referenced `accountId`s are valid/connected, without creating anything."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - content
                - platforms
              properties:
                content:
                  type: string
                  minLength: 1
                platforms:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required:
                      - platform
                    properties:
                      platform:
                        "$ref": "#/components/schemas/Platform"
                      accountId:
                        type: string
                        description: "When given, also checked for tenant ownership, platform match, and isConnected."
      responses:
        "200":
          description: Per-platform validation results.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      valid:
                        type: boolean
                        description: "True iff every entry in `results` is ok."
                      results:
                        type: array
                        items:
                          type: object
                          properties:
                            platform:
                              "$ref": "#/components/schemas/Platform"
                            accountId:
                              type:
                                - string
                                - "null"
                            length:
                              type: integer
                            limit:
                              type: integer
                              description: Platform character limit.
                            ok:
                              type: boolean
                            error:
                              type: string
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
  "/v1/queue":
    get:
      tags:
        - Queue
      summary: List queue slots
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: profileId
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated list of recurring queue slots.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/QueueSlot"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
    post:
      tags:
        - Queue
      summary: Create a queue slot
      description: "Requires scope `queue:write`."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - profileId
                - dayOfWeek
                - timeOfDay
              properties:
                profileId:
                  type: string
                dayOfWeek:
                  type: integer
                  minimum: 0
                  maximum: 6
                timeOfDay:
                  type: integer
                  minimum: 0
                  maximum: 1439
                timezone:
                  type: string
                  default: UTC
                enabled:
                  type: boolean
                  default: true
      responses:
        "201":
          description: Queue slot created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/QueueSlot"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/queue/preview":
    get:
      tags:
        - Queue
      summary: Preview upcoming queue slot occurrences
      description: "Any authenticated API key (no extra scope). Read-only projection of a profile's enabled QueueSlots into their next N upcoming UTC occurrences; does not read or write any Post."
      parameters:
        - name: profileId
          in: query
          required: true
          schema:
            type: string
        - name: count
          in: query
          description: "Defaults to 10, capped at 50."
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
        - name: from
          in: query
          description: ISO 8601 instant to project from. Defaults to now.
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: "Upcoming occurrences, soonest first."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        queueSlotId:
                          type: string
                        dayOfWeek:
                          type: integer
                          minimum: 0
                          maximum: 6
                        timeOfDay:
                          type: integer
                          minimum: 0
                          maximum: 1439
                        timezone:
                          type: string
                        scheduledFor:
                          type: string
                          format: date-time
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/queue/{id}":
    get:
      tags:
        - Queue
      summary: Get a queue slot
      parameters:
        - name: id
          in: path
          required: true
          description: Queue slot id.
          schema:
            type: string
      responses:
        "200":
          description: The queue slot.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/QueueSlot"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Queue
      summary: Update a queue slot
      description: "Requires scope `queue:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Queue slot id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              properties:
                dayOfWeek:
                  type: integer
                  minimum: 0
                  maximum: 6
                timeOfDay:
                  type: integer
                  minimum: 0
                  maximum: 1439
                timezone:
                  type: string
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated queue slot.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/QueueSlot"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - Queue
      summary: Delete a queue slot
      description: "Requires scope `queue:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Queue slot id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/media/upload":
    post:
      tags:
        - Media
      summary: Upload media
      description: "Requires scope `media:write`. Accepts multipart/form-data (field `file`) or a JSON body with `url` (fetched pass-through and re-hosted) or `dataBase64`. Bytes are stored as-is — no transcoding. 25MB max."
      requestBody:
        required: true
        content:
          "multipart/form-data":
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                altText:
                  type: string
                width:
                  type: integer
                height:
                  type: integer
          "application/json":
            schema:
              type: object
              description: "Alternative to multipart/form-data: provide either `url` (fetched pass-through and re-hosted) or `dataBase64`."
              properties:
                url:
                  type: string
                  format: uri
                dataBase64:
                  type: string
                filename:
                  type: string
                mimeType:
                  type: string
                altText:
                  type: string
                width:
                  type: integer
                height:
                  type: integer
      responses:
        "201":
          description: MediaAsset created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/MediaAsset"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/media/presign":
    post:
      tags:
        - Media
      summary: Pre-negotiate an upload slot
      description: "Requires scope `media:write`. Returns a short-lived, tenant-bound token plus the URL/method/field name to POST bytes to (currently /v1/media/upload — swappable for a real object-storage presigned URL later without changing callers)."
      requestBody:
        content:
          "application/json":
            schema:
              type: object
              properties:
                filename:
                  type: string
                mimeType:
                  type: string
      responses:
        "201":
          description: Presign details.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      uploadUrl:
                        type: string
                        format: uri
                      method:
                        type: string
                        examples:
                          - POST
                      fieldName:
                        type: string
                        examples:
                          - file
                      token:
                        type: string
                        description: "Opaque, AES-256-GCM-encrypted upload token bound to this tenant/key."
                      expiresAt:
                        type: string
                        format: date-time
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/validate/media-url":
    post:
      tags:
        - Media
      summary: Validate a remote media URL
      description: "Read-only pre-flight check (any valid API key, no extra scope): confirms the URL is reachable, resolves its mime type/size, without downloading or storing it."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
                  format: uri
      responses:
        "200":
          description: Validation result.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      valid:
                        type: boolean
                      url:
                        type: string
                        format: uri
                      mimeType:
                        type:
                          - string
                          - "null"
                      sizeBytes:
                        type:
                          - integer
                          - "null"
                      type:
                        "$ref": "#/components/schemas/MediaType"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
  "/v1/webhooks":
    get:
      tags:
        - Webhooks
      summary: List webhooks
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Paginated list of webhooks.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Webhook"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
    post:
      tags:
        - Webhooks
      summary: Create a webhook
      description: "Requires scope `webhooks:write`. The response is the ONLY place `secret` (used to compute X-OneSocial-Signature) is ever returned."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - url
                - events
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    "$ref": "#/components/schemas/WebhookEvent"
                  minItems: 1
                enabled:
                  type: boolean
                  default: true
      responses:
        "201":
          description: Webhook created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/WebhookCreated"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/webhooks/{id}":
    get:
      tags:
        - Webhooks
      summary: Get a webhook
      parameters:
        - name: id
          in: path
          required: true
          description: Webhook id.
          schema:
            type: string
      responses:
        "200":
          description: The webhook.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Webhook"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Webhooks
      summary: Update a webhook
      description: "Requires scope `webhooks:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Webhook id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    "$ref": "#/components/schemas/WebhookEvent"
                  minItems: 1
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated webhook.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Webhook"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - Webhooks
      summary: Delete a webhook
      description: "Requires scope `webhooks:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Webhook id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/webhooks/{id}/logs":
    get:
      tags:
        - Webhooks
      summary: "List a webhook's delivery history"
      parameters:
        - name: id
          in: path
          required: true
          description: Webhook id.
          schema:
            type: string
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: "Paginated WebhookDelivery rows for this webhook, newest first."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/WebhookDelivery"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/webhooks/test":
    post:
      tags:
        - Webhooks
      summary: Send a test delivery
      description: "Requires scope `webhooks:write`. Sends a one-off `webhook.test` delivery to the given webhook regardless of its subscribed events list."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - webhookId
              properties:
                webhookId:
                  type: string
      responses:
        "200":
          description: Delivery attempt result.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/WebhookDelivery"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/api-keys":
    get:
      tags:
        - ApiKeys
      summary: List API keys
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Paginated list of API keys (never includes keyHash).
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/ApiKey"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
    post:
      tags:
        - ApiKeys
      summary: Create an API key
      description: "Requires scope `api-keys:write`. The raw sk_ value is returned ONCE in this response and is never retrievable again."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                scopes:
                  type: array
                  items:
                    type: string
                  maxItems: 50
                  default: []
                  description: Empty array grants full access.
      responses:
        "201":
          description: API key created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/ApiKeyCreated"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/api-keys/{id}":
    delete:
      tags:
        - ApiKeys
      summary: Revoke an API key
      description: "Requires scope `api-keys:write`. Sets revokedAt; the key immediately stops authenticating."
      parameters:
        - name: id
          in: path
          required: true
          description: API key id.
          schema:
            type: string
      responses:
        "200":
          description: Revoked API key.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/ApiKey"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
  "/v1/user":
    get:
      tags:
        - User
      summary: Get the current actor
      description: Returns the tenant and API key identity behind the bearer sk_ token used on this request.
      responses:
        "200":
          description: Current actor identity.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      tenantId:
                        type: string
                      scopes:
                        type: array
                        items:
                          type: string
                      tenant:
                        "$ref": "#/components/schemas/Tenant"
                      apiKey:
                        type: object
                        properties:
                          id:
                            type: string
                          name:
                            type: string
                          prefix:
                            type: string
                          scopes:
                            type: array
                            items:
                              type: string
                          lastUsedAt:
                            type:
                              - string
                              - "null"
                            format: date-time
                          createdAt:
                            type: string
                            format: date-time
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/usage":
    get:
      tags:
        - Usage
      summary: Get usage summary
      description: "Account and post counters for the authenticated tenant. `from`/`to` default to the current UTC calendar month."
      parameters:
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Usage summary.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      period:
                        type: object
                        properties:
                          from:
                            type: string
                            format: date-time
                          to:
                            type: string
                            format: date-time
                      accounts:
                        type: object
                        properties:
                          total:
                            type: integer
                          connected:
                            type: integer
                          byPlatform:
                            type: object
                            additionalProperties:
                              type: integer
                      posts:
                        type: object
                        properties:
                          total:
                            type: integer
                          scheduled:
                            type: integer
                          published:
                            type: integer
                          partial:
                            type: integer
                          failed:
                            type: integer
                          createdThisPeriod:
                            type: integer
                          publishedThisPeriod:
                            type: integer
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
  "/v1/analytics":
    get:
      tags:
        - Analytics
      summary: Get an analytics rollup
      description: "Requires scope `analytics:read`. Sums the typed PostMetric counters across `since`/`until` (default: trailing 30 days)."
      parameters:
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
        - name: accountId
          in: query
          schema:
            type: string
        - name: postId
          in: query
          schema:
            type: string
        - name: platform
          in: query
          schema:
            "$ref": "#/components/schemas/Platform"
      responses:
        "200":
          description: Analytics summary.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    description: "The flat counters (impressions/reach/.../saves) are duplicated at the top level (mirroring GET /v1/usage's flat+nested convention) and inside `totals`."
                    properties:
                      impressions:
                        type: integer
                      reach:
                        type: integer
                      engagements:
                        type: integer
                      likes:
                        type: integer
                      comments:
                        type: integer
                      shares:
                        type: integer
                      clicks:
                        type: integer
                      saves:
                        type: integer
                      period:
                        type: object
                        properties:
                          since:
                            type: string
                            format: date-time
                          until:
                            type: string
                            format: date-time
                      totals:
                        type: object
                        properties:
                          impressions:
                            type: integer
                          reach:
                            type: integer
                          engagements:
                            type: integer
                          likes:
                            type: integer
                          comments:
                            type: integer
                          shares:
                            type: integer
                          clicks:
                            type: integer
                          saves:
                            type: integer
                      byPlatform:
                        type: array
                        items:
                          type: object
                          properties:
                            platform:
                              "$ref": "#/components/schemas/Platform"
                            impressions:
                              type: integer
                            reach:
                              type: integer
                            engagements:
                              type: integer
                            likes:
                              type: integer
                            comments:
                              type: integer
                            shares:
                              type: integer
                            clicks:
                              type: integer
                            saves:
                              type: integer
                      snapshotCount:
                        type: integer
                        minimum: 0
                        description: Number of PostMetric rows contributing to this summary.
                      filters:
                        type: object
                        properties:
                          accountId:
                            type:
                              - string
                              - "null"
                          postId:
                            type:
                              - string
                              - "null"
                          platform:
                            anyOf:
                              - "$ref": "#/components/schemas/Platform"
                              - type: "null"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/analytics/daily":
    get:
      tags:
        - Analytics
      summary: Get analytics grouped by day
      description: "Requires scope `analytics:read`. Same filters as GET /analytics, grouped by PostMetric.date for charting."
      parameters:
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
        - name: accountId
          in: query
          schema:
            type: string
        - name: postId
          in: query
          schema:
            type: string
        - name: platform
          in: query
          schema:
            "$ref": "#/components/schemas/Platform"
      responses:
        "200":
          description: "Daily analytics points, ascending by date."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date-time
                        impressions:
                          type: integer
                        reach:
                          type: integer
                        engagements:
                          type: integer
                        likes:
                          type: integer
                        comments:
                          type: integer
                        shares:
                          type: integer
                        clicks:
                          type: integer
                        saves:
                          type: integer
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/analytics/best-time":
    get:
      tags:
        - Analytics
      summary: Get the best time to post
      description: "Requires scope `analytics:read`. Buckets PostMetric rows by weekday x hour and ranks by engagement, to suggest optimal posting slots."
      parameters:
        - name: accountId
          in: query
          schema:
            type: string
        - name: platform
          in: query
          schema:
            "$ref": "#/components/schemas/Platform"
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: "Ranked weekday x hour slots, best first."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        dayOfWeek:
                          type: integer
                          minimum: 0
                          maximum: 6
                          description: 0=Sunday .. 6=Saturday
                        hour:
                          type: integer
                          minimum: 0
                          maximum: 23
                        score:
                          type: number
                          description: Aggregate engagement score for this weekday x hour bucket.
                        sampleSize:
                          type: integer
                          minimum: 0
                          description: Number of PostMetric rows contributing to this bucket.
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/comments":
    get:
      tags:
        - Comments
      summary: List comments
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: accountId
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            "$ref": "#/components/schemas/CommentStatus"
        - name: platform
          in: query
          schema:
            "$ref": "#/components/schemas/Platform"
        - name: postId
          in: query
          schema:
            type: string
        - name: parentId
          in: query
          schema:
            type: string
        - name: q
          in: query
          description: Case-insensitive substring match against comment text.
          schema:
            type: string
      responses:
        "200":
          description: "Paginated list of comments, newest first."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Comment"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
  "/v1/comments/{id}":
    get:
      tags:
        - Comments
      summary: Get a comment
      description: "Includes its threaded replies (Comment rows with parentId set) as `replies`, oldest first."
      parameters:
        - name: id
          in: path
          required: true
          description: Comment id.
          schema:
            type: string
      responses:
        "200":
          description: The comment.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    allOf:
                      - "$ref": "#/components/schemas/Comment"
                      - type: object
                        properties:
                          replies:
                            type: array
                            items:
                              "$ref": "#/components/schemas/Comment"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - Comments
      summary: Delete/hide a comment
      description: "Requires scope `comments:write`. Marks the comment status = deleted; does not remove the row."
      parameters:
        - name: id
          in: path
          required: true
          description: Comment id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted comment.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Comment"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/comments/{id}/reply":
    post:
      tags:
        - Comments
      summary: Reply to a comment
      description: "Requires scope `comments:write`. Persists the reply as a new Comment row (isOwn=true, parentId set to the original), flips the original's status to replied, and dispatches `comment.replied`. 409 if the original comment is already status=deleted."
      parameters:
        - name: id
          in: path
          required: true
          description: Comment id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - text
              properties:
                text:
                  type: string
                  minLength: 1
      responses:
        "201":
          description: The original comment (now status=replied) and the new reply comment.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      comment:
                        "$ref": "#/components/schemas/Comment"
                      reply:
                        "$ref": "#/components/schemas/Comment"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
  "/v1/mentions":
    get:
      tags:
        - Mentions
      summary: List mentions
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint). Read-only — mentions have no reply concept.
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: accountId
          in: query
          schema:
            type: string
        - name: isRead
          in: query
          schema:
            type: boolean
        - name: platform
          in: query
          schema:
            "$ref": "#/components/schemas/Platform"
        - name: q
          in: query
          description: Case-insensitive substring match against mention text.
          schema:
            type: string
      responses:
        "200":
          description: "Paginated list of mentions, newest first."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Mention"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
  "/v1/reviews":
    get:
      tags:
        - Reviews
      summary: List reviews
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: accountId
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            "$ref": "#/components/schemas/ReviewStatus"
        - name: platform
          in: query
          schema:
            "$ref": "#/components/schemas/Platform"
        - name: rating
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 5
        - name: q
          in: query
          description: Case-insensitive substring match against review text.
          schema:
            type: string
      responses:
        "200":
          description: "Paginated list of reviews, newest first."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Review"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
  "/v1/reviews/{id}":
    get:
      tags:
        - Reviews
      summary: Get a review
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
      parameters:
        - name: id
          in: path
          required: true
          description: Review id.
          schema:
            type: string
      responses:
        "200":
          description: The review.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Review"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/reviews/{id}/reply":
    post:
      tags:
        - Reviews
      summary: Reply to a review
      description: "Requires scope `reviews:write`. Sets reply/repliedAt and status=replied, and dispatches `review.replied`. Re-posting to an already-replied review overwrites the stored reply (edit-in-place; there is a single `reply` field, not a thread)."
      parameters:
        - name: id
          in: path
          required: true
          description: Review id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - text
              properties:
                text:
                  type: string
                  minLength: 1
      responses:
        "200":
          description: The updated review.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Review"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/messages":
    get:
      tags:
        - Messages
      summary: List conversations
      description: "Requires scope `messages:read`. Despite the path, this lists Conversation rows (message threads), sorted by lastMessageAt."
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: accountId
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            "$ref": "#/components/schemas/ConversationStatus"
        - name: platform
          in: query
          schema:
            "$ref": "#/components/schemas/Platform"
        - name: contactId
          in: query
          schema:
            type: string
        - name: unread
          in: query
          description: "true = unreadCount > 0, false = unreadCount == 0."
          schema:
            type: boolean
      responses:
        "200":
          description: Paginated list of conversations.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Conversation"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/conversations/{id}":
    get:
      tags:
        - Messages
      summary: Get a conversation
      description: "Requires scope `messages:read`. Includes its messages, oldest first."
      parameters:
        - name: id
          in: path
          required: true
          description: Conversation id.
          schema:
            type: string
      responses:
        "200":
          description: The conversation with messages.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Conversation"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/conversations/{id}/mark-read":
    post:
      tags:
        - Messages
      summary: Mark a conversation read
      description: "Requires scope `messages:write`. Sets unreadCount=0 and isRead=true on its inbound messages."
      parameters:
        - name: id
          in: path
          required: true
          description: Conversation id.
          schema:
            type: string
      responses:
        "200":
          description: The updated conversation.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Conversation"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/messages/send":
    post:
      tags:
        - Messages
      summary: Send a DM
      description: "Requires scope `messages:write`. Sends via the platform adapter and records an outbound Message; pass `conversationId` to append to an existing thread, or `accountId`+`contactId` to start a new one."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - text
              description: "Provide either `conversationId` to append to an existing thread, or `contactId` + `accountId` to start/continue one with that contact on that account."
              properties:
                conversationId:
                  type: string
                  description: Existing Conversation to append to.
                contactId:
                  type: string
                  description: Required (with accountId) when conversationId is omitted.
                accountId:
                  type: string
                  description: Required (with contactId) when conversationId is omitted.
                platform:
                  "$ref": "#/components/schemas/Platform"
                  description: "Optional assertion that must match the account's platform."
                text:
                  type: string
                  minLength: 1
                attachments:
                  type:
                    - object
                    - array
                    - "null"
      responses:
        "201":
          description: The sent message.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Message"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/messages/{id}":
    patch:
      tags:
        - Messages
      summary: Edit a message
      description: "Requires scope `messages:write`. Only own outbound messages may be edited; sets editedAt."
      parameters:
        - name: id
          in: path
          required: true
          description: Message id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - text
              properties:
                text:
                  type: string
                  minLength: 1
      responses:
        "200":
          description: The updated message.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Message"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - Messages
      summary: Delete a message
      description: "Requires scope `messages:write`. Soft-delete: sets deletedAt and status=deleted; thread ordering is preserved."
      parameters:
        - name: id
          in: path
          required: true
          description: Message id.
          schema:
            type: string
      responses:
        "200":
          description: The deleted message.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Message"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/contacts":
    get:
      tags:
        - Contacts
      summary: List contacts
      description: "Requires scope `contacts:read`."
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: q
          in: query
          description: Case-insensitive substring match against first/last name or email.
          schema:
            type: string
        - name: email
          in: query
          schema:
            type: string
            format: email
        - name: tag
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated list of contacts.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Contact"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
    post:
      tags:
        - Contacts
      summary: Create a contact
      description: "Requires scope `contacts:write`. `customFields` is validated against this tenant's CustomFieldDef rows via validateCustomFields() before persistence."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              properties:
                firstName:
                  type: string
                  maxLength: 200
                lastName:
                  type: string
                  maxLength: 200
                email:
                  type: string
                  format: email
                  maxLength: 320
                phone:
                  type: string
                  maxLength: 50
                avatarUrl:
                  type: string
                  format: uri
                  maxLength: 2000
                notes:
                  type: string
                  maxLength: 5000
                tags:
                  type: array
                  items:
                    type: string
                    maxLength: 100
                  maxItems: 200
                customFields:
                  type: object
                  description: "Validated against this tenant's CustomFieldDef rows before persistence. Use POST /contacts/{id}/channels to attach delivery channels after creation."
      responses:
        "201":
          description: Contact created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Contact"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/contacts/bulk-create":
    post:
      tags:
        - Contacts
      summary: Bulk-create contacts
      description: "Requires scope `contacts:write`. Up to 1000 contacts per call; each is validated (including customFields) before a single createMany."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - contacts
              properties:
                contacts:
                  type: array
                  items:
                    type: object
                    properties:
                      firstName:
                        type: string
                        maxLength: 200
                      lastName:
                        type: string
                        maxLength: 200
                      email:
                        type: string
                        format: email
                        maxLength: 320
                      phone:
                        type: string
                        maxLength: 50
                      avatarUrl:
                        type: string
                        format: uri
                        maxLength: 2000
                      notes:
                        type: string
                        maxLength: 5000
                      tags:
                        type: array
                        items:
                          type: string
                          maxLength: 100
                        maxItems: 200
                      customFields:
                        type: object
                        description: "Validated against this tenant's CustomFieldDef rows before persistence. Use POST /contacts/{id}/channels to attach delivery channels after creation."
                  minItems: 1
                  maxItems: 1000
      responses:
        "201":
          description: Bulk-create result.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      count:
                        type: integer
                      contacts:
                        type: array
                        items:
                          "$ref": "#/components/schemas/Contact"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/contacts/{id}":
    get:
      tags:
        - Contacts
      summary: Get a contact
      description: "Requires scope `contacts:read`. Includes channels."
      parameters:
        - name: id
          in: path
          required: true
          description: Contact id.
          schema:
            type: string
      responses:
        "200":
          description: The contact.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Contact"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Contacts
      summary: Update a contact
      description: "Requires scope `contacts:write`. `customFields`, if present, is re-validated in full against this tenant's CustomFieldDef rows."
      parameters:
        - name: id
          in: path
          required: true
          description: Contact id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              properties:
                firstName:
                  type:
                    - string
                    - "null"
                lastName:
                  type:
                    - string
                    - "null"
                email:
                  type:
                    - string
                    - "null"
                  format: email
                phone:
                  type:
                    - string
                    - "null"
                avatarUrl:
                  type:
                    - string
                    - "null"
                  format: uri
                notes:
                  type:
                    - string
                    - "null"
                tags:
                  type: array
                  items:
                    type: string
                customFields:
                  type: object
                  description: "Validated against this tenant's CustomFieldDef rows before persistence."
      responses:
        "200":
          description: Updated contact.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Contact"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - Contacts
      summary: Delete a contact
      description: "Requires scope `contacts:write`. Cascades to its channels, broadcast recipient rows, and sequence enrollments."
      parameters:
        - name: id
          in: path
          required: true
          description: Contact id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/contacts/{id}/channels":
    post:
      tags:
        - Contacts
      summary: Add a channel to a contact
      description: "Requires scope `contacts:write`. Rejects with 409 if this exact (platform, externalId) is already attached to the contact — unique per (contactId, platform, externalId)."
      parameters:
        - name: id
          in: path
          required: true
          description: Contact id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - platform
                - externalId
              properties:
                platform:
                  "$ref": "#/components/schemas/Platform"
                externalId:
                  type: string
                  minLength: 1
                  maxLength: 500
                accountId:
                  type: string
                  description: "When given, must be a tenant account on the same platform."
                handle:
                  type:
                    - string
                    - "null"
                  maxLength: 200
      responses:
        "201":
          description: Channel added.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/ContactChannel"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
  "/v1/custom-fields":
    get:
      tags:
        - CustomFields
      summary: List custom field definitions
      description: "Requires scope `custom-fields:read`."
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Paginated list of custom field defs.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/CustomFieldDef"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
    post:
      tags:
        - CustomFields
      summary: Create a custom field definition
      description: "Requires scope `custom-fields:write`. Unique per tenant on `key`; 409 if that key already exists."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - key
                - label
              properties:
                key:
                  type: string
                  minLength: 1
                  maxLength: 100
                label:
                  type: string
                  minLength: 1
                  maxLength: 200
                type:
                  "$ref": "#/components/schemas/CustomFieldType"
                required:
                  type: boolean
                  default: false
                options:
                  type: array
                  items:
                    type: string
                  description: Allowed values when type = select.
      responses:
        "201":
          description: Custom field def created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/CustomFieldDef"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "409":
          "$ref": "#/components/responses/Conflict"
  "/v1/custom-fields/{id}":
    get:
      tags:
        - CustomFields
      summary: Get a custom field definition
      description: "Requires scope `custom-fields:read`."
      parameters:
        - name: id
          in: path
          required: true
          description: Custom field def id.
          schema:
            type: string
      responses:
        "200":
          description: The custom field def.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/CustomFieldDef"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - CustomFields
      summary: Update a custom field definition
      description: "Requires scope `custom-fields:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Custom field def id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              description: "`key` is immutable after creation (it's used verbatim inside stored Contact.customFields JSON); every other field, including `type`, may be changed."
              properties:
                label:
                  type: string
                  minLength: 1
                  maxLength: 200
                type:
                  "$ref": "#/components/schemas/CustomFieldType"
                required:
                  type: boolean
                options:
                  type: array
                  items:
                    type: string
                  description: "Must be a non-empty array when the resulting type is 'select'."
      responses:
        "200":
          description: Updated custom field def.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/CustomFieldDef"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - CustomFields
      summary: Delete a custom field definition
      description: "Requires scope `custom-fields:write`. Does not retroactively strip the key from existing Contact.customFields JSON."
      parameters:
        - name: id
          in: path
          required: true
          description: Custom field def id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/broadcasts":
    get:
      tags:
        - Broadcasts
      summary: List broadcasts
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: status
          in: query
          schema:
            "$ref": "#/components/schemas/BroadcastStatus"
      responses:
        "200":
          description: Paginated list of broadcasts.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Broadcast"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
    post:
      tags:
        - Broadcasts
      summary: Create a broadcast
      description: "Requires scope `broadcasts:write`. Created in status=draft (or scheduled, when `scheduledFor` is given); optionally seeds BroadcastRecipient rows from `contactIds`."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - name
                - message
              description: "`platform` is required only when `accountId` is omitted (it's otherwise inferred from the account)."
              properties:
                accountId:
                  type: string
                platform:
                  "$ref": "#/components/schemas/Platform"
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                message:
                  type: string
                  minLength: 1
                contactIds:
                  type: array
                  items:
                    type: string
                  description: "Optional initial recipient set. Contacts with no matching ContactChannel for the resolved platform are skipped (see `skippedContactIds` in the response) rather than failing the request."
                scheduledFor:
                  type: string
                  format: date-time
                  description: "When set, the broadcast is created in status=scheduled instead of draft."
      responses:
        "201":
          description: Broadcast created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    allOf:
                      - "$ref": "#/components/schemas/Broadcast"
                      - type: object
                        properties:
                          skippedContactIds:
                            type: array
                            items:
                              type: string
                            description: "contactIds that had no matching ContactChannel for the resolved platform, so were not seeded as recipients."
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/broadcasts/{id}":
    get:
      tags:
        - Broadcasts
      summary: Get a broadcast
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint). Includes its recipients.
      parameters:
        - name: id
          in: path
          required: true
          description: Broadcast id.
          schema:
            type: string
      responses:
        "200":
          description: The broadcast.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    allOf:
                      - "$ref": "#/components/schemas/Broadcast"
                      - type: object
                        properties:
                          recipients:
                            type: array
                            items:
                              "$ref": "#/components/schemas/BroadcastRecipient"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Broadcasts
      summary: Update a broadcast
      description: "Requires scope `broadcasts:write`. Only broadcasts in status draft or scheduled may be updated. Passing `contactIds` deletes all existing BroadcastRecipient rows for this broadcast and re-creates them (there is no separate add-recipients endpoint)."
      parameters:
        - name: id
          in: path
          required: true
          description: Broadcast id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              description: "Only broadcasts in status draft or scheduled may be updated. Passing `contactIds` fully replaces the existing recipient set (there is no separate add-recipients endpoint)."
              properties:
                accountId:
                  type:
                    - string
                    - "null"
                platform:
                  "$ref": "#/components/schemas/Platform"
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                message:
                  type: string
                  minLength: 1
                contactIds:
                  type: array
                  items:
                    type: string
                  description: Replaces all existing BroadcastRecipient rows for this broadcast.
                scheduledFor:
                  type:
                    - string
                    - "null"
                  format: date-time
                  description: A non-null value moves status to scheduled; null moves it back to draft.
      responses:
        "200":
          description: Updated broadcast.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    allOf:
                      - "$ref": "#/components/schemas/Broadcast"
                      - type: object
                        properties:
                          skippedContactIds:
                            type: array
                            items:
                              type: string
                            description: "Present when `contactIds` was provided: entries with no matching ContactChannel for the resolved platform."
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
    delete:
      tags:
        - Broadcasts
      summary: Cancel a broadcast
      description: "Requires scope `broadcasts:write`. Despite the HTTP method this does NOT delete the row: only broadcasts in status draft or scheduled may be cancelled; it marks their pending BroadcastRecipient rows status=skipped and sets the broadcast status=cancelled. There is no separate /cancel endpoint."
      parameters:
        - name: id
          in: path
          required: true
          description: Broadcast id.
          schema:
            type: string
      responses:
        "200":
          description: The cancelled broadcast.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Broadcast"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
  "/v1/broadcasts/{id}/send":
    post:
      tags:
        - Broadcasts
      summary: Send a broadcast now
      description: "Requires scope `broadcasts:write`. Sets status=sending for the /api/cron/engage broadcasts pass to pick up immediately."
      parameters:
        - name: id
          in: path
          required: true
          description: Broadcast id.
          schema:
            type: string
      responses:
        "200":
          description: "The broadcast, now sending."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Broadcast"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
  "/v1/broadcasts/{id}/schedule":
    post:
      tags:
        - Broadcasts
      summary: Schedule a broadcast
      description: "Requires scope `broadcasts:write`. Sets status=scheduled and scheduledFor for the cron pass to claim once due."
      parameters:
        - name: id
          in: path
          required: true
          description: Broadcast id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - scheduledFor
              properties:
                scheduledFor:
                  type: string
                  format: date-time
                  description: Must be in the future.
      responses:
        "200":
          description: The scheduled broadcast.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Broadcast"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
  "/v1/sequences":
    get:
      tags:
        - Sequences
      summary: List sequences
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: status
          in: query
          schema:
            "$ref": "#/components/schemas/SequenceStatus"
        - name: platform
          in: query
          schema:
            "$ref": "#/components/schemas/Platform"
        - name: accountId
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated list of sequences.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Sequence"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
    post:
      tags:
        - Sequences
      summary: Create a sequence
      description: "Requires scope `sequences:write`. Created in status=draft (unless `status` is given) with the given inline `steps` (defaults to an empty array)."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - name
              properties:
                accountId:
                  type: string
                platform:
                  "$ref": "#/components/schemas/Platform"
                status:
                  "$ref": "#/components/schemas/SequenceStatus"
                  description: Defaults to draft.
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                steps:
                  type: array
                  items:
                    type: object
                    required:
                      - id
                      - type
                    properties:
                      id:
                        type: string
                      type:
                        type: string
                        enum:
                          - delay
                          - message
                          - broadcast
                      content:
                        type: string
                      delayHours:
                        type: number
                  maxItems: 50
                  description: "Defaults to an empty array. `delay` steps require delayHours; `message`/`broadcast` steps require content."
      responses:
        "201":
          description: Sequence created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Sequence"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/sequences/{id}":
    get:
      tags:
        - Sequences
      summary: Get a sequence
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
      parameters:
        - name: id
          in: path
          required: true
          description: Sequence id.
          schema:
            type: string
      responses:
        "200":
          description: The sequence.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Sequence"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Sequences
      summary: Update a sequence
      description: "Requires scope `sequences:write`. Sets `status` to active/paused/draft here — there are no separate activate/pause endpoints."
      parameters:
        - name: id
          in: path
          required: true
          description: Sequence id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              description: "Only draft/active/paused transitions are supported for `status` (set it to `active` or `paused` here; there are no separate activate/pause endpoints)."
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                accountId:
                  type:
                    - string
                    - "null"
                platform:
                  anyOf:
                    - "$ref": "#/components/schemas/Platform"
                    - type: "null"
                status:
                  "$ref": "#/components/schemas/SequenceStatus"
                steps:
                  type: array
                  items:
                    type: object
                    required:
                      - id
                      - type
                    properties:
                      id:
                        type: string
                      type:
                        type: string
                        enum:
                          - delay
                          - message
                          - broadcast
                      content:
                        type: string
                      delayHours:
                        type: number
                  maxItems: 50
      responses:
        "200":
          description: Updated sequence.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Sequence"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
    delete:
      tags:
        - Sequences
      summary: Delete a sequence
      description: "Requires scope `sequences:write`. Hard-deletes the row (there is no cancelled status for sequences); rejected with 409 while any enrollment is still status=active."
      parameters:
        - name: id
          in: path
          required: true
          description: Sequence id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
  "/v1/sequences/{id}/enroll":
    post:
      tags:
        - Sequences
      summary: Enroll a contact in a sequence
      description: "Requires scope `sequences:write`. Upserts a single SequenceEnrollment row on (sequenceId, contactId) with status=active, currentStep=0, and nextRunAt derived from the first step. There is no separate unenroll endpoint or bulk form — call this once per contactId, and set the enrollment's status via a client-side record if you need to track unenrollment (the API itself has no route for it yet)."
      parameters:
        - name: id
          in: path
          required: true
          description: Sequence id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - contactId
              description: Enrolls one contact per call. Re-enrolling a contact whose enrollment is status=completed or unenrolled restarts it at step 0; any other existing status is a 409 conflict.
              properties:
                contactId:
                  type: string
      responses:
        "200":
          description: An existing completed/unenrolled enrollment was reset and restarted at step 0.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/SequenceEnrollment"
        "201":
          description: New enrollment created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/SequenceEnrollment"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          description: The contact already has an active or paused enrollment in this sequence.
          content:
            "application/json":
              schema:
                "$ref": "#/components/schemas/Error"
  "/v1/automations/comment-to-dm":
    get:
      tags:
        - Automations
      summary: List comment-to-DM automations
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: accountId
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated list of automations.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/CommentAutomation"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
    post:
      tags:
        - Automations
      summary: Create a comment-to-DM automation
      description: "Requires scope `automations:write`."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - accountId
                - keywords
                - dmMessage
              description: "`platform` is always derived from `accountId` and is not accepted here. `name` defaults to \"Comment automation for <account>\" when omitted."
              properties:
                accountId:
                  type: string
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                enabled:
                  type: boolean
                  default: true
                keywords:
                  type: array
                  items:
                    type: string
                  minItems: 1
                matchType:
                  type: string
                  enum:
                    - any
                    - all
                    - exact
                  default: any
                dmMessage:
                  type: string
                  minLength: 1
                replyMessage:
                  type: string
      responses:
        "201":
          description: Automation created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/CommentAutomation"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/automations/comment-to-dm/{id}":
    get:
      tags:
        - Automations
      summary: Get a comment-to-DM automation
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
      parameters:
        - name: id
          in: path
          required: true
          description: Automation id.
          schema:
            type: string
      responses:
        "200":
          description: The automation.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/CommentAutomation"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Automations
      summary: Update a comment-to-DM automation
      description: "Requires scope `automations:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Automation id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              description: "`accountId`, when given, re-derives `platform` from the new account."
              properties:
                accountId:
                  type: string
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                enabled:
                  type: boolean
                keywords:
                  type: array
                  items:
                    type: string
                  minItems: 1
                matchType:
                  type: string
                  enum:
                    - any
                    - all
                    - exact
                dmMessage:
                  type: string
                  minLength: 1
                replyMessage:
                  type:
                    - string
                    - "null"
      responses:
        "200":
          description: Updated automation.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/CommentAutomation"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - Automations
      summary: Delete a comment-to-DM automation
      description: "Requires scope `automations:write`. Cascades to its trigger logs."
      parameters:
        - name: id
          in: path
          required: true
          description: Automation id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/automations/comment-to-dm/{id}/logs":
    get:
      tags:
        - Automations
      summary: "List an automation's trigger logs"
      description: Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
      parameters:
        - name: id
          in: path
          required: true
          description: Automation id.
          schema:
            type: string
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: "Paginated CommentAutomationLog rows, newest first."
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/CommentAutomationLog"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/media/file/{tenantId}/{filename}":
    get:
      tags:
        - Media
      summary: Serve an uploaded media file from disk
      description: "Not bearer-authenticated on purpose — URLs returned by POST /v1/media/upload are embedded directly in outbound posts/DMs and rendered in <img>/<video> tags, so they must be fetchable by browsers and third-party platforms that can't attach an Authorization header. Access control relies on `filename` being an unguessable random UUID. Storage lives outside `public/` (fixes uploaded media 404ing under `next start`); both path segments are validated against a strict filesystem-safe pattern and resolved against the uploads root, rejecting any traversal attempt. Content-Type is derived from a small extension allowlist (falls back to application/octet-stream); responses always carry X-Content-Type-Options: nosniff and a restrictive Content-Security-Policy. Returns 404 (never 403) on a miss or a rejected/malformed path, so the endpoint never reveals filesystem shape."
      security: []
      parameters:
        - name: tenantId
          in: path
          required: true
          description: "Owning tenant id, as returned in the media asset's url."
          schema:
            type: string
        - name: filename
          in: path
          required: true
          description: "Random UUID-based stored filename (with extension), as returned in the media asset's url."
          schema:
            type: string
      responses:
        "200":
          description: "The raw file bytes, with a Content-Type derived from the file extension."
          content:
            "application/octet-stream":
              schema:
                type: string
                format: binary
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ad-accounts":
    get:
      tags:
        - AdAccounts
      summary: List ad accounts
      description: "Requires scope `ad-accounts:read`."
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: network
          in: query
          schema:
            "$ref": "#/components/schemas/AdNetwork"
      responses:
        "200":
          description: Paginated list of ad accounts.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/AdAccount"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
    post:
      tags:
        - AdAccounts
      summary: Connect an ad account
      description: "Requires scope `ad-accounts:write`. `credentials` is validated live via the network adapter's `connectAdAccount()` when that network is configured server-side (`<NETWORK>_ADS_*` env vars present); otherwise it is stored as given. Always AES-256-GCM encrypted at rest and never returned."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - network
                - credentials
              properties:
                network:
                  "$ref": "#/components/schemas/AdNetwork"
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                externalAccountId:
                  type: string
                  description: "Network-native ad account id, when already known."
                credentials:
                  type: object
                  description: "Network-native credentials/tokens JSON. Validated live via the adapter's connectAdAccount() when the network is configured server-side, else stored as-is. AES-256-GCM encrypted at rest; never returned by any response."
                meta:
                  type: object
                  description: Non-secret connection config.
      responses:
        "201":
          description: Ad account connected.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdAccount"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
  "/v1/ad-accounts/{id}":
    get:
      tags:
        - AdAccounts
      summary: Get an ad account
      description: "Requires scope `ad-accounts:read`."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad account id.
          schema:
            type: string
      responses:
        "200":
          description: The ad account.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdAccount"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - AdAccounts
      summary: Update an ad account
      description: "Requires scope `ad-accounts:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad account id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                status:
                  "$ref": "#/components/schemas/AdAccountStatus"
                meta:
                  type: object
      responses:
        "200":
          description: Updated ad account.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdAccount"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - AdAccounts
      summary: Disconnect an ad account
      description: "Requires scope `ad-accounts:write`. Sets status=disconnected; cascades to its campaigns, ads, audiences, and tracking tags."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad account id.
          schema:
            type: string
      responses:
        "200":
          description: Disconnected.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ad-campaigns":
    get:
      tags:
        - AdCampaigns
      summary: List ad campaigns
      description: "Requires scope `ad-campaigns:read`."
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: adAccountId
          in: query
          schema:
            type: string
        - name: network
          in: query
          schema:
            "$ref": "#/components/schemas/AdNetwork"
        - name: status
          in: query
          schema:
            "$ref": "#/components/schemas/AdStatus"
      responses:
        "200":
          description: Paginated list of ad campaigns.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/AdCampaign"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
    post:
      tags:
        - AdCampaigns
      summary: Create an ad campaign
      description: "Requires scope `ad-campaigns:write`. `adAccountId` must belong to the caller's tenant. Created in status=draft."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - adAccountId
                - name
                - objective
              properties:
                adAccountId:
                  type: string
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                objective:
                  "$ref": "#/components/schemas/AdObjective"
                dailyBudget:
                  type: integer
                  minimum: 0
                  description: Minor units (cents).
                lifetimeBudget:
                  type: integer
                  minimum: 0
                  description: Minor units (cents).
                startDate:
                  type: string
                  format: date-time
                endDate:
                  type: string
                  format: date-time
      responses:
        "201":
          description: Ad campaign created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdCampaign"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ad-campaigns/{id}":
    get:
      tags:
        - AdCampaigns
      summary: Get an ad campaign
      description: "Requires scope `ad-campaigns:read`."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad campaign id.
          schema:
            type: string
      responses:
        "200":
          description: The ad campaign.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdCampaign"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - AdCampaigns
      summary: Update an ad campaign
      description: "Requires scope `ad-campaigns:write`. Status transitions go through `POST /v1/ad-campaigns/{id}/status`, not this endpoint."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad campaign id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              description: "Status transitions go through the dedicated pause/resume actions, not this endpoint."
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                objective:
                  "$ref": "#/components/schemas/AdObjective"
                dailyBudget:
                  type: integer
                  minimum: 0
                  description: Minor units (cents).
                lifetimeBudget:
                  type: integer
                  minimum: 0
                  description: Minor units (cents).
                startDate:
                  type:
                    - string
                    - "null"
                  format: date-time
                endDate:
                  type:
                    - string
                    - "null"
                  format: date-time
      responses:
        "200":
          description: Updated ad campaign.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdCampaign"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - AdCampaigns
      summary: Delete an ad campaign
      description: "Requires scope `ad-campaigns:write`. Detaches (does not delete) any ads currently assigned to it."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad campaign id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ad-campaigns/{id}/status":
    post:
      tags:
        - AdCampaigns
      summary: Pause or resume an ad campaign
      description: "Requires scope `ad-campaigns:write`. Body `{ action: \"pause\" | \"resume\" }`. `pause` is only valid from status=active and `resume` only from status=paused; any other current status is a 400. Dispatches to the network adapter (best-effort when configured), sets status accordingly, and emits `ad.status_changed`."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad campaign id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - action
              properties:
                action:
                  type: string
                  enum:
                    - pause
                    - resume
                  description: "`pause` an active campaign or `resume` a paused one."
      responses:
        "200":
          description: The updated ad campaign.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdCampaign"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ads":
    get:
      tags:
        - Ads
      summary: List ads
      description: "Requires scope `ads:read`."
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: adAccountId
          in: query
          schema:
            type: string
        - name: campaignId
          in: query
          schema:
            type: string
        - name: network
          in: query
          schema:
            "$ref": "#/components/schemas/AdNetwork"
        - name: status
          in: query
          schema:
            "$ref": "#/components/schemas/AdStatus"
      responses:
        "200":
          description: Paginated list of ads.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Ad"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
    post:
      tags:
        - Ads
      summary: Create an ad
      description: "Requires scope `ads:write`. `adAccountId` (and `campaignId`, when given) must belong to the caller's tenant, and `campaignId`'s adAccountId must match. Created in status=draft unless `status` is given."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - adAccountId
                - name
              properties:
                adAccountId:
                  type: string
                campaignId:
                  type: string
                  description: "Must belong to the same tenant (and, when both given, the same adAccountId)."
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                creative:
                  type: object
                targeting:
                  type: object
                dailyBudget:
                  type: integer
                  minimum: 0
                  description: Minor units (cents).
                lifetimeBudget:
                  type: integer
                  minimum: 0
                  description: Minor units (cents).
                startDate:
                  type: string
                  format: date-time
                endDate:
                  type: string
                  format: date-time
                status:
                  "$ref": "#/components/schemas/AdStatus"
      responses:
        "201":
          description: Ad created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Ad"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ads/{id}":
    get:
      tags:
        - Ads
      summary: Get an ad
      description: "Requires scope `ads:read`."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad id.
          schema:
            type: string
      responses:
        "200":
          description: The ad.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Ad"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - Ads
      summary: Update an ad
      description: "Requires scope `ads:write`. Only permitted while the ad's current status is draft or paused. Emits `ad.status_changed` when `status` changes."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              description: "Only permitted while the ad's current status is draft or paused."
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                status:
                  "$ref": "#/components/schemas/AdStatus"
                creative:
                  type: object
                targeting:
                  type: object
                dailyBudget:
                  type: integer
                  minimum: 0
                  description: Minor units (cents).
                lifetimeBudget:
                  type: integer
                  minimum: 0
                  description: Minor units (cents).
                startDate:
                  type:
                    - string
                    - "null"
                  format: date-time
                endDate:
                  type:
                    - string
                    - "null"
                  format: date-time
      responses:
        "200":
          description: Updated ad.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Ad"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
        "409":
          "$ref": "#/components/responses/Conflict"
    delete:
      tags:
        - Ads
      summary: Delete an ad
      description: "Requires scope `ads:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ads/{id}/analytics":
    get:
      tags:
        - Ads
      summary: "Get an ad's daily analytics"
      description: "Requires scope `ads:read`. Reads from AdMetric."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad id.
          schema:
            type: string
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
        - name: metrics
          in: query
          description: "Comma-separated subset of metric names to include in `extra`; omit for the default set."
          schema:
            type: string
      responses:
        "200":
          description: Per-day metric rows.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/AdMetric"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ads/boost-post":
    post:
      tags:
        - Ads
      summary: Boost an organic post into a paid ad
      description: "Requires scope `ads:write`. `postId` must belong to the caller's tenant (404, not leaked, on mismatch). Creates an `Ad` with `postId` set."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - postId
                - adAccountId
                - budget
              properties:
                postId:
                  type: string
                  description: "Must belong to the caller's tenant."
                adAccountId:
                  type: string
                campaignId:
                  type: string
                budget:
                  type: object
                  description: "At least one of daily/total, minor units (cents)."
                  properties:
                    daily:
                      type: integer
                      minimum: 0
                      description: Minor units (cents).
                    total:
                      type: integer
                      minimum: 0
                      description: Minor units (cents).
                targeting:
                  type: object
                content:
                  type: string
                  description: "Fallback copy when the post can't be referenced natively."
                mediaUrls:
                  type: array
                  items:
                    type: string
                    format: uri
      responses:
        "201":
          description: Ad created from the boosted post.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Ad"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ads/create-lead-form":
    post:
      tags:
        - Ads
      summary: Create a lead form
      description: "Requires scope `ads:write`. `adAccountId` must belong to the caller's tenant."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - adAccountId
                - name
                - fields
              properties:
                adAccountId:
                  type: string
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                fields:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required:
                      - key
                      - label
                      - type
                    properties:
                      key:
                        type: string
                      label:
                        type: string
                      type:
                        type: string
                      required:
                        type: boolean
      responses:
        "201":
          description: Lead form created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/LeadForm"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ads/lead-forms/{id}/submissions":
    post:
      tags:
        - Ads
      summary: Submit a lead against a lead form
      description: "Requires scope `ads:write`. The lead form `{id}` must belong to the caller's tenant (404, not leaked, on mismatch). `data` is validated against the form's field specs — required fields must be present and unknown keys are rejected. Optional `adId` links the lead to a tenant-owned ad. Creates a `Lead` and emits the `lead.received` webhook."
      parameters:
        - name: id
          in: path
          required: true
          description: Lead form id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - data
              properties:
                data:
                  type: object
                  description: "Submitted field values keyed by the lead form's field keys. Required fields must be present; unknown keys are rejected."
                  additionalProperties: true
                adId:
                  type: string
                  description: Optional id of a tenant-owned ad that generated this lead.
      responses:
        "201":
          description: Lead recorded.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/Lead"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ads/search-targeting":
    post:
      tags:
        - Ads
      summary: Search network targeting suggestions
      description: "Requires scope `ads:write`. `adAccountId` must belong to the caller's tenant. Returns a cached echo of the query when the account's network has no credentials configured, so this endpoint is exercisable without ad creds."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - adAccountId
                - query
              properties:
                adAccountId:
                  type: string
                query:
                  type: string
                  minLength: 1
      responses:
        "200":
          description: Targeting suggestions.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/TargetingSuggestion"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ads/send-conversions":
    post:
      tags:
        - Ads
      summary: Send conversion events
      description: "Requires scope `ads:write`. `trackingTagId` must belong to the caller's tenant (404, not leaked, on mismatch). Any `userData.email`/`userData.phone` in an event's payload is SHA-256 hashed before persist and before being forwarded. Persists one `AdConversion` per event and forwards them to the network CAPI via the adapter when configured (`forwarded` reflects the outcome; unconfigured networks persist with `forwarded=false`)."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - trackingTagId
                - events
              properties:
                trackingTagId:
                  type: string
                  description: "Must belong to the caller's tenant."
                events:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required:
                      - eventName
                    properties:
                      eventName:
                        type: string
                      occurredAt:
                        type: string
                        format: date-time
                      payload:
                        type: object
                        description: "Event data (value, currency, etc.). `userData.email`/`userData.phone`, when present, are SHA-256 hashed server-side before persist and before being forwarded to the network CAPI — raw PII is never stored."
                        properties:
                          value:
                            type: number
                          currency:
                            type: string
                          userData:
                            type: object
                            properties:
                              email:
                                type: string
                                format: email
                              phone:
                                type: string
      responses:
        "201":
          description: Conversions recorded.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/AdConversion"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ad-audiences":
    get:
      tags:
        - AdAudiences
      summary: List ad audiences
      description: "Requires scope `ad-audiences:read`."
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: adAccountId
          in: query
          schema:
            type: string
        - name: type
          in: query
          schema:
            "$ref": "#/components/schemas/AdAudienceType"
      responses:
        "200":
          description: Paginated list of ad audiences.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/AdAudience"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
    post:
      tags:
        - AdAudiences
      summary: Create an ad audience
      description: "Requires scope `ad-audiences:write`. `adAccountId` must belong to the caller's tenant. Created in status=building."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - adAccountId
                - name
                - type
              properties:
                adAccountId:
                  type: string
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                type:
                  "$ref": "#/components/schemas/AdAudienceType"
                spec:
                  type: object
                  description: "Rule/spec JSON for saved or lookalike audiences (source audience, ratio, filters)."
      responses:
        "201":
          description: Ad audience created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdAudience"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ad-audiences/{id}":
    get:
      tags:
        - AdAudiences
      summary: Get an ad audience
      description: "Requires scope `ad-audiences:read`."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad audience id.
          schema:
            type: string
      responses:
        "200":
          description: The ad audience.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdAudience"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - AdAudiences
      summary: Update an ad audience
      description: "Requires scope `ad-audiences:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad audience id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                spec:
                  type: object
      responses:
        "200":
          description: Updated ad audience.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdAudience"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - AdAudiences
      summary: Delete an ad audience
      description: "Requires scope `ad-audiences:write`. Cascades to its members."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad audience id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/ad-audiences/{id}/upload":
    post:
      tags:
        - AdAudiences
      summary: Upload audience members
      description: "Requires scope `ad-audiences:write`. `id` must belong to the caller's tenant (404, not leaked, on mismatch). Raw email/phone are SHA-256 hashed immediately; only the hash is ever persisted (`AdAudienceMember.hashedValue`) or handed to the network adapter. `memberCount` is bumped by the number of newly-accepted (previously unseen) hashes."
      parameters:
        - name: id
          in: path
          required: true
          description: Ad audience id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - members
              description: Raw email/phone are SHA-256 hashed server-side immediately; the plaintext is never persisted or forwarded.
              properties:
                members:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    description: Exactly one of email/phone.
                    properties:
                      email:
                        type: string
                        format: email
                      phone:
                        type: string
      responses:
        "200":
          description: Upload result.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/AdAudienceUploadResult"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/tracking-tags":
    get:
      tags:
        - TrackingTags
      summary: List tracking tags
      description: "Requires scope `tracking-tags:read`."
      parameters:
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: adAccountId
          in: query
          schema:
            type: string
        - name: type
          in: query
          schema:
            "$ref": "#/components/schemas/TrackingTagType"
      responses:
        "200":
          description: Paginated list of tracking tags.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/TrackingTag"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
    post:
      tags:
        - TrackingTags
      summary: Create a tracking tag
      description: "Requires scope `tracking-tags:write`. `adAccountId` must belong to the caller's tenant. `credentials`, when given, is AES-256-GCM encrypted at rest and never returned."
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - adAccountId
                - name
                - type
              properties:
                adAccountId:
                  type: string
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                type:
                  "$ref": "#/components/schemas/TrackingTagType"
                config:
                  type: object
                  description: "Non-secret config (gtag measurement id, allowed domains, etc.)."
                credentials:
                  type: object
                  description: Optional secret (e.g. a CAPI access token). AES-256-GCM encrypted at rest; never returned by any response.
      responses:
        "201":
          description: Tracking tag created.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/TrackingTag"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/tracking-tags/{id}":
    get:
      tags:
        - TrackingTags
      summary: Get a tracking tag
      description: "Requires scope `tracking-tags:read`."
      parameters:
        - name: id
          in: path
          required: true
          description: Tracking tag id.
          schema:
            type: string
      responses:
        "200":
          description: The tracking tag.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/TrackingTag"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    patch:
      tags:
        - TrackingTags
      summary: Update a tracking tag
      description: "Requires scope `tracking-tags:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Tracking tag id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              minProperties: 1
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                config:
                  type: object
                credentials:
                  type: object
      responses:
        "200":
          description: Updated tracking tag.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/TrackingTag"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    delete:
      tags:
        - TrackingTags
      summary: Delete a tracking tag
      description: "Requires scope `tracking-tags:write`. Cascades to its events and conversions."
      parameters:
        - name: id
          in: path
          required: true
          description: Tracking tag id.
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      deleted:
                        type: boolean
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
  "/v1/tracking-tags/{id}/events":
    get:
      tags:
        - TrackingTags
      summary: "List a tracking tag's events"
      description: "Requires scope `tracking-tags:read`."
      parameters:
        - name: id
          in: path
          required: true
          description: Tracking tag id.
          schema:
            type: string
        - name: page
          in: query
          description: 1-indexed page number.
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: "Page size, capped at 100."
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Paginated list of tracking tag events.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                  - page
                  - limit
                  - total
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/TrackingTagEvent"
                  page:
                    type: integer
                    minimum: 1
                  limit:
                    type: integer
                    minimum: 1
                    maximum: 100
                  total:
                    type: integer
                    minimum: 0
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
    post:
      tags:
        - TrackingTags
      summary: Record a tracking tag event
      description: "Requires scope `tracking-tags:write`."
      parameters:
        - name: id
          in: path
          required: true
          description: Tracking tag id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          "application/json":
            schema:
              type: object
              required:
                - eventName
              properties:
                eventName:
                  type: string
                payload:
                  type: object
                externalId:
                  type: string
                occurredAt:
                  type: string
                  format: date-time
      responses:
        "201":
          description: Event recorded.
          content:
            "application/json":
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    "$ref": "#/components/schemas/TrackingTagEvent"
        "400":
          "$ref": "#/components/responses/BadRequest"
        "401":
          "$ref": "#/components/responses/Unauthorized"
        "403":
          "$ref": "#/components/responses/Forbidden"
        "404":
          "$ref": "#/components/responses/NotFound"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_...
      description: "Pass your OneSocial API key as `Authorization: Bearer sk_...`."
  parameters:
    Page:
      name: page
      in: query
      description: 1-indexed page number.
      schema:
        type: integer
        minimum: 1
        default: 1
    Limit:
      name: limit
      in: query
      description: "Page size, capped at 100."
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
  responses:
    BadRequest:
      description: The request body or query params failed validation.
      content:
        "application/json":
          schema:
            "$ref": "#/components/schemas/Error"
    Unauthorized:
      description: "Missing, invalid, or revoked API key."
      content:
        "application/json":
          schema:
            "$ref": "#/components/schemas/Error"
    Forbidden:
      description: "The API key's scopes do not permit this action."
      content:
        "application/json":
          schema:
            "$ref": "#/components/schemas/Error"
    NotFound:
      description: No resource with that id exists for this tenant.
      content:
        "application/json":
          schema:
            "$ref": "#/components/schemas/Error"
    Conflict:
      description: "The request conflicts with the resource's current state."
      content:
        "application/json":
          schema:
            "$ref": "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              examples:
                - bad_request
            message:
              type: string
    Platform:
      type: string
      enum:
        - bluesky
        - telegram
        - discord
        - mastodon
      description: Supported publisher platform.
    PostStatus:
      type: string
      enum:
        - draft
        - scheduled
        - publishing
        - published
        - partial
        - failed
        - cancelled
    TargetStatus:
      type: string
      enum:
        - pending
        - publishing
        - published
        - failed
        - skipped
    AccountStatus:
      type: string
      enum:
        - connected
        - disconnected
        - error
    MediaType:
      type: string
      enum:
        - image
        - video
        - document
    WebhookDeliveryStatus:
      type: string
      enum:
        - pending
        - success
        - failed
        - dead
    WebhookEvent:
      type: string
      enum:
        - post.scheduled
        - post.published
        - post.partial
        - post.failed
        - post.cancelled
        - post.platform.published
        - post.platform.failed
        - account.connected
        - account.disconnected
        - webhook.test
        - comment.received
        - mention.received
        - review.new
        - review.updated
        - message.received
        - message.sent
        - conversation.started
        - broadcast.sent
        - sequence.enrolled
        - sequence.completed
        - automation.triggered
    Tenant:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type:
            - string
            - "null"
          format: email
        createdAt:
          type: string
          format: date-time
    ApiKey:
      type: object
      description: "Safe API key fields — `keyHash` is never returned."
      properties:
        id:
          type: string
        name:
          type: string
        prefix:
          type: string
          description: "First characters of the raw key, for display."
        scopes:
          type: array
          items:
            type: string
          description: Empty array = full access.
        lastUsedAt:
          type:
            - string
            - "null"
          format: date-time
        revokedAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
    ApiKeyCreated:
      allOf:
        - "$ref": "#/components/schemas/ApiKey"
        - type: object
          required:
            - key
          properties:
            key:
              type: string
              description: The raw sk_-prefixed secret key. Shown exactly once — store it now.
              examples:
                - sk_live_a1b2c3d4e5f6...
    Profile:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        name:
          type: string
        description:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Account:
      type: object
      description: "Connected social account. `credentials` (encrypted secrets) is never returned."
      properties:
        id:
          type: string
        tenantId:
          type: string
        profileId:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        username:
          type:
            - string
            - "null"
        displayName:
          type:
            - string
            - "null"
        profilePictureUrl:
          type:
            - string
            - "null"
        platformAccountId:
          type:
            - string
            - "null"
        status:
          "$ref": "#/components/schemas/AccountStatus"
        isConnected:
          type: boolean
        meta:
          type:
            - object
            - "null"
          description: Non-secret connection config (e.g. mastodon instance host).
        connectedAt:
          type:
            - string
            - "null"
          format: date-time
        disconnectedAt:
          type:
            - string
            - "null"
          format: date-time
        lastError:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    AccountHealth:
      type: object
      properties:
        id:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        status:
          "$ref": "#/components/schemas/AccountStatus"
        isConnected:
          type: boolean
        hasCredentials:
          type: boolean
        healthy:
          type: boolean
        lastError:
          type:
            - string
            - "null"
        connectedAt:
          type:
            - string
            - "null"
          format: date-time
        disconnectedAt:
          type:
            - string
            - "null"
          format: date-time
        lastUpdatedAt:
          type: string
          format: date-time
        checkedAt:
          type: string
          format: date-time
    AccountGroup:
      type: object
      properties:
        id:
          type: string
        profileId:
          type: string
        name:
          type: string
        accountIds:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    MediaAsset:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        type:
          "$ref": "#/components/schemas/MediaType"
        url:
          type: string
          format: uri
        mimeType:
          type:
            - string
            - "null"
        sizeBytes:
          type:
            - integer
            - "null"
        width:
          type:
            - integer
            - "null"
        height:
          type:
            - integer
            - "null"
        altText:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
    PostTarget:
      type: object
      properties:
        id:
          type: string
        postId:
          type: string
        accountId:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        status:
          "$ref": "#/components/schemas/TargetStatus"
        scheduledFor:
          type:
            - string
            - "null"
          format: date-time
        publishedUrl:
          type:
            - string
            - "null"
        platformPostId:
          type:
            - string
            - "null"
        error:
          type:
            - string
            - "null"
        attempts:
          type: integer
          minimum: 0
        publishedAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Post:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        content:
          type: string
        status:
          "$ref": "#/components/schemas/PostStatus"
        source:
          type: string
          description: "'api' | 'dashboard' | 'mcp' | 'queue'"
          examples:
            - api
        publishNow:
          type: boolean
        scheduledFor:
          type:
            - string
            - "null"
          format: date-time
        publishedAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        targets:
          type: array
          items:
            "$ref": "#/components/schemas/PostTarget"
        media:
          type: array
          items:
            "$ref": "#/components/schemas/MediaAsset"
    QueueSlot:
      type: object
      properties:
        id:
          type: string
        profileId:
          type: string
        dayOfWeek:
          type: integer
          minimum: 0
          maximum: 6
          description: 0=Sunday .. 6=Saturday
        timeOfDay:
          type: integer
          minimum: 0
          maximum: 1439
          description: Minutes from local midnight.
        timezone:
          type: string
          examples:
            - UTC
        enabled:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Webhook:
      type: object
      description: "Webhook subscription. `secret` is never returned here."
      properties:
        id:
          type: string
        tenantId:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            "$ref": "#/components/schemas/WebhookEvent"
        enabled:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    WebhookCreated:
      allOf:
        - "$ref": "#/components/schemas/Webhook"
        - type: object
          required:
            - secret
          properties:
            secret:
              type: string
              description: "Plaintext HMAC signing secret (whsec_...). Shown exactly once, at creation."
              examples:
                - whsec_9f1c2e...
    WebhookDelivery:
      type: object
      properties:
        id:
          type: string
        webhookId:
          type: string
        eventId:
          type: string
          description: Sent as the X-OneSocial-Event-Id header.
        event:
          type: string
        payload:
          type: object
        status:
          "$ref": "#/components/schemas/WebhookDeliveryStatus"
        attempts:
          type: integer
          minimum: 0
        responseCode:
          type:
            - integer
            - "null"
        responseBody:
          type:
            - string
            - "null"
        nextAttemptAt:
          type:
            - string
            - "null"
          format: date-time
        deliveredAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CommentStatus:
      type: string
      enum:
        - open
        - replied
        - hidden
        - deleted
    ReviewStatus:
      type: string
      enum:
        - unreplied
        - replied
    ConversationStatus:
      type: string
      enum:
        - open
        - archived
        - closed
    MessageDirection:
      type: string
      enum:
        - inbound
        - outbound
    MessageStatus:
      type: string
      enum:
        - sent
        - delivered
        - read
        - failed
        - deleted
    CustomFieldType:
      type: string
      enum:
        - text
        - number
        - boolean
        - date
        - select
        - url
    BroadcastStatus:
      type: string
      enum:
        - draft
        - scheduled
        - sending
        - sent
        - cancelled
    BroadcastRecipientStatus:
      type: string
      enum:
        - pending
        - sent
        - delivered
        - failed
        - skipped
    SequenceStatus:
      type: string
      enum:
        - draft
        - active
        - paused
    EnrollmentStatus:
      type: string
      enum:
        - active
        - paused
        - completed
        - unenrolled
    AutomationLogStatus:
      type: string
      enum:
        - matched
        - queued
        - sent
        - failed
        - skipped
    PostMetric:
      type: object
      description: A per-day metric snapshot; may be account-level (postId null) and/or post-level.
      properties:
        id:
          type: string
        tenantId:
          type: string
        accountId:
          type:
            - string
            - "null"
        postId:
          type:
            - string
            - "null"
        platform:
          "$ref": "#/components/schemas/Platform"
        date:
          type: string
          format: date-time
          description: UTC-midnight day bucket this snapshot summarizes.
        impressions:
          type: integer
          minimum: 0
        reach:
          type: integer
          minimum: 0
        engagements:
          type: integer
          minimum: 0
        likes:
          type: integer
          minimum: 0
        comments:
          type: integer
          minimum: 0
        shares:
          type: integer
          minimum: 0
        clicks:
          type: integer
          minimum: 0
        saves:
          type: integer
          minimum: 0
        extra:
          type:
            - object
            - "null"
          description: "Platform-native metrics that don't map to a typed column."
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Comment:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        accountId:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        externalId:
          type:
            - string
            - "null"
          description: Platform-native comment id (idempotent ingest key).
        externalPostId:
          type:
            - string
            - "null"
          description: Platform-native id of the post/media this comment sits on.
        postId:
          type:
            - string
            - "null"
          description: "Our Post, when the comment maps to one we published."
        parentId:
          type:
            - string
            - "null"
        authorName:
          type:
            - string
            - "null"
        authorHandle:
          type:
            - string
            - "null"
        authorAvatarUrl:
          type:
            - string
            - "null"
        text:
          type: string
        status:
          "$ref": "#/components/schemas/CommentStatus"
        isOwn:
          type: boolean
          description: True for comments we authored (our replies).
        likeCount:
          type: integer
          minimum: 0
        postedAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Mention:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        accountId:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        externalId:
          type:
            - string
            - "null"
        authorName:
          type:
            - string
            - "null"
        authorHandle:
          type:
            - string
            - "null"
        text:
          type: string
        url:
          type:
            - string
            - "null"
          format: uri
        isRead:
          type: boolean
        mentionedAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
    Review:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        accountId:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        externalId:
          type:
            - string
            - "null"
        authorName:
          type:
            - string
            - "null"
        rating:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 5
        text:
          type:
            - string
            - "null"
        reply:
          type:
            - string
            - "null"
          description: "Our reply text, once posted."
        repliedAt:
          type:
            - string
            - "null"
          format: date-time
        status:
          "$ref": "#/components/schemas/ReviewStatus"
        reviewedAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Conversation:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        accountId:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        externalId:
          type:
            - string
            - "null"
        contactId:
          type:
            - string
            - "null"
        participantName:
          type:
            - string
            - "null"
        participantHandle:
          type:
            - string
            - "null"
        participantAvatarUrl:
          type:
            - string
            - "null"
        status:
          "$ref": "#/components/schemas/ConversationStatus"
        unreadCount:
          type: integer
          minimum: 0
        lastMessageAt:
          type:
            - string
            - "null"
          format: date-time
        lastMessageText:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        messages:
          type: array
          items:
            "$ref": "#/components/schemas/Message"
          description: "Included only on GET /conversations/{id}."
    Message:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        conversationId:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        externalId:
          type:
            - string
            - "null"
        direction:
          "$ref": "#/components/schemas/MessageDirection"
        text:
          type: string
        status:
          "$ref": "#/components/schemas/MessageStatus"
        attachments:
          type:
            - object
            - array
            - "null"
          description: Media/link attachments as platform-native JSON.
        isRead:
          type: boolean
        editedAt:
          type:
            - string
            - "null"
          format: date-time
        deletedAt:
          type:
            - string
            - "null"
          format: date-time
          description: "Soft-delete: keeps thread ordering intact."
        sentAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Contact:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        firstName:
          type:
            - string
            - "null"
        lastName:
          type:
            - string
            - "null"
        email:
          type:
            - string
            - "null"
          format: email
        phone:
          type:
            - string
            - "null"
        avatarUrl:
          type:
            - string
            - "null"
        notes:
          type:
            - string
            - "null"
        tags:
          type: array
          items:
            type: string
        customFields:
          type: object
          description: "Keys/values validated against the tenant's CustomFieldDef rows at write time."
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        channels:
          type: array
          items:
            "$ref": "#/components/schemas/ContactChannel"
    ContactChannel:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        contactId:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        accountId:
          type:
            - string
            - "null"
        externalId:
          type: string
          description: "Platform-native identity for this contact (handle, PSID, chat id) — delivery target."
        handle:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
    CustomFieldDef:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        key:
          type: string
          description: Machine key used inside Contact.customFields JSON.
        label:
          type: string
        type:
          "$ref": "#/components/schemas/CustomFieldType"
        required:
          type: boolean
        options:
          type: array
          items:
            type: string
          description: Allowed values when type = select.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Broadcast:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        accountId:
          type:
            - string
            - "null"
        platform:
          "$ref": "#/components/schemas/Platform"
        name:
          type: string
        message:
          type: string
        status:
          "$ref": "#/components/schemas/BroadcastStatus"
        scheduledFor:
          type:
            - string
            - "null"
          format: date-time
        sentAt:
          type:
            - string
            - "null"
          format: date-time
        recipientCount:
          type: integer
          minimum: 0
        sentCount:
          type: integer
          minimum: 0
        failedCount:
          type: integer
          minimum: 0
        totalRecipients:
          type: integer
          minimum: 0
          description: Live BroadcastRecipient row count (may exceed recipientCount after add-recipients calls).
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    BroadcastRecipient:
      type: object
      properties:
        id:
          type: string
        broadcastId:
          type: string
        contactId:
          type:
            - string
            - "null"
        channelExternalId:
          type:
            - string
            - "null"
          description: "Snapshot delivery target so a later contact/channel edit can't retarget an in-flight send."
        status:
          "$ref": "#/components/schemas/BroadcastRecipientStatus"
        error:
          type:
            - string
            - "null"
        sentAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    SequenceStep:
      type: object
      required:
        - id
        - type
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - delay
            - message
            - broadcast
        content:
          type: string
        delayHours:
          type: number
    Sequence:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        accountId:
          type:
            - string
            - "null"
        platform:
          anyOf:
            - "$ref": "#/components/schemas/Platform"
            - type: "null"
        name:
          type: string
        status:
          "$ref": "#/components/schemas/SequenceStatus"
        steps:
          type: array
          items:
            "$ref": "#/components/schemas/SequenceStep"
          description: "Ordered steps, stored inline as JSON (no separate step model)."
        enrollmentCount:
          type: integer
          minimum: 0
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    SequenceEnrollment:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        sequenceId:
          type: string
        contactId:
          type: string
        status:
          "$ref": "#/components/schemas/EnrollmentStatus"
        currentStep:
          type: integer
          minimum: 0
          description: Index into Sequence.steps.
        nextRunAt:
          type:
            - string
            - "null"
          format: date-time
          description: When the current step becomes due.
        enrolledAt:
          type: string
          format: date-time
        completedAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CommentAutomation:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        accountId:
          type: string
        platform:
          "$ref": "#/components/schemas/Platform"
        name:
          type: string
        enabled:
          type: boolean
        keywords:
          type: array
          items:
            type: string
          description: "Keywords that, when matched in an inbound comment, queue a DM."
        matchType:
          type: string
          enum:
            - any
            - all
            - exact
          default: any
        dmMessage:
          type: string
        replyMessage:
          type:
            - string
            - "null"
          description: Optional public reply posted alongside the DM.
        triggerCount:
          type: integer
          minimum: 0
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CommentAutomationLog:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        automationId:
          type: string
        commentId:
          type:
            - string
            - "null"
          description: The inbound comment that triggered this rule.
        contactId:
          type:
            - string
            - "null"
          description: The contact the DM was queued/sent to.
        matchedKeyword:
          type:
            - string
            - "null"
        status:
          "$ref": "#/components/schemas/AutomationLogStatus"
        conversationId:
          type:
            - string
            - "null"
          description: "Resulting DM conversation, once queued/sent."
        error:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
    AdNetwork:
      type: string
      enum:
        - meta
        - google
        - tiktok
        - linkedin
        - pinterest
        - x
      description: "Ad network. Independent of the organic `Platform` enum's legacy `twitter` value."
    AdStatus:
      type: string
      enum:
        - draft
        - pending_review
        - active
        - paused
        - archived
        - rejected
        - completed
      description: "Status for Ad, AdCampaign, and LeadForm. Campaign pause = active->paused, resume = paused->active; other transitions are rejected."
    AdObjective:
      type: string
      enum:
        - awareness
        - traffic
        - engagement
        - leads
        - app_promotion
        - sales
    AdAccountStatus:
      type: string
      enum:
        - connected
        - disconnected
        - error
    AdAudienceType:
      type: string
      enum:
        - custom
        - lookalike
        - saved
    AdAudienceStatus:
      type: string
      enum:
        - building
        - ready
        - error
    TrackingTagType:
      type: string
      enum:
        - pixel
        - conversion_api
        - gtag
    AdAccount:
      type: object
      description: "Connected ad account. `credentials` (AES-256-GCM encrypted network tokens) is never returned."
      properties:
        id:
          type: string
        tenantId:
          type: string
        network:
          "$ref": "#/components/schemas/AdNetwork"
        name:
          type: string
        externalAccountId:
          type:
            - string
            - "null"
          description: "Network-native ad account id (e.g. Meta act_123, Google customer id)."
        status:
          "$ref": "#/components/schemas/AdAccountStatus"
        isConnected:
          type: boolean
        currency:
          type:
            - string
            - "null"
        timezone:
          type:
            - string
            - "null"
        meta:
          type:
            - object
            - "null"
          description: "Non-secret connection config (network account name, business id, etc.)."
        connectedAt:
          type:
            - string
            - "null"
          format: date-time
        disconnectedAt:
          type:
            - string
            - "null"
          format: date-time
        lastError:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    AdCampaign:
      type: object
      description: Budgets and spend are integer minor units (cents).
      properties:
        id:
          type: string
        tenantId:
          type: string
        adAccountId:
          type: string
        network:
          "$ref": "#/components/schemas/AdNetwork"
        name:
          type: string
        objective:
          "$ref": "#/components/schemas/AdObjective"
        status:
          "$ref": "#/components/schemas/AdStatus"
        dailyBudget:
          type:
            - integer
            - "null"
          description: Minor units (cents).
        lifetimeBudget:
          type:
            - integer
            - "null"
          description: Minor units (cents).
        spend:
          type: integer
          minimum: 0
          description: "Accumulated spend, minor units (cents)."
        startDate:
          type:
            - string
            - "null"
          format: date-time
        endDate:
          type:
            - string
            - "null"
          format: date-time
        externalId:
          type:
            - string
            - "null"
          description: "Network-native campaign id, once created live."
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Ad:
      type: object
      description: Budgets and spend are integer minor units (cents). Editable via PATCH only while status is draft or paused.
      properties:
        id:
          type: string
        tenantId:
          type: string
        adAccountId:
          type: string
        campaignId:
          type:
            - string
            - "null"
        network:
          "$ref": "#/components/schemas/AdNetwork"
        name:
          type: string
        status:
          "$ref": "#/components/schemas/AdStatus"
        creative:
          type:
            - object
            - "null"
          description: "Creative payload (headline, body, media urls, cta, link) as network-native JSON."
        targeting:
          type:
            - object
            - "null"
          description: Targeting spec as network-native JSON.
        dailyBudget:
          type:
            - integer
            - "null"
          description: Minor units (cents).
        lifetimeBudget:
          type:
            - integer
            - "null"
          description: Minor units (cents).
        spend:
          type: integer
          minimum: 0
          description: "Accumulated spend, minor units (cents)."
        startDate:
          type:
            - string
            - "null"
          format: date-time
        endDate:
          type:
            - string
            - "null"
          format: date-time
        postId:
          type:
            - string
            - "null"
          description: Set when this ad was created by boosting an existing organic Post.
        externalId:
          type:
            - string
            - "null"
          description: "Network-native ad id, once created live."
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    AdMetric:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        adId:
          type: string
        network:
          "$ref": "#/components/schemas/AdNetwork"
        date:
          type: string
          format: date-time
          description: UTC-midnight day bucket.
        impressions:
          type: integer
          minimum: 0
        clicks:
          type: integer
          minimum: 0
        spend:
          type: integer
          minimum: 0
          description: Minor units (cents).
        conversions:
          type: integer
          minimum: 0
        reach:
          type: integer
          minimum: 0
        extra:
          type:
            - object
            - "null"
          description: "Network-native metrics that don't map to a typed column."
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    AdAudience:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        adAccountId:
          type: string
        network:
          "$ref": "#/components/schemas/AdNetwork"
        name:
          type: string
        type:
          "$ref": "#/components/schemas/AdAudienceType"
        status:
          "$ref": "#/components/schemas/AdAudienceStatus"
        memberCount:
          type: integer
          minimum: 0
        spec:
          type:
            - object
            - "null"
          description: "Rule/spec JSON for saved or lookalike audiences (source audience, ratio, filters)."
        externalId:
          type:
            - string
            - "null"
          description: "Network-native audience id, once created live."
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    AdAudienceUploadResult:
      type: object
      description: Raw email/phone are SHA-256 hashed server-side before persist; the plaintext never touches the DB.
      properties:
        audienceId:
          type: string
        received:
          type: integer
          minimum: 0
        accepted:
          type: integer
          minimum: 0
          description: "Number of new, previously-unseen hashed members persisted."
        memberCount:
          type: integer
          minimum: 0
          description: "The audience's updated total member count."
    TrackingTag:
      type: object
      description: "`credentials` (AES-256-GCM encrypted, e.g. a CAPI access token) is never returned."
      properties:
        id:
          type: string
        tenantId:
          type: string
        adAccountId:
          type: string
        network:
          "$ref": "#/components/schemas/AdNetwork"
        name:
          type: string
        type:
          "$ref": "#/components/schemas/TrackingTagType"
        externalId:
          type:
            - string
            - "null"
          description: Network-native pixel/tag id.
        config:
          type:
            - object
            - "null"
          description: "Non-secret config (gtag measurement id, allowed domains, etc.)."
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    TrackingTagEvent:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        tagId:
          type: string
        eventName:
          type: string
          description: "'pageview' | 'conversion' | 'purchase' | custom event name."
        payload:
          type:
            - object
            - "null"
        externalId:
          type:
            - string
            - "null"
          description: Optional dedup / event id.
        occurredAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
    LeadForm:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        adAccountId:
          type: string
        network:
          "$ref": "#/components/schemas/AdNetwork"
        name:
          type: string
        fields:
          type: array
          description: Ordered field specs.
          items:
            type: object
            required:
              - key
              - label
              - type
            properties:
              key:
                type: string
              label:
                type: string
              type:
                type: string
              required:
                type: boolean
        status:
          "$ref": "#/components/schemas/AdStatus"
        externalId:
          type:
            - string
            - "null"
          description: "Network-native lead form id, once created live."
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Lead:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        leadFormId:
          type: string
        data:
          type: object
          description: Submitted field values keyed by field key.
          additionalProperties: true
        adId:
          type:
            - string
            - "null"
          description: Optional link to the ad that generated this lead.
        createdAt:
          type: string
          format: date-time
    AdConversion:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        trackingTagId:
          type: string
        adAccountId:
          type:
            - string
            - "null"
        eventName:
          type: string
          description: "e.g. \"Purchase\", \"Lead\"."
        payload:
          type:
            - object
            - "null"
          description: "Event data forwarded to the network CAPI (value, currency, hashed user_data, etc.)."
        forwarded:
          type: boolean
          description: Whether the network CAPI forward succeeded (false until attempted / when no creds).
        externalId:
          type:
            - string
            - "null"
        occurredAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
    TargetingSuggestion:
      type: object
      description: Returned as a cached echo of the query when the network has no credentials configured.
      properties:
        id:
          type: string
        name:
          type: string
        type:
          type: string
          description: "e.g. \"interest\" | \"geo\" | \"demographic\" | \"behavior\"."
        audienceSize:
          type:
            - integer
            - "null"
        raw:
          type:
            - object
            - "null"
