openapi: 3.0.3
info:
  title: GetMulticast API
  version: "1.0"
  description: >
    Publish, schedule, and manage replies across every platform GetMulticast
    connects to — programmatically. This is the same engine the GetMulticast
    dashboard itself runs on, not a lighter reimplementation. Full reference
    with examples: https://getmulticast.com/api-docs.html
  contact:
    email: support@getmulticast.com
servers:
  - url: https://getmulticast.com/api/v1
security:
  - bearerAuth: []
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: gmc_live_...
      description: >
        API key from Dashboard → Settings → Developer, sent as
        `Authorization: Bearer gmc_live_...`. Each key carries its own
        scopes; a request outside a key's granted scopes returns
        403 insufficient_scope.
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: invalid_request }
            message: { type: string }
            requestId: { type: string }
  responses:
    UnauthorizedError:
      description: Missing, invalid, or revoked API key
      content:
        application/json: { schema: { $ref: '#/components/schemas/Error' } }
    ForbiddenError:
      description: Key lacks the required scope, or the resource belongs to someone else
      content:
        application/json: { schema: { $ref: '#/components/schemas/Error' } }
    NotFoundError:
      description: No such resource
      content:
        application/json: { schema: { $ref: '#/components/schemas/Error' } }
    RateLimitError:
      description: Rate limit exceeded — see Retry-After header
      content:
        application/json: { schema: { $ref: '#/components/schemas/Error' } }
