{
  "openapi": "3.1.0",
  "info": {
    "title": "Preflight API",
    "version": "2026.10.06",
    "summary": "Schedule, validate and publish social media posts to twelve networks.",
    "description": "Preflight (preflight.social) is a social media scheduler that checks every post against each network's real publishing rules before it leaves. This is its REST API: upload a file, create and schedule a post to YouTube, TikTok, Instagram, Facebook, Threads, LinkedIn, Pinterest, Bluesky, Mastodon, Telegram, Discord and Tumblr, read its figures, act on comments, edit or take it down. The same workspace is reachable by AI assistants over MCP at https://preflight.social/mcp. Prose documentation: https://preflight.social/documentation. API keys and webhooks are on the Pro and Studio plans.",
    "termsOfService": "https://preflight.social/terms",
    "contact": { "name": "Preflight", "url": "https://preflight.social", "email": "info@preflight.social" }
  },
  "servers": [{ "url": "https://preflight.social/v1" }],
  "externalDocs": { "description": "Documentation", "url": "https://preflight.social/documentation" },
  "security": [{ "apiKey": [] }],
  "tags": [
    { "name": "Posts", "description": "A post and one target per network account it goes to." },
    { "name": "Targets", "description": "One network's copy of a post: its figures, comments and the live post." },
    { "name": "Validation", "description": "The capability sheet and the dry run every post is checked with." },
    { "name": "Media", "description": "Files, uploaded straight to storage with signed URLs or fetched from a public URL." },
    { "name": "Accounts", "description": "The connected social accounts and what each allows." },
    { "name": "AI", "description": "Captions per network and reviews of published posts." },
    { "name": "Webhooks", "description": "Subscriptions to post.published and post.failed." }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk_live_…",
        "description": "An API key from Settings → API keys (Pro and Studio plans). Scopes: posts:read, posts:write (drafts), posts:publish (anything that goes live), media:read, media:write, webhooks:read, webhooks:write, or * for everything. A missing scope answers 403 forbidden and names it."
      }
    },
    "parameters": {
      "postId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
      "targetId": { "name": "targetId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
      "mediaId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
      "accountId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "A connected account's id, from GET /oauth/accounts." }
    },
    "schemas": {
      "Platform": {
        "type": "string",
        "enum": ["youtube", "tiktok", "instagram", "facebook", "threads", "twitter", "linkedin", "pinterest", "bluesky", "mastodon", "telegram", "discord", "tumblr"],
        "description": "A network's name. twitter (X) is text-only and not offered to new accounts."
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "string", "description": "Stable: invalid_request, unauthorized, forbidden, not_found, post_limit, upload_limit, file_too_large, channel_limit, api_not_on_plan, ai_limit, ai_not_in_plan, account_not_connected, media_unusable, already_started, targets_locked, in_flight, nothing_to_retry, unsupported, not_published, in_use, video_on_youtube, payload_too_large, validation_failed, ai_declined, rate_limited, ai_unavailable." },
              "message": { "type": "string", "description": "Meant to be read by a person." },
              "field": { "type": "string", "description": "For invalid_request: which field." },
              "limit": { "type": "integer", "description": "For a plan refusal: the allowance." },
              "upgradeTo": { "type": "string", "description": "For a plan refusal: the first plan that would allow it." },
              "issues": { "type": "array", "items": { "$ref": "#/components/schemas/ValidationIssue" }, "description": "For validation_failed." }
            }
          }
        }
      },
      "ValidationIssue": {
        "type": "object",
        "required": ["severity", "message"],
        "properties": {
          "platform": { "$ref": "#/components/schemas/Platform", "description": "Absent for a problem with the post as a whole, such as a file." },
          "severity": { "type": "string", "enum": ["error", "warning"] },
          "message": { "type": "string" },
          "code": { "type": "string", "description": "For a plan limit: post_limit, upload_limit or channel_limit." }
        }
      },
      "TargetInput": {
        "type": "object",
        "required": ["platform"],
        "properties": {
          "platform": { "$ref": "#/components/schemas/Platform" },
          "accountId": { "type": "string", "format": "uuid", "description": "Which connected account, when the workspace has several for one network." },
          "options": {
            "type": "object",
            "additionalProperties": true,
            "description": "Per-network settings: youtube.title, tags, categoryId, privacy, playlistId, thumbnailMediaId, subtitles, firstComment; tiktok.privacy (required), allowComments, asDraft; instagram.placement (feed, reel, story), shareToFeed, collaborators; facebook.asReel, firstComment; threads.poll, thread; pinterest.boardId (required), title; bluesky/mastodon/twitter.thread; mastodon.visibility, spoilerText; discord.threadName; tumblr.tags; linkedin.firstComment; caption to override the text for this network. See /documentation#options."
          }
        }
      },
      "PostInput": {
        "type": "object",
        "required": ["targets"],
        "properties": {
          "content": { "type": "string", "maxLength": 64000, "description": "The text. On YouTube it is the description unless youtube.description is set." },
          "linkUrl": { "type": "string", "format": "uri" },
          "mediaIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "minItems": 1, "maxItems": 35, "description": "Files in order; they must be ready." },
          "altTexts": { "type": "array", "items": { "type": ["string", "null"], "maxLength": 1000 }, "description": "Alt text per file, same order as mediaIds." },
          "scheduledAt": { "type": "string", "format": "date-time", "description": "One instant for every channel." },
          "scheduledLocal": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$", "description": "The same wall-clock time read in each channel's own zone." },
          "queue": { "type": "boolean", "default": false, "description": "Each channel takes its next free slot from its posting schedule." },
          "publishNow": { "type": "boolean", "default": false },
          "saveAsDraft": { "type": "boolean", "default": false, "description": "Park it without a time. Needs only posts:write." },
          "targets": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/TargetInput" } }
        }
      },
      "Target": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "platform": { "$ref": "#/components/schemas/Platform" },
          "accountId": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["pending", "publishing", "published", "failed", "skipped"] },
          "remoteId": { "type": ["string", "null"] },
          "remoteUrl": { "type": ["string", "null"], "format": "uri" },
          "publishedAt": { "type": ["string", "null"], "format": "date-time" },
          "error": { "type": ["string", "null"] },
          "errorCode": { "type": ["string", "null"] },
          "errorPermanent": { "type": "boolean", "description": "Retrying cannot succeed without changing something." },
          "attempts": { "type": "integer" },
          "nextAttemptAt": { "type": ["string", "null"], "format": "date-time" },
          "canEditRemote": { "type": "boolean" },
          "canDeleteRemote": { "type": "boolean" },
          "options": { "type": "object", "additionalProperties": true },
          "metrics": { "type": ["object", "null"], "additionalProperties": true, "description": "The latest figures, with fetchedAt." }
        }
      },
      "Post": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["draft", "scheduled", "publishing", "done", "canceled"] },
          "content": { "type": "string" },
          "linkUrl": { "type": ["string", "null"] },
          "mediaIds": { "type": "array", "items": { "type": "string", "format": "uuid" } },
          "altTexts": { "type": "array", "items": { "type": ["string", "null"] } },
          "scheduledAt": { "type": ["string", "null"], "format": "date-time" },
          "approvalState": { "type": ["string", "null"], "description": "pending, approved or changes when a client approval link exists." },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" },
          "targets": { "type": "array", "items": { "$ref": "#/components/schemas/Target" } }
        }
      },
      "Media": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "kind": { "type": "string", "enum": ["image", "video"] },
          "status": { "type": "string", "enum": ["awaiting_upload", "ingesting", "processing", "ready", "failed", "deleted"] },
          "mimeType": { "type": ["string", "null"] },
          "sizeBytes": { "type": ["integer", "null"] },
          "durationSeconds": { "type": ["number", "null"] },
          "width": { "type": ["integer", "null"] },
          "height": { "type": ["integer", "null"] },
          "fps": { "type": ["number", "null"] },
          "fileName": { "type": ["string", "null"] },
          "previewUrl": { "type": ["string", "null"], "format": "uri", "description": "A short-lived link to the file." },
          "error": { "type": ["string", "null"] },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "MediaInput": {
        "type": "object",
        "properties": {
          "kind": { "type": "string", "enum": ["image", "video"] },
          "mimeType": { "type": "string", "examples": ["video/mp4", "image/jpeg"] },
          "sizeBytes": { "type": "integer", "description": "Exact size; required for a direct upload." },
          "fileName": { "type": "string" },
          "durationSeconds": { "type": "number", "description": "Measured by the client; lets a dry run warn about length limits." },
          "fps": { "type": "number" },
          "width": { "type": "integer" },
          "height": { "type": "integer" },
          "sourceUrl": { "type": "string", "format": "uri", "description": "A public https URL the server fetches instead of an upload." }
        }
      },
      "UploadInstructions": {
        "type": "object",
        "description": "Where to PUT the bytes. The file never passes through the API.",
        "properties": {
          "media": { "$ref": "#/components/schemas/Media" },
          "upload": {
            "type": "object",
            "properties": {
              "mode": { "type": "string", "enum": ["single", "multipart", "ingest"] },
              "uploadUrl": { "type": "string", "format": "uri", "description": "single: PUT the whole file here, then POST /media/{id}/uploaded." },
              "headers": { "type": "object", "additionalProperties": { "type": "string" } },
              "partSize": { "type": "integer", "description": "multipart: bytes per part; the last is shorter." },
              "parts": { "type": "array", "items": { "type": "object", "properties": { "partNumber": { "type": "integer" }, "url": { "type": "string", "format": "uri" } } }, "description": "multipart: PUT each part, keep its ETag, then POST /media/{id}/complete." }
            }
          }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "platform": { "$ref": "#/components/schemas/Platform" },
          "username": { "type": ["string", "null"] },
          "displayName": { "type": ["string", "null"] },
          "avatarUrl": { "type": ["string", "null"] },
          "connection": { "type": ["string", "null"], "description": "Instagram: direct or via_page." },
          "canDeleteRemote": { "type": "boolean" },
          "needsReconnect": { "type": "boolean" },
          "paused": { "type": "boolean", "description": "Connected, but beyond what the plan covers." },
          "timezone": { "type": ["string", "null"], "description": "IANA zone the channel's schedule is read in." }
        }
      },
      "PlatformCapabilities": {
        "type": "object",
        "description": "What one network accepts, as GET /platforms reports it.",
        "properties": {
          "platform": { "$ref": "#/components/schemas/Platform" },
          "name": { "type": "string" },
          "charLimit": { "type": "integer" },
          "requiresMedia": { "type": "string", "enum": ["none", "image", "video", "image_or_video"] },
          "supportsImage": { "type": "boolean" },
          "supportsVideo": { "type": "boolean" },
          "allowsEmptyText": { "type": "boolean" },
          "maxMediaPerPost": { "type": "integer" },
          "maxImageBytes": { "type": "integer" },
          "maxVideoBytes": { "type": "integer" },
          "minVideoDurationSeconds": { "type": "number" },
          "maxVideoDurationSeconds": { "type": "number" },
          "minVideoFps": { "type": "number" },
          "maxVideoFps": { "type": "number" },
          "supportsThread": { "type": "boolean" },
          "supportsAltText": { "type": "boolean" },
          "supportsThumbnail": { "type": "boolean" },
          "supportsFirstComment": { "type": "boolean" },
          "canEditRemote": { "type": "boolean" },
          "canDeleteRemote": { "type": "boolean" },
          "canReadComments": { "type": "boolean" },
          "reportsFollowers": { "type": "boolean" },
          "metricFields": { "type": "array", "items": { "type": "string" } },
          "notes": { "type": "string" }
        }
      },
      "Comment": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "author": { "type": ["string", "null"] },
          "text": { "type": "string" },
          "createdAt": { "type": ["string", "null"], "format": "date-time" },
          "own": { "type": "boolean", "description": "Written by the connected account." },
          "hidden": { "type": "boolean" },
          "likeCount": { "type": ["integer", "null"] }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "events": { "type": "array", "items": { "type": "string", "enum": ["post.published", "post.failed"] } },
          "secret": { "type": "string", "description": "whsec_…, shown once, on creation only." },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      }
    },
    "responses": {
      "BadRequest": { "description": "The body did not validate (invalid_request; field says which).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "Missing, expired or unrecognised key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "PaymentRequired": { "description": "The plan's allowance: post_limit, upload_limit, file_too_large, channel_limit, api_not_on_plan, ai_limit, ai_not_in_plan. Carries limit and upgradeTo.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "The key lacks the scope for this call.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "No such post, file or account in this workspace.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Conflict": { "description": "Not possible in the current state, or the network does not allow it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unprocessable": { "description": "A network would refuse it (validation_failed, with issues).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "TooMany": { "description": "Rate limited; retry after the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  },
  "paths": {
    "/platforms": {
      "get": {
        "tags": ["Validation"],
        "operationId": "listPlatforms",
        "summary": "The capability sheet: what each network accepts",
        "description": "No key needed. Character limits, media requirements, size and duration ceilings, what can be edited or deleted after publishing, and which figures each network reports.",
        "security": [],
        "responses": {
          "200": { "description": "One entry per network, keyed by platform.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/PlatformCapabilities" } } } } } } }
        }
      }
    },
    "/platforms/validate": {
      "post": {
        "tags": ["Validation"],
        "operationId": "validatePost",
        "summary": "Dry run: would each network accept this post?",
        "description": "The same checks POST /posts runs, without creating anything: text length the way each network counts it, hashtags, mentions, files, video length, fps, aspect ratio, per-network options and the plan's allowance. Any scope.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["targets"],
            "properties": {
              "content": { "type": "string" },
              "mediaIds": { "type": "array", "items": { "type": "string", "format": "uuid" } },
              "mediaItems": { "type": "array", "maxItems": 35, "items": { "type": "object", "required": ["kind"], "properties": { "kind": { "type": "string", "enum": ["image", "video"] }, "sizeBytes": { "type": "integer" }, "durationSeconds": { "type": "number" }, "fps": { "type": "number" }, "mimeType": { "type": "string" }, "width": { "type": "integer" }, "height": { "type": "integer" } } }, "description": "The files' facts, when they are not uploaded yet." },
              "altTexts": { "type": "array", "items": { "type": ["string", "null"] } },
              "targets": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/TargetInput" } }
            }
          } } }
        },
        "responses": {
          "200": { "description": "The issues, if any; ok is false when any is an error.", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "issues": { "type": "array", "items": { "$ref": "#/components/schemas/ValidationIssue" } } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/posts": {
      "get": {
        "tags": ["Posts"],
        "operationId": "listPosts",
        "summary": "Posts with their per-network state",
        "description": "Most recent first by when they went out. Anything still to come is always included. Scope posts:read.",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["draft", "scheduled", "publishing", "done", "canceled"] } },
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "With to: only posts that went out, or are due, in [from, to)." },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date-time" } },
          { "name": "days", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 730, "default": 120 }, "description": "Without a range: how far back." },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 200 } },
          { "name": "before", "in": "query", "schema": { "type": "string" }, "description": "The cursor from the previous page." }
        ],
        "responses": {
          "200": { "description": "A page of posts.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Post" } }, "nextCursor": { "type": ["string", "null"] } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "post": {
        "tags": ["Posts"],
        "operationId": "createPost",
        "summary": "Create, schedule, queue or publish a post",
        "description": "Checked against every target network first; a refusal is 422 validation_failed with the issues. Scope posts:write for a draft, posts:publish for anything that goes out. Send an Idempotency-Key header: a retried request returns the first post instead of creating a second.",
        "parameters": [{ "name": "Idempotency-Key", "in": "header", "schema": { "type": "string", "minLength": 8, "maxLength": 200 }, "description": "Unique per post, e.g. a UUID." }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostInput" } } } },
        "responses": {
          "201": { "description": "Created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } },
          "200": { "description": "The Idempotency-Key matched an earlier post, which is returned instead.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/Unprocessable" }
        }
      }
    },
    "/posts/metrics/refresh": {
      "post": {
        "tags": ["Posts"],
        "operationId": "refreshMetrics",
        "summary": "Fetch fresh figures for the last fortnight's posts",
        "responses": {
          "200": { "description": "Queued; the figures arrive on the posts shortly.", "content": { "application/json": { "schema": { "type": "object", "properties": { "queued": { "type": "integer" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/TooMany" }
        }
      }
    },
    "/posts/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/postId" }],
      "get": {
        "tags": ["Posts"],
        "operationId": "getPost",
        "summary": "One post and what happened on every network",
        "responses": {
          "200": { "description": "The post.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "tags": ["Posts"],
        "operationId": "updatePost",
        "summary": "Change content, files, time or networks of a post that has not gone out",
        "description": "Only the fields sent change. Giving a draft a time schedules it (posts:publish). Targets cannot change once any has started going out (409 targets_locked).",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostInput" } } } },
        "responses": {
          "200": { "description": "The post as it is now.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/Unprocessable" }
        }
      },
      "delete": {
        "tags": ["Posts"],
        "operationId": "deletePost",
        "summary": "Delete a draft, or cancel a scheduled post",
        "description": "Removes Preflight's record. What has already gone out stays on the networks; take it down per target first with DELETE /posts/{id}/targets/{targetId}/remote. Scope posts:write.",
        "responses": {
          "204": { "description": "Deleted." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/posts/{id}/retry": {
      "parameters": [{ "$ref": "#/components/parameters/postId" }],
      "post": {
        "tags": ["Posts"],
        "operationId": "retryPost",
        "summary": "Send the post again to the networks where it failed",
        "description": "Channels where it went out are left alone. Scopes posts:write and posts:publish. Check errorPermanent first: a permanent failure fails again until it is fixed.",
        "responses": {
          "200": { "description": "The post, with the failed targets pending again.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "nothing_to_retry or in_flight.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/posts/{id}/targets/{targetId}/metrics": {
      "parameters": [{ "$ref": "#/components/parameters/postId" }, { "$ref": "#/components/parameters/targetId" }],
      "post": {
        "tags": ["Targets"],
        "operationId": "fetchTargetMetrics",
        "summary": "Fetch this target's figures from the network now",
        "responses": {
          "200": { "description": "The latest figures.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/posts/{id}/targets/{targetId}/metrics/history": {
      "parameters": [{ "$ref": "#/components/parameters/postId" }, { "$ref": "#/components/parameters/targetId" }, { "name": "since", "in": "query", "schema": { "type": "string", "format": "date-time" } }],
      "get": {
        "tags": ["Targets"],
        "operationId": "targetMetricsHistory",
        "summary": "Every reading of this target's figures, oldest first",
        "responses": {
          "200": { "description": "Readings, at most the latest 500.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/posts/{id}/targets/{targetId}/insights": {
      "parameters": [{ "$ref": "#/components/parameters/postId" }, { "$ref": "#/components/parameters/targetId" }],
      "get": {
        "tags": ["Targets"],
        "operationId": "targetInsights",
        "summary": "YouTube retention curve and traffic sources",
        "responses": {
          "200": { "description": "Insights where the network reports them; supported false otherwise.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/posts/{id}/targets/{targetId}/comments": {
      "parameters": [{ "$ref": "#/components/parameters/postId" }, { "$ref": "#/components/parameters/targetId" }],
      "get": {
        "tags": ["Targets"],
        "operationId": "listComments",
        "summary": "Comments under this target, read live from the network",
        "responses": {
          "200": { "description": "The comments and what the account may do with them.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Comment" } }, "supported": { "type": "boolean" }, "can": { "type": "object", "properties": { "reply": { "type": "boolean" }, "edit": { "type": "boolean" }, "hide": { "type": "boolean" }, "like": { "type": "boolean" }, "delete": { "type": "boolean" } } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/posts/{id}/targets/{targetId}/comments/{commentId}": {
      "parameters": [{ "$ref": "#/components/parameters/postId" }, { "$ref": "#/components/parameters/targetId" }, { "name": "commentId", "in": "path", "required": true, "schema": { "type": "string" } }],
      "post": {
        "tags": ["Targets"],
        "operationId": "actOnComment",
        "summary": "Reply to, edit, hide, like or delete a comment",
        "description": "As the connected account, in public. Scope posts:publish.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["action"], "properties": { "action": { "type": "string", "enum": ["reply", "edit", "hide", "unhide", "like", "unlike", "delete"] }, "text": { "type": "string", "description": "For reply and edit." } } } } } },
        "responses": {
          "200": { "description": "Done; for a reply, the new comment.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } },
          "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" }
        }
      }
    },
    "/posts/{id}/targets/{targetId}/remote": {
      "parameters": [{ "$ref": "#/components/parameters/postId" }, { "$ref": "#/components/parameters/targetId" }],
      "patch": {
        "tags": ["Targets"],
        "operationId": "editPublishedPost",
        "summary": "Edit the live post on the network",
        "description": "Where canEditRemote is true: Facebook, YouTube, Mastodon, Telegram and Discord. Scopes posts:write and posts:publish.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string" }, "title": { "type": "string", "description": "YouTube only." } } } } } },
        "responses": {
          "200": { "description": "The target as it is now.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Target" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      },
      "delete": {
        "tags": ["Targets"],
        "operationId": "deletePublishedPost",
        "summary": "Take the live post down from the network",
        "description": "Where canDeleteRemote is true. The target is marked skipped and the record kept. Scopes posts:write and posts:publish.",
        "responses": {
          "200": { "description": "Taken down.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Target" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/posts/{id}/targets/{targetId}/account": {
      "parameters": [{ "$ref": "#/components/parameters/postId" }, { "$ref": "#/components/parameters/targetId" }],
      "patch": {
        "tags": ["Targets"],
        "operationId": "moveTarget",
        "summary": "Point this target at another account of the same network",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string", "format": "uuid" } } } } } },
        "responses": {
          "200": { "description": "Moved.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Target" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/media": {
      "get": {
        "tags": ["Media"],
        "operationId": "listMedia",
        "summary": "Files, newest first",
        "parameters": [{ "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } }],
        "responses": {
          "200": { "description": "Files, each with how many posts use it.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Media" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "tags": ["Media"],
        "operationId": "createMedia",
        "summary": "Declare a file and get upload instructions",
        "description": "Either sizeBytes for a direct upload (signed URLs come back: one PUT, or one per part for files over 16 MB) or sourceUrl for the server to fetch. Scope media:write. Up to 60 a minute.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MediaInput" } } } },
        "responses": {
          "201": { "description": "The record and where to put the bytes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadInstructions" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "413": { "description": "file_too_large: over the server's ceiling.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/TooMany" }
        }
      }
    },
    "/media/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/mediaId" }],
      "get": {
        "tags": ["Media"],
        "operationId": "getMedia",
        "summary": "A file's status, metadata and a preview link",
        "responses": {
          "200": { "description": "The file.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Media" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Media"],
        "operationId": "deleteMedia",
        "summary": "Delete the file; the record stays as deleted",
        "responses": {
          "200": { "description": "Deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Media" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "in_use: a post that has not finished going out still needs it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/media/{id}/uploaded": {
      "parameters": [{ "$ref": "#/components/parameters/mediaId" }],
      "post": {
        "tags": ["Media"],
        "operationId": "finishSingleUpload",
        "summary": "Finish a single-part upload",
        "responses": {
          "200": { "description": "The file, now processing or ready.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Media" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/media/{id}/complete": {
      "parameters": [{ "$ref": "#/components/parameters/mediaId" }],
      "post": {
        "tags": ["Media"],
        "operationId": "finishMultipartUpload",
        "summary": "Finish a multipart upload",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["parts"], "properties": { "parts": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["partNumber", "etag"], "properties": { "partNumber": { "type": "integer" }, "etag": { "type": "string" } } } } } } } } },
        "responses": {
          "200": { "description": "The file, now processing or ready.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Media" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/media/{id}/thumb": {
      "parameters": [{ "$ref": "#/components/parameters/mediaId" }],
      "get": {
        "tags": ["Media"],
        "operationId": "mediaThumbnail",
        "summary": "A small WebP preview",
        "responses": {
          "200": { "description": "The image.", "content": { "image/webp": { "schema": { "type": "string", "format": "binary" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/media/{id}/posts": {
      "parameters": [{ "$ref": "#/components/parameters/mediaId" }],
      "get": {
        "tags": ["Media"],
        "operationId": "mediaPosts",
        "summary": "The posts that use this file",
        "responses": {
          "200": { "description": "Posts.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Post" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/oauth/accounts": {
      "get": {
        "tags": ["Accounts"],
        "operationId": "listAccounts",
        "summary": "Connected accounts, with their zone and what the plan allows",
        "description": "Accounts are connected and disconnected in the app, not over the API. Scope posts:read.",
        "responses": {
          "200": { "description": "Accounts.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Account" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/oauth/audience": {
      "get": {
        "tags": ["Accounts"],
        "operationId": "audience",
        "summary": "Followers per account as daily readings",
        "parameters": [{ "name": "days", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 730, "default": 30 } }],
        "responses": {
          "200": { "description": "One series per account whose network reports followers.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "additionalProperties": true } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/oauth/accounts/{id}/boards": {
      "parameters": [{ "$ref": "#/components/parameters/accountId" }],
      "get": {
        "tags": ["Accounts"],
        "operationId": "pinterestBoards",
        "summary": "A Pinterest account's boards",
        "description": "Every pin needs one: pass its id as pinterest.boardId.",
        "responses": {
          "200": { "description": "Boards.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/oauth/accounts/{id}/youtube/playlists": {
      "parameters": [{ "$ref": "#/components/parameters/accountId" }],
      "get": {
        "tags": ["Accounts"],
        "operationId": "youtubePlaylists",
        "summary": "A YouTube channel's playlists",
        "responses": {
          "200": { "description": "Playlists.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "privacy": { "type": "string" } } } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      },
      "post": {
        "tags": ["Accounts"],
        "operationId": "createYoutubePlaylist",
        "summary": "Create a playlist on the channel",
        "description": "A playlist of the same name that already exists is returned instead. Scope posts:publish.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["title"], "properties": { "title": { "type": "string" }, "privacy": { "type": "string", "enum": ["public", "unlisted", "private"], "default": "public" } } } } } },
        "responses": {
          "201": { "description": "Created, or the existing one.", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "privacy": { "type": "string" }, "duplicate": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/oauth/accounts/{id}/tiktok/creator": {
      "parameters": [{ "$ref": "#/components/parameters/accountId" }],
      "get": {
        "tags": ["Accounts"],
        "operationId": "tiktokCreatorInfo",
        "summary": "What this TikTok creator may use right now",
        "description": "The privacy levels tiktok.privacy must be one of, whether comments, duets and stitches are off, and the longest video. TikTok asks for this before every post.",
        "responses": {
          "200": { "description": "The creator's settings.", "content": { "application/json": { "schema": { "type": "object", "properties": { "privacyLevels": { "type": "array", "items": { "type": "string" } }, "commentDisabled": { "type": "boolean" }, "duetDisabled": { "type": "boolean" }, "stitchDisabled": { "type": "boolean" }, "maxVideoDurationSeconds": { "type": "integer" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/oauth/accounts/{id}/mentions": {
      "parameters": [{ "$ref": "#/components/parameters/accountId" }, { "name": "q", "in": "query", "required": true, "schema": { "type": "string" } }],
      "get": {
        "tags": ["Accounts"],
        "operationId": "searchMentions",
        "summary": "Handles to mention, where the network offers a lookup",
        "responses": {
          "200": { "description": "Matches.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "handle": { "type": "string" }, "name": { "type": "string" } } } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/ai/captions": {
      "post": {
        "tags": ["AI"],
        "operationId": "aiCaptions",
        "summary": "Rewrite a caption for each network named",
        "description": "Pro: 300 a month, Studio: 1,500. Scope posts:write.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["content", "platforms"], "properties": { "content": { "type": "string" }, "platforms": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/Platform" } } } } } } },
        "responses": {
          "200": { "description": "One caption per network.", "content": { "application/json": { "schema": { "type": "object", "properties": { "captions": { "type": "object", "additionalProperties": { "type": "string" } } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "422": { "description": "ai_declined: the assistant would not write this.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/TooMany" },
          "503": { "description": "ai_unavailable.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/ai/review": {
      "post": {
        "tags": ["AI"],
        "operationId": "aiReview",
        "summary": "What a published post's figures say to change next time",
        "description": "Pro: 500 a month, Studio: 2,500. The stored review is returned unless refresh is true.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["targetId"], "properties": { "targetId": { "type": "string", "format": "uuid" }, "refresh": { "type": "boolean", "default": false } } } } } },
        "responses": {
          "200": { "description": "The review.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooMany" },
          "503": { "description": "ai_unavailable.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/ai/review/{targetId}": {
      "parameters": [{ "$ref": "#/components/parameters/targetId" }],
      "get": {
        "tags": ["AI"],
        "operationId": "getAiReview",
        "summary": "The last review of this target, free to read",
        "responses": {
          "200": { "description": "The review, or null.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/workspace/webhooks": {
      "get": {
        "tags": ["Webhooks"],
        "operationId": "listWebhooks",
        "summary": "Subscriptions",
        "description": "Scope webhooks:read.",
        "responses": {
          "200": { "description": "Subscriptions, without secrets.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Webhook" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "post": {
        "tags": ["Webhooks"],
        "operationId": "createWebhook",
        "summary": "Subscribe an https address to events",
        "description": "Scope webhooks:write. The secret is shown once. Deliveries carry x-schedulr-event, x-schedulr-id, x-schedulr-timestamp and x-schedulr-signature-v2 (t=…,v2=hex HMAC-SHA256 of `${t}.${body}`); answer 2xx within 10 seconds or it is retried five times.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "format": "uri", "description": "https and public." }, "events": { "type": "array", "items": { "type": "string", "enum": ["post.published", "post.failed"] }, "description": "Defaults to both." } } } } } },
        "responses": {
          "201": { "description": "Created, with the secret.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Webhook" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/workspace/webhooks/{id}": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
      "delete": {
        "tags": ["Webhooks"],
        "operationId": "deleteWebhook",
        "summary": "Unsubscribe",
        "responses": {
          "204": { "description": "Deleted." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "webhooks": {
    "post.published": {
      "post": {
        "summary": "One target is live",
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "event": { "type": "string", "const": "post.published" }, "createdAt": { "type": "string", "format": "date-time" }, "data": { "type": "object", "properties": { "postId": { "type": "string" }, "targetId": { "type": "string" }, "platform": { "$ref": "#/components/schemas/Platform" }, "remoteId": { "type": ["string", "null"] }, "remoteUrl": { "type": ["string", "null"] } } } } } } } },
        "responses": { "2XX": { "description": "Acknowledged." } }
      }
    },
    "post.failed": {
      "post": {
        "summary": "One target failed and retrying has stopped",
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "event": { "type": "string", "const": "post.failed" }, "createdAt": { "type": "string", "format": "date-time" }, "data": { "type": "object", "properties": { "postId": { "type": "string" }, "targetId": { "type": "string" }, "platform": { "$ref": "#/components/schemas/Platform" }, "error": { "type": "string" }, "permanent": { "type": "boolean" }, "attempts": { "type": "integer" } } } } } } } },
        "responses": { "2XX": { "description": "Acknowledged." } }
      }
    }
  }
}