paths:
  /accounts:
    get:
      operationId: listAccounts
      summary: List every connected social account across every platform
      security: [{ bearerAuth: [accounts:read] }]
      responses:
        '200': { description: OK }
        '401': { $ref: '#/components/responses/UnauthorizedError' }

  /credits:
    get:
      operationId: getCredits
      summary: Real remaining AI credit balance
      security: [{ bearerAuth: [credits:read] }]
      responses:
        '200': { description: OK }
        '401': { $ref: '#/components/responses/UnauthorizedError' }

  /videos:
    post:
      operationId: createVideo
      summary: Generate a new AI video (same engine as the dashboard's Generate Video button)
      description: >
        Async — returns 202 immediately with a videoId; poll GET /videos/{id}
        until status is "ready" or "failed". Charges AI credits immediately
        unless saveAsDraft is true (429 resource_exhausted if the balance is
        too low, before anything is charged or generation starts).
      security: [{ bearerAuth: [videos:write] }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                prompt: { type: string, description: 'What the video should be about — becomes a single implicit scene when scenes is omitted' }
                scenes:
                  type: array
                  description: 'Advanced: multiple scenes instead of one prompt'
                  items:
                    type: object
                    properties:
                      mediaSource: { type: string, enum: [ai, upload] }
                      aiPrompt: { type: string }
                      mediaUrl: { type: string }
                      voiceoverScript: { type: string }
                aspectRatio: { type: string, enum: ["9:16", "1:1", "16:9"], default: "9:16" }
                aiVoiceover: { type: boolean }
                voiceId: { type: string }
                saveAsDraft: { type: boolean }
      responses:
        '202': { description: 'Generation started — poll GET /videos/{id} for status' }
        '400': { description: invalid_request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '401': { $ref: '#/components/responses/UnauthorizedError' }
        '429': { description: resource_exhausted — insufficient AI credits, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    get:
      operationId: listVideos
      summary: Lists your videos, newest first
      security: [{ bearerAuth: [videos:read] }]
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        '200': { description: OK }

  /videos/{id}:
    get:
      operationId: getVideo
      summary: One video's generation status (draft, processing, ready, or failed)
      security: [{ bearerAuth: [videos:read] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFoundError' }
    delete:
      operationId: deleteVideo
      summary: Permanently deletes a video
      security: [{ bearerAuth: [videos:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: Deleted }
        '404': { $ref: '#/components/responses/NotFoundError' }

  /posts:
    post:
      operationId: createPost
      summary: Publish now to one or more connected accounts
      security: [{ bearerAuth: [posts:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mediaUrl, mediaType, selections]
              properties:
                mediaUrl: { type: string, description: Public URL to the video or image file }
                mediaType: { type: string, enum: [video, image] }
                selections:
                  type: object
                  description: 'platform -> array of account ids from GET /accounts'
                  additionalProperties: { type: array, items: { type: string } }
                caption: { type: string }
                captionOverrides: { type: object, additionalProperties: { type: string } }
                hashtags: { type: array, items: { type: string } }
                firstComment: { type: string, description: Facebook & Instagram only today }
      responses:
        '202': { description: Queued — publishing happens in the background }
        '400': { description: invalid_request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '401': { $ref: '#/components/responses/UnauthorizedError' }
    get:
      operationId: listPosts
      summary: Lists your posts, newest first
      security: [{ bearerAuth: [posts:read] }]
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        '200': { description: OK }

  /posts/scheduled:
    post:
      operationId: schedulePost
      summary: Same fields as POST /posts, plus date/time/timezone
      security: [{ bearerAuth: [posts:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mediaUrl, mediaType, selections, date]
              properties:
                mediaUrl: { type: string }
                mediaType: { type: string, enum: [video, image] }
                selections: { type: object, additionalProperties: { type: array, items: { type: string } } }
                caption: { type: string }
                date: { type: string, example: "2026-09-20" }
                time: { type: string, example: "09:00", default: "09:00" }
                timezone: { type: string, example: America/New_York, default: UTC }
      responses:
        '201': { description: Scheduled }
        '400': { description: invalid_request }

  /posts/{id}:
    get:
      operationId: getPost
      summary: One post's full status and per-account publish results
      security: [{ bearerAuth: [posts:read] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFoundError' }

  /posts/{id}/analytics:
    get:
      operationId: getPostAnalytics
      summary: Real, live metrics for one post on one platform (youtube, x, pinterest, bluesky only)
      security: [{ bearerAuth: [analytics:read] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: platform, in: query, required: true, schema: { type: string, enum: [youtube, x, pinterest, bluesky] } }
      responses:
        '200': { description: OK }
        '400': { description: not_supported for any other platform }

  /posts/{id}/retry:
    post:
      operationId: retryPost
      summary: Retries only the accounts that failed on a given platform
      security: [{ bearerAuth: [posts:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody:
        content:
          application/json:
            schema: { type: object, required: [platform], properties: { platform: { type: string } } }
      responses:
        '200': { description: OK }

  /scheduled-posts:
    get:
      operationId: listScheduledPosts
      summary: Lists posts still waiting to publish, soonest first
      security: [{ bearerAuth: [posts:read] }]
      responses:
        '200': { description: OK }

  /scheduled-posts/{id}:
    patch:
      operationId: rescheduleScheduledPost
      summary: Reschedules a still-pending post
      security: [{ bearerAuth: [posts:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                scheduledAtUtc: { type: string, example: "2026-09-21T14:00:00Z" }
                date: { type: string }
                time: { type: string }
      responses:
        '200': { description: OK }
    delete:
      operationId: deleteScheduledPost
      summary: Cancels a still-pending scheduled post
      security: [{ bearerAuth: [posts:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '204': { description: Cancelled }

  /comments:
    get:
      operationId: listComments
      summary: Recent comments (last 7 days) across monitored accounts
      security: [{ bearerAuth: [comments:read] }]
      responses:
        '200': { description: OK }

  /comments/reply:
    post:
      operationId: replyToComment
      summary: Replies to one comment surfaced by GET /comments
      security: [{ bearerAuth: [comments:write] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [platform, accountId, commentId, text]
              properties:
                platform: { type: string }
                accountId: { type: string }
                commentId: { type: string }
                text: { type: string }
                directText: { type: string }
                instruction: { type: string }
                postUrn: { type: string, description: LinkedIn only }
                rootUri: { type: string, description: Bluesky only }
                rootCid: { type: string, description: Bluesky only }
                commentCid: { type: string, description: Bluesky only }
      responses:
        '200': { description: OK }

  /comments/dismiss:
    post:
      operationId: dismissComment
      summary: Marks a comment as handled without replying
      security: [{ bearerAuth: [comments:write] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [platform, accountId, commentId]
              properties: { platform: { type: string }, accountId: { type: string }, commentId: { type: string } }
      responses:
        '200': { description: OK }

  /replies/pending:
    get:
      operationId: listPendingReplies
      summary: The Review Inbox — AI-drafted replies waiting for approval
      security: [{ bearerAuth: [comments:read] }]
      responses:
        '200': { description: OK }

  /replies/pending/{id}/approve:
    post:
      operationId: approvePendingReply
      summary: Sends the pending reply
      security: [{ bearerAuth: [comments:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { editedText: { type: string } } }
      responses:
        '200': { description: OK }

  /replies/pending/{id}/reject:
    post:
      operationId: rejectPendingReply
      summary: Discards a pending reply without sending it
      security: [{ bearerAuth: [comments:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK }

  /replies/pending/{id}/regenerate:
    post:
      operationId: regeneratePendingReply
      summary: Asks the AI to revise the current draft
      security: [{ bearerAuth: [comments:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody:
        content:
          application/json:
            schema: { type: object, required: [context], properties: { context: { type: string } } }
      responses:
        '200': { description: OK }

  /replies/pending/{id}:
    patch:
      operationId: updatePendingReplyText
      summary: Overwrites the draft text without sending it
      security: [{ bearerAuth: [comments:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody:
        content:
          application/json:
            schema: { type: object, required: [text], properties: { text: { type: string } } }
      responses:
        '200': { description: OK }

  /webhooks:
    post:
      operationId: registerWebhook
      summary: Registers a webhook — response's secret field shown exactly once
      security: [{ bearerAuth: [webhooks:write] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string }
                events:
                  type: array
                  items: { type: string, enum: [post.published, post.failed, comment.received, reply.sent] }
      responses:
        '200': { description: OK }
    get:
      operationId: listWebhooks
      summary: Lists your registered webhooks
      security: [{ bearerAuth: [webhooks:read] }]
      responses:
        '200': { description: OK }

  /webhooks/{id}:
    delete:
      operationId: deleteWebhook
      summary: Removes a webhook
      security: [{ bearerAuth: [webhooks:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '204': { description: Removed }

  /webhooks/{id}/deliveries:
    get:
      operationId: listWebhookDeliveries
      summary: Real delivery history for one webhook, newest first
      security: [{ bearerAuth: [webhooks:read] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK }

  /rss-feeds:
    post:
      operationId: createRssFeed
      summary: Registers an RSS/Atom feed to auto-post new items
      security: [{ bearerAuth: [rss:write] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [url, selections]
              properties:
                url: { type: string }
                selections: { type: object, additionalProperties: { type: array, items: { type: string } }, description: 'Only image-capable platforms: facebook, instagram, threads, pinterest, bluesky' }
                captionTemplate: { type: string, default: "{title}\n\n{link}" }
                checkIntervalMinutes: { type: integer, default: 60, minimum: 15 }
      responses:
        '200': { description: OK }
    get:
      operationId: listRssFeeds
      summary: Lists your feeds
      security: [{ bearerAuth: [rss:read] }]
      responses:
        '200': { description: OK }

  /rss-feeds/{id}:
    patch:
      operationId: updateRssFeed
      summary: Updates active/selections/captionTemplate/checkIntervalMinutes
      security: [{ bearerAuth: [rss:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK }
    delete:
      operationId: deleteRssFeed
      security: [{ bearerAuth: [rss:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '204': { description: Removed }

  /rss-feeds/{id}/check-now:
    post:
      operationId: checkRssFeedNow
      summary: Queues an immediate check instead of waiting for the next interval
      security: [{ bearerAuth: [rss:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK }

  /recycling-rules:
    post:
      operationId: createRecyclingRule
      summary: Re-publishes one of your own already-published posts every N days
      security: [{ bearerAuth: [recycling:write] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [sourcePostId]
              properties:
                sourcePostId: { type: string, description: A post id from GET /posts that already published successfully }
                selections: { type: object, additionalProperties: { type: array, items: { type: string } } }
                intervalDays: { type: integer, default: 30, minimum: 1 }
                rewriteCaption: { type: boolean, default: false }
      responses:
        '200': { description: OK }
    get:
      operationId: listRecyclingRules
      summary: Lists your rules
      security: [{ bearerAuth: [recycling:read] }]
      responses:
        '200': { description: OK }

  /recycling-rules/{id}:
    patch:
      operationId: updateRecyclingRule
      summary: Updates active/selections/intervalDays/rewriteCaption
      security: [{ bearerAuth: [recycling:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK }
    delete:
      operationId: deleteRecyclingRule
      security: [{ bearerAuth: [recycling:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '204': { description: Removed }

  /recycling-rules/{id}/run-now:
    post:
      operationId: runRecyclingRuleNow
      summary: Queues an immediate recycle instead of waiting for the next interval
      security: [{ bearerAuth: [recycling:write] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK }
