{
  "openapi": "3.1.0",
  "info": {
    "title": "PostEverywhere API Reference",
    "version": "1.0",
    "description": "Public v1 API for PostEverywhere — schedule and publish content across\nsocial platforms (X/Twitter, Instagram, Facebook, LinkedIn, YouTube,\nTikTok, Threads, Pinterest, Bluesky, Telegram, Discord).\n\nNew here? Start with the [Quick Start](/quick-start) (first post in 5\nminutes) and [Authentication](/authentication). Platform-specific\nbehavior lives in the [Platform Guides](/platforms/instagram), and\nmedia constraints in [Media Requirements](/media-requirements).\n\n## Authentication\nAll endpoints (except the device-login pair under `/auth/device`) accept an\nAPI key in the `Authorization` header:\n\n```\nAuthorization: Bearer pe_live_...\n```\n\nKeys are created in the dashboard (Developers) or minted by the CLI device\nflow (`POST /auth/device` → `POST /auth/device/token`). Keys carry scopes\n(`read`, `write`, `ai`); each operation's required scope is noted via\n`x-scope`. A missing scope returns `403 insufficient_scope`. A browser\nsession cookie also works as a fallback for interactive testing; scopes are\nthen implicitly `read, write, ai` and API-key rate limits are skipped.\n\nAuth failures return `401` with codes `invalid_api_key`, `api_key_revoked`,\nor `api_key_expired`. Organizations without an active subscription receive\n`402 subscription_required` (the error object additionally carries\n`subscription_status`).\n\n## Response envelope\nEvery endpoint responds with the same JSON envelope:\n\n```json\n{ \"data\": { ... }, \"error\": null, \"meta\": { \"request_id\": \"1a2b3c4d\", \"timestamp\": \"2026-07-08T09:00:00.000Z\" } }\n```\n\nOn failure `data` is `null` and `error` is populated:\n\n```json\n{ \"data\": null,\n  \"error\": { \"message\": \"...\", \"code\": \"validation_error\", \"retryable\": false, \"details\": { } },\n  \"meta\": { \"request_id\": \"1a2b3c4d\", \"timestamp\": \"...\" } }\n```\n\n`error.retryable` is `true` for transient failures (HTTP 5xx and 429) and\n`false` for permanent ones — clients should not blindly retry 4xx.\nException: the auth layer's own 401/402 responses omit `retryable` and\n`meta.request_id` (they carry only `meta.timestamp`).\n\n## Rate limiting\nAPI keys are limited to 60 requests/minute and 1,000 requests/hour.\nWhen the API-key limit is hit the `429` response includes\n`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`\n(unix seconds) headers. Every `429` (including posting-budget and\nAI-budget limits, which are separate from the API-key limit) includes a\n`Retry-After` header (seconds, default 30).\n\nPosting budgets (per user): immediate publishes 20/min, 200/hour,\n1,000/day; scheduled posts 100/min, 1,000/hour with no daily cap.\nMedia uploads: 60/hour, 300/day (counted at `/media/{id}/complete` or\n`/media/upload-from-url`, not at presign).\n\n## Timestamps\nAll timestamps are ISO 8601. `scheduled_for` request values without an\nexplicit offset are interpreted as UTC. The `timezone` field is display\nmetadata only — it never changes when a post fires.\n",
    "contact": {
      "name": "PostEverywhere Developers",
      "url": "https://developers.posteverywhere.ai"
    }
  },
  "servers": [
    {
      "url": "https://app.posteverywhere.ai/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Introspection",
      "description": "Ask the API who you are — key scopes, organization, quota, and usage. Lets SDKs, CLIs, and AI agents self-configure from a bare API key."
    },
    {
      "name": "Accounts",
      "description": "List and inspect connected social accounts across all 11 platforms, including per-account publish health and reconnect status."
    },
    {
      "name": "Posts",
      "description": "Create, schedule, draft, retry, and inspect posts across 11 social platforms — single posts, bulk batches of 50, and per-platform content overrides."
    },
    {
      "name": "Media",
      "description": "Upload images, video, and PDFs — direct, presigned three-step, or imported straight from a URL — then attach them to posts on any platform."
    },
    {
      "name": "AI",
      "description": "Generate platform-tuned social media captions and AI images from the API, using your organization's AI credit allowance."
    },
    {
      "name": "Analytics",
      "description": "Pull cross-platform performance summaries — impressions, likes, comments, and per-post results — for reporting and dashboards."
    },
    {
      "name": "Campaigns",
      "description": "Group posts into campaigns to tag, filter, and report on related content across platforms."
    },
    {
      "name": "Webhooks",
      "description": "Subscribe to real-time post lifecycle events (published, failed, updated) with signed deliveries and automatic retries."
    },
    {
      "name": "Auth",
      "description": "Device-grant login and browser OAuth handoff — connect social accounts from CLIs, servers, and AI agents without a dashboard session."
    },
    {
      "name": "Platform Rules",
      "description": "Per-platform composer rules — character limits, media constraints, and feature support — so clients can validate before publishing."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "x-scope": "read",
        "tags": [
          "Introspection"
        ],
        "summary": "Get current API key context, organization, quota and usage",
        "description": "Returns who the key belongs to, its scopes, the organization's\nsubscription state, quota usage (accounts, AI credits, storage,\nseats) and post counts. Lets SDK/MCP clients self-configure without\nbeing handed `organization_id` etc.\n",
        "responses": {
          "200": {
            "description": "Current context",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Me"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/accounts": {
      "get": {
        "operationId": "listAccounts",
        "x-scope": "read",
        "tags": [
          "Accounts"
        ],
        "summary": "List connected social accounts",
        "description": "Returns all active social accounts in the organization with a brief\nhealth block per account. Not paginated. Facebook-flow Instagram rows\nthat have an Instagram-direct sibling are excluded (they are hidden in\nthe dashboard too).\n",
        "responses": {
          "200": {
            "description": "Accounts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "accounts"
                      ],
                      "properties": {
                        "accounts": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SocialAccount"
                          }
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                },
                "examples": {
                  "accounts": {
                    "value": {
                      "data": {
                        "accounts": [
                          {
                            "id": 2280,
                            "platform": "instagram",
                            "account_name": "acme.co",
                            "avatar_url": "https://cdn.posteverywhere.ai/avatars/2280.jpg",
                            "is_active": true,
                            "created_at": "2026-05-01T10:00:00.000Z",
                            "health": {
                              "status": "healthy",
                              "can_post": true,
                              "needs_reconnection": false
                            }
                          }
                        ]
                      },
                      "error": null,
                      "meta": {
                        "request_id": "9f2c1a3b",
                        "timestamp": "2026-07-08T09:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/accounts/{id}": {
      "get": {
        "operationId": "getAccount",
        "x-scope": "read",
        "tags": [
          "Accounts"
        ],
        "summary": "Get a single connected social account",
        "description": "Account IDs are integers (as returned by `GET /accounts`), not UUIDs.\nNon-numeric IDs return `400 invalid_id`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccountId"
          }
        ],
        "responses": {
          "200": {
            "description": "Account",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/SocialAccount"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/accounts/{id}/health": {
      "get": {
        "operationId": "getAccountHealth",
        "x-scope": "read",
        "tags": [
          "Accounts"
        ],
        "summary": "Get a detailed health report for an account",
        "description": "Tells clients whether an account is currently usable (token alive, no\nreconnection required, recent publish activity). `status` verdicts:\n`healthy` (post away), `warning` (`recent_permanent_failures` or\n`token_expiring_soon` — posting still allowed), `broken`\n(`account_inactive`, `needs_reconnection` or `token_expired` —\n`can_post` is false).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccountId"
          }
        ],
        "responses": {
          "200": {
            "description": "Health report",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AccountHealthReport"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/accounts/connect-link": {
      "post": {
        "operationId": "createConnectLink",
        "x-scope": "write",
        "tags": [
          "Accounts"
        ],
        "summary": "Mint an OAuth connect link for a new social account",
        "description": "Returns a short-lived (10 minute) authorization URL for connecting a\nNEW account on an OAuth platform: `x`, `instagram`, `facebook`,\n`youtube`, `pinterest`, `threads`, `linkedin`, `tiktok`. Hand the URL\nto the account owner; they approve in any browser (logged in to\nPostEverywhere or not, identity is carried in the signed state), and\nthe account lands in this organization. Poll `GET /accounts` until it\nappears. For `telegram`, `discord`, `bluesky` use\n`POST /accounts/connect-credential` instead, no browser needed.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "platform"
                ],
                "properties": {
                  "platform": {
                    "type": "string",
                    "enum": [
                      "x",
                      "instagram",
                      "facebook",
                      "youtube",
                      "pinterest",
                      "threads",
                      "linkedin",
                      "tiktok"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Connect link minted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "platform": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string",
                          "description": "Authorization URL to open in a browser"
                        },
                        "expires_in_seconds": {
                          "type": "integer",
                          "example": 600
                        },
                        "instructions": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/accounts/connect-credential": {
      "post": {
        "operationId": "connectCredentialAccount",
        "x-scope": "write",
        "tags": [
          "Accounts"
        ],
        "summary": "Connect telegram, discord, or bluesky directly with credentials",
        "description": "Connects a credential-based platform entirely through the API, no\nbrowser involved. Credentials are validated live against the platform\nbefore saving; invalid credentials return a 400 with the reason.\nRe-submitting credentials for an already-connected account updates it\nin place (this is also how these platforms reconnect).\nRequired fields by platform: telegram needs `bot_token` (from\n@BotFather; the bot must be an admin of the channel) and `channel`\n(@username or numeric chat id); discord needs `webhook_url`; bluesky\nneeds `handle` and `app_password` (an app password, never the main\naccount password).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "platform"
                ],
                "properties": {
                  "platform": {
                    "type": "string",
                    "enum": [
                      "telegram",
                      "discord",
                      "bluesky"
                    ]
                  },
                  "bot_token": {
                    "type": "string",
                    "description": "telegram only"
                  },
                  "channel": {
                    "type": "string",
                    "description": "telegram only: @username or chat id"
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "discord only"
                  },
                  "handle": {
                    "type": "string",
                    "description": "bluesky only, e.g. me.bsky.social"
                  },
                  "app_password": {
                    "type": "string",
                    "description": "bluesky only"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account connected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "platform": {
                          "type": "string"
                        },
                        "connected": {
                          "type": "boolean"
                        },
                        "account": {
                          "type": "object",
                          "nullable": true
                        },
                        "instructions": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/accounts/{id}/reconnect": {
      "post": {
        "operationId": "createReconnectLink",
        "x-scope": "write",
        "tags": [
          "Accounts"
        ],
        "summary": "Mint an OAuth link to repair an existing broken account",
        "description": "Returns a short-lived (10 minute) authorization URL that repairs an\nEXISTING OAuth account whose token died (`needs_reconnection` or\n`token_expired` from the health endpoint). The owner must approve\nwhile logged in to the platform as that same profile; matching is by\nthe platform's account id, so authorizing as a different profile\ncreates a new account instead. Verify with\n`GET /accounts/{id}/health` afterwards. Credential platforms\n(telegram, discord, bluesky) reconnect by re-submitting credentials\nvia `POST /accounts/connect-credential`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccountId"
          }
        ],
        "responses": {
          "200": {
            "description": "Reconnect link minted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "account_id": {
                          "type": "integer"
                        },
                        "platform": {
                          "type": "string"
                        },
                        "account_name": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        },
                        "expires_in_seconds": {
                          "type": "integer",
                          "example": 600
                        },
                        "instructions": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/posts": {
      "get": {
        "operationId": "listPosts",
        "x-scope": "read",
        "tags": [
          "Posts"
        ],
        "summary": "List posts with filters",
        "description": "Post status is derived live from per-destination statuses (the raw\n`posts.post_status` column is frozen at creation). Multi-value filters\n(`status`, `platform`) accept comma-separated lists. Invalid filter\nvalues are silently dropped, not rejected.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Comma-separated list of: draft, scheduled, publishing, published, partially_failed, failed",
            "schema": {
              "type": "string"
            },
            "example": "scheduled,publishing"
          },
          {
            "name": "platform",
            "in": "query",
            "description": "Comma-separated list of: instagram, facebook, x, twitter, linkedin, youtube, tiktok, threads, pinterest, bluesky, telegram, discord",
            "schema": {
              "type": "string"
            },
            "example": "instagram,tiktok"
          },
          {
            "name": "account_id",
            "in": "query",
            "description": "Filter to posts targeting this social account (integer ID)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "campaign_id",
            "in": "query",
            "description": "Filter to posts in this campaign (integer ID)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "created_after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "created_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "scheduled_after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "scheduled_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "published_after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "published_before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "updated_after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive substring match on post content",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "scheduled_for",
                "updated_at",
                "published_at"
              ],
              "default": "created_at"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100,
              "minimum": 1
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "approval",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "changes_requested"
              ]
            },
            "description": "Filter by approval workflow state. Separate axis from status: a pending post is also a draft."
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated posts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "posts",
                        "pagination"
                      ],
                      "properties": {
                        "posts": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Post"
                          }
                        },
                        "pagination": {
                          "$ref": "#/components/schemas/Pagination"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "operationId": "createPost",
        "x-scope": "write",
        "tags": [
          "Posts"
        ],
        "summary": "Create, schedule, or draft a post",
        "description": "Omit `scheduled_for` to publish immediately; provide it (ISO 8601,\nassumed UTC when no offset given) to schedule. Set `draft: true` to\nsave a native draft instead (no destinations are created and nothing\npublishes until `POST /posts/{id}/schedule`).\n\nAttach media by id (see [media requirements per platform](/media-requirements)),\nand use `platform_content` for per-platform overrides — the\n[Platform Guides](/platforms/instagram) document every option.\n\nField-name handling: camelCase aliases (`accountIds`, `mediaIds`,\n`scheduledFor`, `platformContent`) and the deprecated `scheduled_at`\nalias are accepted. Fields whose names imply a different shape\n(`media`, `media_id`, `mediaId`, `attachments`) are rejected with\n`400 invalid_field_name`.\n\nContent rules: `content` may be omitted only when `platform_content`\nprovides at least one per-platform caption or media is attached.\nMax 10,000 characters. Max 50 `account_ids`. Instagram captions with\nmore than 30 hashtags are rejected\n(`instagram_hashtag_limit_exceeded`).\n\nMedia referenced by `media_ids` must exist in the workspace and be\n`ready` (`media_not_found` / `media_not_ready` otherwise; media stuck\nin `uploading` is auto-finalized via the `/complete` flow when the\nfile was actually uploaded). Platform media requirements are\nvalidated pre-flight (`platform_requirement_violation`, e.g. video to\nPinterest).\n\nA circuit breaker refuses a request body that has already failed\n5+ times in 6 hours for the same key with\n`422 permanent_failure_circuit_breaker` — change any field to reset.\n\nError codes: `invalid_json`, `wrong_content_type_for_endpoint`,\n`invalid_field_name`, `validation_error`, `content_too_long`,\n`account_ids_required`, `too_many_accounts`, `invalid_timezone`,\n`invalid_accounts`, `instagram_hashtag_limit_exceeded`,\n`invalid_datetime`, `past_schedule_time`, `media_not_found`,\n`media_not_ready`, `platform_requirement_violation`,\n`post_creation_failed`, `rate_limit_exceeded`,\n`permanent_failure_circuit_breaker`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePostRequest"
              },
              "examples": {
                "scheduled": {
                  "summary": "Schedule to two accounts",
                  "value": {
                    "content": "Big news dropping tomorrow 👀",
                    "account_ids": [
                      2280,
                      2291
                    ],
                    "scheduled_for": "2026-07-10T14:30:00Z",
                    "timezone": "America/New_York",
                    "media_ids": [
                      "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    ]
                  }
                },
                "publish_now": {
                  "summary": "Publish immediately",
                  "value": {
                    "content": "We're live!",
                    "account_ids": [
                      2280
                    ]
                  }
                },
                "draft": {
                  "summary": "Save a draft for review",
                  "value": {
                    "content": "Draft for the launch thread",
                    "draft": true,
                    "account_ids": [
                      2280
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Post created. Regular creates return the full publishing topology\n(`destinations`); `draft: true` returns the smaller draft shape\nwith `next_steps`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/PostCreated"
                        },
                        {
                          "$ref": "#/components/schemas/DraftCreated"
                        }
                      ]
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Circuit breaker open for this exact request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/posts/{id}": {
      "get": {
        "operationId": "getPost",
        "x-scope": "read",
        "tags": [
          "Posts"
        ],
        "summary": "Get a post with destination statuses",
        "description": "For drafts, the chosen accounts and per-platform content are surfaced\nfrom the draft data and `scheduled_for` is `null` unless pre-set.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/PostId"
          }
        ],
        "responses": {
          "200": {
            "description": "Post",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Post"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "patch": {
        "operationId": "updatePost",
        "x-scope": "write",
        "tags": [
          "Posts"
        ],
        "summary": "Update a scheduled or draft post",
        "description": "Only posts whose derived status is `scheduled` or `draft` can be\nedited (`400 invalid_post_status` otherwise). At least one field is\nrequired. Changing `account_ids` rebuilds all destinations; changing\nonly `scheduled_for` shifts every queued destination by the same\ndelta. Same alias and wrong-field-name rules as `createPost`.\nError codes: `validation_error`, `invalid_post_status`,\n`content_too_long`, `too_many_accounts`, `invalid_timezone`,\n`invalid_datetime`, `past_schedule_time`, `invalid_accounts`,\n`invalid_field_name`, `invalid_id`, `not_found`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/PostId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "content": {
                    "type": "string",
                    "maxLength": 10000
                  },
                  "scheduled_for": {
                    "type": "string",
                    "format": "date-time",
                    "description": "New fire time (ISO 8601, assumed UTC without offset, must be future)"
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA timezone (display only)"
                  },
                  "account_ids": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "integer"
                    }
                  },
                  "media_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              },
              "example": {
                "scheduled_for": "2026-07-11T09:00:00Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated post",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/UpdatedPost"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deletePost",
        "x-scope": "write",
        "tags": [
          "Posts"
        ],
        "summary": "Delete a post and all its destinations",
        "description": "Permanent. Fires the `post.deleted` webhook event.",
        "parameters": [
          {
            "$ref": "#/components/parameters/PostId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "deleted",
                        "id"
                      ],
                      "properties": {
                        "deleted": {
                          "type": "boolean",
                          "const": true
                        },
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/posts/{id}/results": {
      "get": {
        "operationId": "getPostResults",
        "x-scope": "read",
        "tags": [
          "Posts"
        ],
        "summary": "Get per-platform publish results for a post",
        "description": "Poll this after an immediate publish. Each destination's status moves\nfrom `queued` → `publishing` → `published` or `failed`;\n`platform_post_url` is set after a successful publish.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/PostId"
          }
        ],
        "responses": {
          "200": {
            "description": "Publish results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "post_id",
                        "post_status",
                        "results"
                      ],
                      "properties": {
                        "post_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "post_status": {
                          "$ref": "#/components/schemas/PostStatus"
                        },
                        "results": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PostResult"
                          }
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/posts/{id}/retry": {
      "post": {
        "operationId": "retryPost",
        "x-scope": "write",
        "tags": [
          "Posts"
        ],
        "summary": "Retry all failed destinations of one post",
        "description": "Resets every `failed` destination of the post to `queued` with a fresh\nattempt budget and `scheduled_for = now` (publishes on the next\nscheduler cycle). Returns `400 no_failed_destinations` when nothing is\nin a failed state. Counts against the immediate-publish posting\nbudget.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/PostId"
          }
        ],
        "responses": {
          "200": {
            "description": "Destinations queued for retry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "retried_count",
                        "destinations"
                      ],
                      "properties": {
                        "retried_count": {
                          "type": "integer"
                        },
                        "destinations": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "platform": {
                                "type": "string"
                              },
                              "new_status": {
                                "type": "string",
                                "const": "queued"
                              }
                            }
                          }
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/posts/{id}/schedule": {
      "post": {
        "operationId": "schedulePost",
        "x-scope": "write",
        "tags": [
          "Posts"
        ],
        "summary": "Schedule (publish) a draft",
        "description": "Turns a draft into a live scheduled/publishing post: flips the post to\n`scheduled`, clears draft data, and creates one queued destination per\naccount. Provide either `scheduled_for` (at least 1 minute in the\nfuture) or `publish_now: true` (queued to fire within ~1 scheduler\ncycle). `account_ids` overrides the draft's saved targets; required if\nthe draft has none. Non-drafts return `409 not_a_draft` — use\n`PATCH /posts/{id}` to move an already-scheduled post.\nPlan post limits are enforced here\n(`429 post_limit_reached` / `daily_limit_reached`).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/PostId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scheduled_for": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Required unless publish_now is true"
                  },
                  "publish_now": {
                    "type": "boolean"
                  },
                  "account_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "timezone": {
                    "type": "string"
                  }
                }
              },
              "examples": {
                "schedule": {
                  "value": {
                    "scheduled_for": "2026-07-12T14:00:00Z"
                  }
                },
                "now": {
                  "value": {
                    "publish_now": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draft scheduled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "post_id",
                        "status",
                        "scheduled_for",
                        "destinations"
                      ],
                      "properties": {
                        "post_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "id": {
                          "type": "string",
                          "format": "uuid",
                          "deprecated": true
                        },
                        "status": {
                          "type": "string",
                          "const": "scheduled"
                        },
                        "post_status": {
                          "type": "string",
                          "const": "scheduled"
                        },
                        "publish_now": {
                          "type": "boolean"
                        },
                        "scheduled_for": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "timezone": {
                          "type": "string"
                        },
                        "destinations": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "account_id": {
                                "type": "integer"
                              },
                              "account_name": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "platform": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string",
                                "description": "queued, or failed if the destination row could not be created"
                              },
                              "error": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "destinations_count": {
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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": {
            "description": "Post is not a draft",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/posts/bulk": {
      "post": {
        "operationId": "bulkCreatePosts",
        "x-scope": "write",
        "tags": [
          "Posts"
        ],
        "summary": "Create up to 50 posts in one call",
        "description": "Each item is dispatched through the same validation as `createPost`\n(sequentially, so posting rate limits stay deterministic). The bulk\ncall counts as one API-rate-limit hit; per-post posting budgets still\napply. Partial success is normal — iterate `results`.\nHTTP status: 201 all succeeded, 207 mixed, 422 all failed.\n",
        "x-todo": "BUG in handler as written: the parse guard tests `parsed.ok` / `parsed.error`, which do not exist on parseJsonBody's return ({body}|{response}) — every valid-JSON request short-circuits to `return undefined`, which Next.js turns into a framework 500 (E985 \"No response is returned\"). The contract below describes the code past that line; verify in prod and fix the guard to `if (parsed.response) return parsed.response`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "posts"
                ],
                "properties": {
                  "posts": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 50,
                    "items": {
                      "$ref": "#/components/schemas/CreatePostRequest"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "All posts created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkResultEnvelope"
                }
              }
            }
          },
          "207": {
            "description": "Partial success — some items failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkResultEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Every item failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkResultEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/posts/retry-failed": {
      "post": {
        "operationId": "retryFailedPosts",
        "x-scope": "write",
        "tags": [
          "Posts"
        ],
        "summary": "Bulk-retry failed destinations by filter",
        "description": "Retries every failed destination matching the filter (at least one\nfilter is required — the API refuses to retry the entire failure\nhistory, `400 no_filter`). Matched destinations are reset to `queued`\nwith attempts zeroed and `scheduled_for = now`. `post_ids` is capped\nat 200 (`bulk_limit_exceeded`). `failed_after` / `failed_before`\ncompare against the destination's last update time. Counts against\nthe immediate-publish posting budget.\n",
        "x-todo": "BUG in handler as written: same `parsed.ok` / `parsed.error` guard issue as bulkCreatePosts — valid-JSON requests currently short-circuit to a framework 500 before any of the documented logic runs.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "At least one of post_ids, account_id, platform, failed_after, failed_before is required",
                "properties": {
                  "post_ids": {
                    "type": "array",
                    "maxItems": 200,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "account_id": {
                    "type": "integer"
                  },
                  "platform": {
                    "type": "string",
                    "enum": [
                      "instagram",
                      "facebook",
                      "x",
                      "twitter",
                      "linkedin",
                      "youtube",
                      "tiktok",
                      "threads",
                      "pinterest",
                      "bluesky",
                      "telegram",
                      "discord"
                    ]
                  },
                  "failed_after": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "failed_before": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "max_attempts": {
                    "type": "integer",
                    "description": "Skip destinations that already have this many attempts or more"
                  }
                }
              },
              "example": {
                "platform": "tiktok",
                "failed_after": "2026-07-07T00:00:00Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Destinations queued for retry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "retried_count",
                        "destinations"
                      ],
                      "properties": {
                        "retried_count": {
                          "type": "integer"
                        },
                        "destinations": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "destination_id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "post_id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "platform": {
                                "type": "string"
                              },
                              "new_status": {
                                "type": "string",
                                "const": "queued"
                              }
                            }
                          }
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/media": {
      "get": {
        "operationId": "listMedia",
        "x-scope": "read",
        "tags": [
          "Media"
        ],
        "summary": "List media library items",
        "description": "Newest first. `pagination` echoes `limit`/`offset` only (no total).\nThe convenience `media_ids` array can be copied straight into\n`createPost`. Viewable URLs are not included in the list response —\nuse `getMedia` for a per-item `url`.\n",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "video",
                "document"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Media items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "media",
                        "media_ids",
                        "pagination"
                      ],
                      "properties": {
                        "media": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/MediaItem"
                          }
                        },
                        "media_ids": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "uuid"
                          }
                        },
                        "pagination": {
                          "type": "object",
                          "properties": {
                            "limit": {
                              "type": "integer"
                            },
                            "offset": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/media/upload": {
      "post": {
        "operationId": "initMediaUpload",
        "x-scope": "write",
        "tags": [
          "Media"
        ],
        "summary": "Upload media (direct or presigned)",
        "description": "TWO modes, selected by the request Content-Type.\n\n**DIRECT mode (images only, recommended for API clients).** Send\n`multipart/form-data` with the file in a field named `file` (optional\n`filename` field to override the name). The upload completes in this\nsingle request and the response returns `media_status: \"ready\"` with\n`upload_required: false`, no `upload_url`, no `/complete` call.\nMax 25 MB. Videos sent this way get `415` with instructions.\n\n**PRESIGNED mode (all types).** Send file METADATA as JSON. Returns an\n`upload_url` plus `upload_method` describing how to send the bytes:\n  - images → POST `multipart/form-data`, field name `file`\n    (Cloudflare Images direct upload)\n  - videos (MP4 only) and PDFs → PUT with the declared `Content-Type`\n    (R2 presigned URL)\n\nThen you MUST call `POST /media/{media_id}/complete`, or the media stays\n`uploading` forever otherwise. `expires_at` is authoritative (image\nURLs last 6 hours, video/PDF URLs 1 hour). Size caps: 20 MB\nimages/PDFs, 500 MB video. Upload quota is only consumed at\n`/complete`, so retries here are free.\n\n**Presigned image caveat**: the image upload host runs an edge bot\nfilter outside our control. Send any custom `User-Agent` header;\nPython stdlib's default (`Python-urllib/x`) is rejected with HTTP 403\nerror 1010. `python-requests`, `curl`, node, Go and empty User-Agents\nall pass. DIRECT mode has no such restriction from any client.\n\nError codes: `validation_error`, `unsupported_media_type`,\n`file_too_large` (413), `length_required` (411, direct mode),\n`storage_quota_exceeded` (403), `rate_limit_exceeded`,\n`service_unavailable`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "content_type",
                  "size"
                ],
                "properties": {
                  "filename": {
                    "type": "string",
                    "maxLength": 255
                  },
                  "content_type": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png",
                      "image/gif",
                      "image/webp",
                      "image/heic",
                      "image/heif",
                      "video/mp4",
                      "application/pdf"
                    ]
                  },
                  "size": {
                    "type": "integer",
                    "description": "File size in bytes"
                  },
                  "width": {
                    "type": "integer"
                  },
                  "height": {
                    "type": "integer"
                  },
                  "duration": {
                    "type": "number",
                    "description": "Video duration in seconds"
                  }
                }
              },
              "example": {
                "filename": "photo.jpg",
                "content_type": "image/jpeg",
                "size": 245361
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The image itself (JPEG/PNG/GIF/WebP/HEIC, max 25 MB). Direct mode is image-only."
                  },
                  "filename": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Optional display-name override."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Presigned mode: an upload ticket (`upload_url` + `upload_method`, then call `/complete`).\nDirect mode: the finished media, `media_status: \"ready\"`, `upload_required: false`, `url` set; attach `media_ids` to a post immediately.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/MediaUploadTicket"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/media/upload-from-url": {
      "post": {
        "operationId": "uploadMediaFromUrl",
        "x-scope": "write",
        "tags": [
          "Media"
        ],
        "summary": "One-shot import of a public image or MP4 video URL",
        "description": "Fetches the URL server-side and stores it — no presign/PUT/complete\ndance.\n\n**Images** (JPEG/PNG/GIF/WebP/HEIC/HEIF, max 25 MB) import\nsynchronously: the response `media_id` is ready immediately.\n\n**Videos** (MP4 only, max 500 MB) import ASYNCHRONOUSLY: the response\nreturns instantly with `media_status: \"uploading\"` while the file\nstreams in server-side. Poll `GET /media/{id}` until `media_status`\nis `ready` (typically seconds), then attach via `media_ids`. If it\nbecomes `failed`, the poll response's `error_message` states exactly\nwhy (too large, not actually an MP4, storage quota, source URL died).\nOversize videos with a Content-Length return `413` up front; storage\nquota returns `403 storage_quota_exceeded`.\n\n30 s fetch timeout to first byte, SSRF-guarded including redirects\n(no localhost/private addresses, `400 invalid_url`). Unreachable or\nslow URLs return `502`/`504` `fetch_failed`. Consumes the media\nupload quota.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Publicly fetchable http(s) image or MP4 video URL"
                  },
                  "filename": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "url": "https://example.com/images/launch-hero.png"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Imported. Images return media_status \"ready\"; videos return \"uploading\" — poll `GET /media/{id}` until ready before attaching.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "media_id",
                        "media_status",
                        "type"
                      ],
                      "properties": {
                        "media_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "media_ids": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "uuid"
                          }
                        },
                        "media_status": {
                          "type": "string",
                          "enum": [
                            "ready",
                            "uploading"
                          ]
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "image",
                            "video"
                          ]
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "filename": {
                          "type": "string"
                        },
                        "size": {
                          "type": "integer"
                        },
                        "content_type": {
                          "type": "string"
                        },
                        "source_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "next_step": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "description": "Downloaded file exceeds 25 MB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "415": {
            "description": "Video URL — use the 3-step upload flow instead",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "description": "Source URL unreachable or upstream image service unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "504": {
            "description": "Source URL timed out (over 30 s)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/media/{id}": {
      "get": {
        "operationId": "getMedia",
        "x-scope": "read",
        "tags": [
          "Media"
        ],
        "summary": "Get media status and details",
        "description": "`media_status` stays `uploading` until you call `/complete` — polling\nthis endpoint alone will never flip it to `ready`. `action_required`\ntells clients the next step when not ready. `url` is a permanent\nviewable URL, populated once ready.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MediaId"
          }
        ],
        "responses": {
          "200": {
            "description": "Media details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/MediaDetail"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deleteMedia",
        "x-scope": "write",
        "tags": [
          "Media"
        ],
        "summary": "Delete a media file",
        "description": "If the media is attached to scheduled posts the delete is refused with\n`409 media_in_use` unless `?force=true`, in which case those pending\ndestinations are cancelled (marked failed with a cancellation note)\nand `cancelled_post_count` reports how many. Deletion is permanent and\nfrees storage. Fires the `media.deleted` webhook event.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MediaId"
          },
          {
            "name": "force",
            "in": "query",
            "description": "Set to true to confirm deletion when the media is attached to scheduled posts",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "deleted",
                        "id"
                      ],
                      "properties": {
                        "deleted": {
                          "type": "boolean",
                          "const": true
                        },
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "cancelled_post_count": {
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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": {
            "description": "Media attached to scheduled posts — retry with force=true",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/media/{id}/complete": {
      "post": {
        "operationId": "completeMediaUpload",
        "x-scope": "write",
        "tags": [
          "Media"
        ],
        "summary": "Finalize a media upload (step 3 of 3)",
        "description": "Call after uploading the file to the presigned `upload_url`. Verifies\nthe file actually exists (`400 file_not_uploaded` when it does not)\nand that its magic bytes match the declared content type\n(`400 file_type_mismatch` — the media is then marked failed). Videos\nget a thumbnail generated in the background. Idempotent: calling on\nalready-ready media returns 200. `400 upload_failed` when the media\nwas previously marked failed. No request body is required.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/MediaId"
          }
        ],
        "responses": {
          "200": {
            "description": "Media finalized (or already ready)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/MediaCompleted"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/ai/generate-caption": {
      "post": {
        "operationId": "generateCaption",
        "x-scope": "ai",
        "tags": [
          "AI"
        ],
        "summary": "Generate social media captions with AI",
        "description": "Generates 1-5 captions for a topic, styled per platform/tone/length.\nCosts 1 AI credit per caption returned; credits are reserved\natomically before generation and refunded for captions that fail.\n`402 insufficient_credits` when the plan's monthly credit budget\n(plus bonus credits) cannot cover the request;\n`502 ai_no_content` when the model returns nothing.\nUnknown platform/tone/length values silently fall back to defaults.\n",
        "x-todo": "BUG in handler as written: same `parsed.ok` / `parsed.error` guard issue as bulkCreatePosts — valid-JSON requests currently short-circuit to a framework 500 before the documented logic runs.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "topic"
                ],
                "properties": {
                  "topic": {
                    "type": "string",
                    "maxLength": 1000
                  },
                  "platform": {
                    "type": "string",
                    "enum": [
                      "instagram",
                      "facebook",
                      "x",
                      "twitter",
                      "linkedin",
                      "youtube",
                      "tiktok",
                      "threads",
                      "pinterest",
                      "bluesky"
                    ],
                    "description": "Sets character limit and style. Omit for generic."
                  },
                  "tone": {
                    "type": "string",
                    "enum": [
                      "professional",
                      "casual",
                      "witty",
                      "enthusiastic",
                      "urgent",
                      "inspirational"
                    ],
                    "default": "professional"
                  },
                  "length": {
                    "type": "string",
                    "enum": [
                      "short",
                      "medium",
                      "long"
                    ],
                    "default": "medium"
                  },
                  "include_hashtags": {
                    "type": "boolean",
                    "default": true
                  },
                  "include_emojis": {
                    "type": "boolean",
                    "default": true
                  },
                  "count": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "default": 1
                  }
                }
              },
              "example": {
                "topic": "Announcing our summer sale — 30% off all plans",
                "platform": "instagram",
                "tone": "enthusiastic",
                "count": 2
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Captions generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "captions",
                        "credits_used",
                        "credits_remaining"
                      ],
                      "properties": {
                        "captions": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "platform": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "tone": {
                          "type": "string"
                        },
                        "length": {
                          "type": "string"
                        },
                        "count_requested": {
                          "type": "integer"
                        },
                        "count_returned": {
                          "type": "integer"
                        },
                        "credits_used": {
                          "type": "integer"
                        },
                        "credits_remaining": {
                          "type": "integer"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "description": "AI model returned no content (credits refunded)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/ai/generate-image": {
      "post": {
        "operationId": "generateImage",
        "x-scope": "ai",
        "tags": [
          "AI"
        ],
        "summary": "Generate an image with AI and store it in the media library",
        "description": "Generates one image via FAL AI, stores it in the media library and\nreturns a ready `media_id` for `createPost`. Credit cost depends on\nthe model; credits are reserved atomically and refunded on failure.\nFree-trial organizations without a paid subscription get\n`402 upgrade_required`. Prompts are safety-filtered\n(`400 prompt_rejected`). Additional AI burst limits apply on top of\nthe API-key limit. Error codes: `validation_error`, `prompt_rejected`,\n`upgrade_required`, `insufficient_credits`, `rate_limit_exceeded`,\n`generation_failed`, `internal_error`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "prompt"
                ],
                "properties": {
                  "prompt": {
                    "type": "string",
                    "maxLength": 2000
                  },
                  "aspect_ratio": {
                    "type": "string",
                    "enum": [
                      "1:1",
                      "16:9",
                      "9:16",
                      "4:3",
                      "3:4",
                      "4:5",
                      "5:4"
                    ],
                    "default": "1:1"
                  },
                  "model": {
                    "type": "string",
                    "enum": [
                      "nano-banana-pro",
                      "ideogram-v2",
                      "gemini-3-pro",
                      "flux-schnell"
                    ],
                    "default": "gemini-3-pro"
                  },
                  "brand_id": {
                    "type": "string",
                    "description": "Optional brand to apply (prefixes brand context to the prompt)"
                  }
                }
              },
              "example": {
                "prompt": "Flat-lay photo of a summer picnic with lemonade, bright pastel palette",
                "aspect_ratio": "4:5"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Image generated and stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/GeneratedImage"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/analytics/summary": {
      "get": {
        "operationId": "getAnalyticsSummary",
        "x-scope": "read",
        "tags": [
          "Analytics"
        ],
        "summary": "Aggregate post counters and engagement metrics for a time window",
        "description": "Buckets posts created in the window by derived status, breaks down\ndestinations per platform, and sums engagement metrics of published\ndestinations. Periods are UTC-anchored rolling windows. Errors:\n`invalid_period`, `invalid_range` (custom period missing/invalid\nfrom/to).\n",
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "week",
                "month",
                "all",
                "custom"
              ],
              "default": "month"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Required when period=custom",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Required when period=custom",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AnalyticsSummary"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/campaigns": {
      "get": {
        "operationId": "listCampaigns",
        "x-scope": "read",
        "tags": [
          "Campaigns"
        ],
        "summary": "List campaigns in the workspace",
        "description": "Returns every campaign in the workspace with name, color, status,\nand post counts — use `status` to filter active vs archived.\n",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaigns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "campaigns",
                        "pagination"
                      ],
                      "properties": {
                        "campaigns": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Campaign"
                          }
                        },
                        "pagination": {
                          "$ref": "#/components/schemas/Pagination"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "operationId": "createCampaign",
        "x-scope": "write",
        "tags": [
          "Campaigns"
        ],
        "summary": "Create a campaign",
        "description": "Campaigns group related posts (assign via `campaign_id` when listing\nposts; the dashboard sets it on posts). Default color is `#3b82f6`.\n",
        "x-todo": "BUG in handler as written: same `parsed.ok` / `parsed.error` guard issue as bulkCreatePosts — valid-JSON requests currently short-circuit to a framework 500 before the documented logic runs.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "color": {
                    "type": "string",
                    "pattern": "^#[0-9a-fA-F]{6}$"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "archived"
                    ],
                    "default": "active"
                  }
                }
              },
              "example": {
                "name": "Q3 Launch",
                "description": "Everything for the July product launch",
                "color": "#3b82f6"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Campaign created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Campaign"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/campaigns/{id}": {
      "get": {
        "operationId": "getCampaign",
        "x-scope": "read",
        "tags": [
          "Campaigns"
        ],
        "summary": "Get a campaign",
        "description": "Fetch one campaign by id — name, description, color, status, and\nits `post_count`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CampaignId"
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Campaign"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "patch": {
        "operationId": "updateCampaign",
        "x-scope": "write",
        "tags": [
          "Campaigns"
        ],
        "summary": "Update a campaign",
        "description": "At least one of name, description, color, status is required.",
        "x-todo": "BUG in handler as written: same `parsed.ok` / `parsed.error` guard issue as bulkCreatePosts — valid-JSON requests currently short-circuit to a framework 500 before the documented logic runs.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/CampaignId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "color": {
                    "type": "string",
                    "pattern": "^#[0-9a-fA-F]{6}$"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "archived"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated campaign",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Campaign"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deleteCampaign",
        "x-scope": "write",
        "tags": [
          "Campaigns"
        ],
        "summary": "Delete a campaign",
        "description": "Posts in the campaign survive — their campaign_id is set to null.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CampaignId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "deleted"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "deleted": {
                          "type": "boolean",
                          "const": true
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "x-scope": "read",
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhook subscriptions",
        "description": "The signing secret is never included — it is returned only on creation.",
        "responses": {
          "200": {
            "description": "Webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "webhooks"
                      ],
                      "properties": {
                        "webhooks": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Webhook"
                          }
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "x-scope": "write",
        "tags": [
          "Webhooks"
        ],
        "summary": "Create a webhook subscription",
        "description": "PostEverywhere POSTs event payloads to your URL, signed with\nHMAC-SHA256 in the `X-PostEverywhere-Signature` header\n(`sha256=<hmac>` over the raw body using the returned `secret`).\nDelivery headers also include `X-PostEverywhere-Event`,\n`X-PostEverywhere-Event-Id`, `X-PostEverywhere-Timestamp` and\n`X-PostEverywhere-Delivery-Id`. The `secret` is returned ONLY on\ncreation. URLs are SSRF-validated (no localhost/private/metadata\naddresses). Max 25 webhooks per organization\n(`400 limit_reached`). Unknown events → `400 invalid_event_type`.\n",
        "x-todo": "BUG in handler as written: same `parsed.ok` / `parsed.error` guard issue as bulkCreatePosts — valid-JSON requests currently short-circuit to a framework 500 before the documented logic runs.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/WebhookEventType"
                    }
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              },
              "example": {
                "url": "https://example.com/hooks/posteverywhere",
                "events": [
                  "post.published",
                  "post.failed"
                ],
                "name": "Production notifier"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — includes the one-time signing secret",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Webhook"
                        },
                        {
                          "type": "object",
                          "required": [
                            "secret"
                          ],
                          "properties": {
                            "secret": {
                              "type": "string",
                              "description": "whsec_-prefixed signing secret, shown exactly once"
                            },
                            "secret_warning": {
                              "type": "string"
                            }
                          }
                        }
                      ]
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/webhooks/{id}": {
      "get": {
        "operationId": "getWebhook",
        "x-scope": "read",
        "tags": [
          "Webhooks"
        ],
        "summary": "Get a webhook subscription (without secret)",
        "description": "Fetch one webhook subscription — URL, subscribed events, active\nstate, and delivery stats. The signing secret is only returned at\ncreation time.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookId"
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Webhook"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "x-scope": "write",
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook subscription",
        "description": "At least one of url, events, name, description, is_active is required.\nSetting `is_active: true` on a disabled webhook clears the\nauto-disabled marker and resets its failure counter. URL changes are\nre-validated with the same SSRF rules as creation.\n",
        "x-todo": "BUG in handler as written: same `parsed.ok` / `parsed.error` guard issue as bulkCreatePosts — valid-JSON requests currently short-circuit to a framework 500 before the documented logic runs.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/WebhookEventType"
                    }
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500
                  },
                  "is_active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated webhook",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Webhook"
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "x-scope": "write",
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook subscription",
        "description": "Cascades to stored deliveries.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "deleted"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "deleted": {
                          "type": "boolean",
                          "const": true
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/webhooks/{id}/test": {
      "post": {
        "operationId": "testWebhook",
        "x-scope": "write",
        "tags": [
          "Webhooks"
        ],
        "summary": "Send a signed test payload to a webhook URL",
        "description": "Sends a synthetic ping (payload has `data.test: true`) so you can\nverify signature validation end-to-end. The delivery result is\nreturned inline; no delivery row is stored. Always responds 200 —\ncheck `data.ok` for the delivery outcome.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookId"
          }
        ],
        "responses": {
          "200": {
            "description": "Test delivery result (ok=false means your endpoint failed)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "ok"
                      ],
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        },
                        "status": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "HTTP status your endpoint returned"
                        },
                        "duration_ms": {
                          "type": "integer"
                        },
                        "error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/auth/device": {
      "post": {
        "operationId": "startDeviceAuth",
        "x-scope": "none",
        "security": [],
        "tags": [
          "Auth"
        ],
        "summary": "Start a device-grant login (RFC 8628 style)",
        "description": "UNAUTHENTICATED. Show the returned `user_code` and\n`verification_uri` to the user; they approve in a logged-in browser.\nThen poll `POST /auth/device/token` every `interval` seconds. Codes\nexpire after `expires_in` seconds (15 min). Per-IP rate limited.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client_name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Shown as the API key name, e.g. \"CLI on jamie-macbook\""
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Device code allocated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "device_code",
                        "user_code",
                        "verification_uri",
                        "expires_in",
                        "interval"
                      ],
                      "properties": {
                        "device_code": {
                          "type": "string"
                        },
                        "user_code": {
                          "type": "string",
                          "description": "Human-typable code, format XXXX-XXXX"
                        },
                        "verification_uri": {
                          "type": "string",
                          "format": "uri"
                        },
                        "verification_uri_complete": {
                          "type": "string",
                          "format": "uri"
                        },
                        "expires_in": {
                          "type": "integer",
                          "const": 900
                        },
                        "interval": {
                          "type": "integer",
                          "const": 5
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/auth/device/token": {
      "post": {
        "operationId": "pollDeviceToken",
        "x-scope": "none",
        "security": [],
        "tags": [
          "Auth"
        ],
        "summary": "Poll a device login and mint the API key on approval",
        "description": "UNAUTHENTICATED. Poll every 5 s with the `device_code`. Non-terminal\nstates return 200 with `status` of `pending` or `slow_down`\n(includes `interval`). Terminal: `expired`, `denied`, or\n`authorized`. On the first poll after browser approval the response\ncarries the plaintext `api_key` (scopes read/write/ai, no expiry)\nEXACTLY ONCE — persist it immediately; subsequent polls return\n`expired`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "device_code"
                ],
                "properties": {
                  "device_code": {
                    "type": "string",
                    "minLength": 32
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Poll result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "status"
                      ],
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "slow_down",
                            "expired",
                            "denied",
                            "authorized"
                          ]
                        },
                        "interval": {
                          "type": "integer",
                          "description": "Present with slow_down"
                        },
                        "api_key": {
                          "type": "string",
                          "description": "pe_live_... plaintext key — returned once, only with status=authorized"
                        },
                        "api_key_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "scopes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "organization_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "user": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "email": {
                              "type": "string"
                            },
                            "name": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/auth/social-connect/start": {
      "post": {
        "operationId": "startSocialConnect",
        "x-scope": "write",
        "tags": [
          "Auth"
        ],
        "summary": "Start a browser OAuth connect session",
        "description": "Requires an API key (session fallback rejected with\n`401 api_key_required`). For the browser-OAuth platforms (instagram,\nfacebook, threads, x, linkedin, tiktok, youtube, pinterest) this\nreturns the dashboard's OAuth-start `authorize_url` to open in a\nlogged-in browser, plus a `session_id` to poll. Headless platforms\n(bluesky, telegram, discord) are rejected with\n`400 use_headless_endpoint` — POST credentials to their dedicated\nendpoints instead. `mode: reconnect` requires `account_id`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "platform"
                ],
                "properties": {
                  "platform": {
                    "type": "string",
                    "enum": [
                      "instagram",
                      "facebook",
                      "threads",
                      "x",
                      "linkedin",
                      "tiktok",
                      "youtube",
                      "pinterest"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "connect",
                      "reconnect"
                    ],
                    "default": "connect"
                  },
                  "account_id": {
                    "type": "integer",
                    "description": "Required for mode=reconnect"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "session_id",
                        "authorize_url",
                        "expires_in",
                        "interval"
                      ],
                      "properties": {
                        "session_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "authorize_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "expires_in": {
                          "type": "integer",
                          "const": 900
                        },
                        "interval": {
                          "type": "integer",
                          "const": 3
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/auth/social-connect/poll/{session_id}": {
      "get": {
        "operationId": "pollSocialConnect",
        "x-scope": "read",
        "tags": [
          "Auth"
        ],
        "summary": "Poll a connect session until the browser OAuth completes",
        "description": "Requires the SAME API key that started the session. Resolves to\n`completed` (with the connected `account`) once an account for the\nsession's platform appears (or, for reconnect, the target account's\ntoken refreshes) after the session started. Other statuses:\n`pending`, `expired`, `cancelled`.\n",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "status"
                      ],
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "completed",
                            "expired",
                            "cancelled"
                          ]
                        },
                        "account": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "platform": {
                              "type": "string"
                            },
                            "account_name": {
                              "type": "string"
                            },
                            "avatar_url": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/auth/social-connect/bluesky": {
      "post": {
        "operationId": "connectBluesky",
        "x-scope": "write",
        "tags": [
          "Auth"
        ],
        "summary": "Connect a Bluesky account (headless, app password)",
        "description": "Requires an API key. Use an App Password (Bluesky Settings → App\nPasswords), not the login password (`401 auth_failed` otherwise).\nErrors: `validation_error`, `account_limit_reached` (403),\n`already_connected` (409 — account belongs to another organization).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "handle",
                  "app_password"
                ],
                "properties": {
                  "handle": {
                    "type": "string",
                    "description": "e.g. alice.bsky.social (leading @ tolerated)"
                  },
                  "app_password": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectCompletedEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Account already connected to another organization",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/auth/social-connect/telegram": {
      "post": {
        "operationId": "connectTelegram",
        "x-scope": "write",
        "tags": [
          "Auth"
        ],
        "summary": "Connect a Telegram channel (headless, bot token)",
        "description": "Requires an API key. The bot (from @BotFather) must be an\nadministrator of the channel with Post Messages enabled\n(`400 bot_not_admin` otherwise, with setup instructions). `channel`\naccepts \"@name\" or a numeric chat ID. Provider errors are mapped to\nspecific codes; upstream Telegram failures return 502.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bot_token",
                  "channel"
                ],
                "properties": {
                  "bot_token": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string",
                    "description": "@mychannel or numeric chat id"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectCompletedEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Channel already connected to another organization",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "description": "Telegram API unreachable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/auth/social-connect/discord": {
      "post": {
        "operationId": "connectDiscord",
        "x-scope": "write",
        "tags": [
          "Auth"
        ],
        "summary": "Connect a Discord channel (headless, webhook URL)",
        "description": "Requires an API key. Create the webhook in Discord under Server\nSettings → Integrations → Webhooks and paste its URL. Invalid URLs\nreturn `401 auth_failed`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "webhook_url"
                ],
                "properties": {
                  "webhook_url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectCompletedEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Channel already connected to another organization",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/platform-rules": {
      "get": {
        "operationId": "getPlatformRules",
        "x-scope": "read",
        "x-status": "planned",
        "tags": [
          "Platform Rules"
        ],
        "summary": "Get platform composer rules",
        "description": "Server-authoritative composer rules for every supported platform:\ncharacter limits, image/video constraints, and feature support\n(threads, carousels, reels, alt text). Check before composing for an\nunfamiliar platform — limits span 300 characters (Bluesky) to 63,206\n(Facebook), and some constraints are enforced platform-side only.\nServed with an ETag; a newly supported platform appears here with no\nclient update. Live since 2026-08-17.\n",
        "responses": {
          "200": {
            "description": "Map of platform → rules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "error",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "description": "Keys are platform identifiers (instagram, facebook, x, ...)",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/PlatformRule"
                      }
                    },
                    "error": {
                      "type": "null"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                },
                "examples": {
                  "rules": {
                    "value": {
                      "data": {
                        "x": {
                          "characterLimit": 280,
                          "mediaTypes": [
                            "image",
                            "video",
                            "gif"
                          ],
                          "maxImages": 4,
                          "maxVideoBytes": 536870912,
                          "features": [
                            "threads",
                            "polls"
                          ]
                        },
                        "instagram": {
                          "characterLimit": 2200,
                          "mediaTypes": [
                            "image",
                            "video"
                          ],
                          "maxImages": 10,
                          "maxVideoBytes": 104857600,
                          "features": [
                            "carousels",
                            "reels",
                            "cover_photo"
                          ]
                        }
                      },
                      "error": null,
                      "meta": {
                        "request_id": "5d6e7f8a",
                        "timestamp": "2026-07-08T09:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/queue": {
      "get": {
        "operationId": "getQueue",
        "tags": [
          "Posts"
        ],
        "summary": "Get the posting queue",
        "description": "The workspace's recurring posting openings, plus a preview of where the next posts added to the queue will land (wall-clock in the queue's timezone). The preview is a forecast; allocation happens at create time via POST /posts with use_queue: true. Queue management (editing openings) is app-only by design.",
        "parameters": [
          {
            "name": "preview",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 10
            },
            "description": "How many upcoming openings to preview"
          }
        ],
        "responses": {
          "200": {
            "description": "The queue and its upcoming openings, or queue: null when none is set up",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "queue": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "timezone": {
                          "type": "string"
                        },
                        "slots": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "day_of_week": {
                                "type": "integer",
                                "description": "0=Sunday .. 6=Saturday"
                              },
                              "time": {
                                "type": "string",
                                "example": "09:00"
                              }
                            }
                          }
                        }
                      }
                    },
                    "upcoming": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "example": "2026-08-31"
                          },
                          "time": {
                            "type": "string",
                            "example": "09:00"
                          }
                        }
                      }
                    },
                    "exhausted": {
                      "type": "boolean",
                      "description": "True when the horizon ran out before filling the preview"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/posts/{id}/approve": {
      "post": {
        "operationId": "approvePost",
        "tags": [
          "Posts"
        ],
        "summary": "Approve a pending post",
        "description": "Clears a post that is awaiting approval. An API key is an organization credential (only owners and admins can create one), so a key approving is an admin approving by proxy. Approving does not publish: the draft is cleared, and scheduling it is what sends it out. One approval authorizes exactly one publish.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "Optional message to the author"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved"
          },
          "400": {
            "description": "Post is not awaiting approval"
          },
          "404": {
            "description": "Post not found in this organization"
          }
        }
      }
    },
    "/posts/{id}/request-changes": {
      "post": {
        "operationId": "requestPostChanges",
        "tags": [
          "Posts"
        ],
        "summary": "Send a pending post back with a note",
        "description": "Returns a post awaiting approval to its author with a required note explaining what to change. The post stays a draft; the author is notified by email.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "note"
                ],
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "What needs changing. Required; the author sees this."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent back to the author"
          },
          "400": {
            "description": "note missing, or post not awaiting approval"
          },
          "404": {
            "description": "Post not found in this organization"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pe_live_...",
        "description": "API key created in the dashboard (Developers) or via the device-login\nflow. Keys start with `pe_live_` and carry scopes (read, write, ai);\nonly a SHA-256 hash is stored server-side. See each operation's\n`x-scope` for the scope it requires.\n"
      }
    },
    "parameters": {
      "PostId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Post UUID (non-UUIDs return 400 invalid_id)",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "MediaId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Media UUID",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "AccountId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Social account ID (integer, as returned by listAccounts)",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "CampaignId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Campaign ID (integer)",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "WebhookId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Webhook UUID",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Validation error — see error.code and error.details",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "data": null,
              "error": {
                "message": "account_ids is required and must be a non-empty array. Use GET /accounts to find your account IDs.",
                "code": "account_ids_required",
                "retryable": false
              },
              "meta": {
                "request_id": "1a2b3c4d",
                "timestamp": "2026-07-08T09:00:00.000Z"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing/invalid/revoked/expired API key (codes invalid_api_key,\napi_key_revoked, api_key_expired, api_key_required, auth_failed).\nAuth-layer 401s omit error.retryable and meta.request_id.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "data": null,
              "error": {
                "message": "Invalid API key format. Expected: Bearer pe_live_...",
                "code": "invalid_api_key"
              },
              "meta": {
                "timestamp": "2026-07-08T09:00:00.000Z"
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "No active subscription (code subscription_required — the error object\nalso carries subscription_status), or insufficient AI credits\n(insufficient_credits) / paid plan required (upgrade_required).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "data": null,
              "error": {
                "message": "Your free trial has ended. Please upgrade to a paid plan to continue using the API.",
                "code": "subscription_required",
                "subscription_status": "expired"
              },
              "meta": {
                "timestamp": "2026-07-08T09:00:00.000Z"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "API key missing the required scope (insufficient_scope), storage quota exceeded (storage_quota_exceeded), or account limit reached (account_limit_reached)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "data": null,
              "error": {
                "message": "API key missing required scope: 'write'. Your key has scopes: [read]",
                "code": "insufficient_scope",
                "retryable": false,
                "details": {
                  "required_scope": "write",
                  "current_scopes": [
                    "read"
                  ]
                }
              },
              "meta": {
                "request_id": "1a2b3c4d",
                "timestamp": "2026-07-08T09:00:00.000Z"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found in your organization/workspace (not_found)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "data": null,
              "error": {
                "message": "Post not found. Verify the post ID belongs to your organization.",
                "code": "not_found",
                "retryable": false
              },
              "meta": {
                "request_id": "1a2b3c4d",
                "timestamp": "2026-07-08T09:00:00.000Z"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (rate_limit_exceeded, or plan budgets\npost_limit_reached / daily_limit_reached on schedulePost). All 429s\ncarry Retry-After. The API-key limiter additionally sets the\nX-RateLimit-* headers and error.details {limit, remaining, reset_at}.\n",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "description": "Requests allowed in the window (API-key limiter only)",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests remaining (API-key limiter only)",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds when the window resets (API-key limiter only)",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "data": null,
              "error": {
                "message": "API rate limit exceeded (60 requests/min). Please slow down.",
                "code": "rate_limit_exceeded",
                "retryable": true,
                "details": {
                  "limit": 60,
                  "remaining": 0,
                  "reset_at": "2026-07-08T09:01:00.000Z"
                }
              },
              "meta": {
                "request_id": "1a2b3c4d",
                "timestamp": "2026-07-08T09:00:00.000Z"
              }
            }
          }
        }
      },
      "ServerError": {
        "description": "Internal error (internal_error and endpoint-specific 5xx codes). Retryable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "data": null,
              "error": {
                "message": "Failed to create post. Verify content, account_ids, and optional media_ids/scheduled_for are valid, then retry.",
                "code": "internal_error",
                "retryable": true
              },
              "meta": {
                "request_id": "1a2b3c4d",
                "timestamp": "2026-07-08T09:00:00.000Z"
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Meta": {
        "type": "object",
        "description": "Response metadata. request_id is an 8-char correlation id (absent on auth-layer 401/402).",
        "properties": {
          "request_id": {
            "type": "string"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "timestamp"
        ]
      },
      "ErrorBody": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code (e.g. validation_error, not_found, rate_limit_exceeded)"
          },
          "retryable": {
            "type": "boolean",
            "description": "true for transient failures (5xx, 429). Absent on auth-layer 401/402 errors."
          },
          "details": {
            "description": "Optional structured context specific to the error code"
          },
          "subscription_status": {
            "type": "string",
            "description": "Present only on 402 subscription_required"
          }
        },
        "additionalProperties": true
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "data",
          "error",
          "meta"
        ],
        "properties": {
          "data": {
            "type": "null"
          },
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "Pagination": {
        "type": "object",
        "required": [
          "limit",
          "offset"
        ],
        "properties": {
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "PostStatus": {
        "type": "string",
        "description": "Derived from live destination statuses (the stored post_status column is frozen at creation)",
        "enum": [
          "draft",
          "scheduled",
          "publishing",
          "published",
          "partially_failed",
          "failed"
        ]
      },
      "DestinationMetrics": {
        "type": [
          "object",
          "null"
        ],
        "description": "Engagement metrics fetched from the platform (null until first fetch; only some platforms have metric fetchers)",
        "properties": {
          "views": {
            "type": [
              "integer",
              "null"
            ]
          },
          "likes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "comments": {
            "type": [
              "integer",
              "null"
            ]
          },
          "shares": {
            "type": [
              "integer",
              "null"
            ]
          },
          "impressions": {
            "type": [
              "integer",
              "null"
            ]
          },
          "saves": {
            "type": [
              "integer",
              "null"
            ]
          },
          "clicks": {
            "type": [
              "integer",
              "null"
            ]
          },
          "fetched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "PostDestination": {
        "type": "object",
        "description": "One platform/account target of a post",
        "required": [
          "destination_id",
          "platform",
          "destination_status"
        ],
        "properties": {
          "destination_id": {
            "type": "string",
            "format": "uuid"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "deprecated": true,
            "description": "Deprecated alias of destination_id"
          },
          "account_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "account_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present on getPost, createPost and results responses (not in listPosts)"
          },
          "account_avatar_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present on createPost response only"
          },
          "platform": {
            "type": "string"
          },
          "destination_status": {
            "type": "string",
            "enum": [
              "queued",
              "preparing",
              "uploading",
              "publishing",
              "verifying",
              "retry_scheduled",
              "done",
              "failed",
              "draft"
            ],
            "description": "Lifecycle of THIS destination. A successful publish is `done` — there is no `published` value at the destination level (that name belongs to the post-level status vocabulary, which is a different taxonomy). In flight: `preparing`, `uploading`, `publishing`, `verifying`. Waiting: `queued`, `retry_scheduled`. Terminal: `done`, `failed`.\n"
          },
          "status": {
            "type": "string",
            "deprecated": true,
            "description": "Deprecated alias of destination_status"
          },
          "scheduled_for": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When THIS destination fires (the scheduler uses this, not the post-level value)"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "platform_post_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "platform_post_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present on createPost and results responses after a successful publish"
          },
          "error": {
            "description": "Last error (string or structured object), null when none"
          },
          "attempts": {
            "type": "integer"
          },
          "next_retry_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "metrics": {
            "$ref": "#/components/schemas/DestinationMetrics"
          }
        }
      },
      "Post": {
        "type": "object",
        "description": "Post as returned by listPosts / getPost",
        "required": [
          "post_id",
          "post_status",
          "content",
          "account_ids",
          "timezone",
          "media_ids",
          "destinations",
          "destinations_count",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "post_id": {
            "type": "string",
            "format": "uuid"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "deprecated": true,
            "description": "Deprecated alias of post_id"
          },
          "post_status": {
            "$ref": "#/components/schemas/PostStatus"
          },
          "status": {
            "deprecated": true,
            "description": "Deprecated alias of post_status",
            "$ref": "#/components/schemas/PostStatus"
          },
          "content": {
            "type": [
              "string",
              "null"
            ]
          },
          "account_ids": {
            "type": "array",
            "description": "For drafts, taken from the draft's saved account selection",
            "items": {
              "type": "integer"
            }
          },
          "platform_content": {
            "type": "object",
            "description": "Per-platform caption overrides (present on drafts that have them)",
            "additionalProperties": true
          },
          "scheduled_for": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "null for drafts without a pre-set time"
          },
          "timezone": {
            "type": "string",
            "default": "UTC"
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "media": {
            "description": "Hydrated media JSONB (array of media objects) — response only"
          },
          "campaign_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Present in listPosts responses"
          },
          "thread_posts": {
            "description": "Thread segments (getPost only), null when not a thread"
          },
          "destinations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PostDestination"
            }
          },
          "destinations_count": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "UpdatedPost": {
        "type": "object",
        "description": "Post as returned by updatePost (a slimmer shape than getPost)",
        "required": [
          "id",
          "status",
          "account_ids",
          "scheduled_for",
          "timezone",
          "media_ids",
          "destinations",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "content": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/PostStatus"
          },
          "account_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "scheduled_for": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "timezone": {
            "type": "string"
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "media": {},
          "destinations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "platform": {
                  "type": "string"
                },
                "status": {
                  "type": "string"
                },
                "account_name": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "account_id": {
                  "type": [
                    "integer",
                    "null"
                  ]
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PostResult": {
        "type": "object",
        "description": "Per-destination publish result (getPostResults)",
        "required": [
          "id",
          "platform",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "platform": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "queued → publishing → published or failed"
          },
          "account_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "account_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "scheduled_for": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "platform_post_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "platform_post_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "error": {},
          "attempts": {
            "type": [
              "integer",
              "null"
            ]
          },
          "next_retry_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "CreatePostRequest": {
        "type": "object",
        "description": "Body for createPost (and each item of bulkCreatePosts). camelCase\naliases (accountIds, mediaIds, scheduledFor, platformContent,\ncoverPhoto) and the deprecated scheduled_at alias are also accepted.\n",
        "properties": {
          "content": {
            "type": "string",
            "maxLength": 10000,
            "description": "Required unless platform_content has a caption or media_ids is non-empty"
          },
          "account_ids": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "integer"
            },
            "description": "Required unless draft=true (drafts may pick accounts at schedule time)"
          },
          "scheduled_for": {
            "type": "string",
            "format": "date-time",
            "description": "Omit to publish immediately. Assumed UTC when no offset. Must be in the future."
          },
          "timezone": {
            "type": "string",
            "default": "UTC",
            "description": "IANA timezone — display metadata only, never changes WHEN the post fires"
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Media UUIDs from the upload flow. Do NOT send `media`/`media_id`/`attachments`."
          },
          "platform_content": {
            "type": "object",
            "description": "Per-platform caption overrides, e.g. { \"x\": { \"content\": \"shorter caption\" } }",
            "additionalProperties": true
          },
          "cover_photo": {
            "type": "object",
            "description": "Video cover photo",
            "required": [
              "url"
            ],
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              },
              "timestamp": {
                "type": "number",
                "default": 0
              },
              "is_custom": {
                "type": "boolean",
                "default": true
              }
            }
          },
          "draft": {
            "type": "boolean",
            "default": false,
            "description": "Save as a reviewable draft instead of scheduling/publishing"
          },
          "use_queue": {
            "type": "boolean",
            "default": false,
            "description": "Let the workspace's posting queue pick the time. The server allocates the next free opening at create time and the response carries the resulting scheduled_for. Mutually exclusive with scheduled_for. Requires a queue to exist (set one up on the Queue page, or read it with GET /queue). 400 with a clear message when the queue is missing, empty, or full."
          },
          "request_approval": {
            "type": "boolean",
            "default": false,
            "description": "Only with draft: true. Submits the draft into the approval workflow immediately: approval_status becomes 'pending', owners and admins are notified, and the draft appears in the Approvals inbox. Track it with GET /posts?approval=pending; an approver clears it via POST /posts/{id}/approve."
          }
        }
      },
      "PostCreated": {
        "type": "object",
        "description": "createPost response for a scheduled/immediate post",
        "required": [
          "post_id",
          "post_status",
          "publish_mode",
          "account_ids",
          "destinations",
          "created_at"
        ],
        "properties": {
          "post_id": {
            "type": "string",
            "format": "uuid"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "deprecated": true
          },
          "post_status": {
            "$ref": "#/components/schemas/PostStatus"
          },
          "status": {
            "deprecated": true,
            "$ref": "#/components/schemas/PostStatus"
          },
          "publish_mode": {
            "type": "string",
            "enum": [
              "immediate",
              "scheduled"
            ]
          },
          "content": {
            "type": [
              "string",
              "null"
            ]
          },
          "account_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "scheduled_for": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "timezone": {
            "type": "string"
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "platform_content": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "destinations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PostDestination"
            }
          },
          "destinations_count": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "accounts_count": {
            "type": "integer",
            "deprecated": true
          },
          "message": {
            "type": "string"
          },
          "next_step": {
            "type": "string"
          },
          "validation_warnings": {
            "type": "object",
            "description": "Non-blocking per-platform warnings (e.g. suboptimal aspect ratio)",
            "additionalProperties": true
          },
          "results": {
            "type": "array",
            "deprecated": true,
            "description": "Deprecated alias of destinations",
            "items": {
              "$ref": "#/components/schemas/PostDestination"
            }
          }
        }
      },
      "DraftCreated": {
        "type": "object",
        "description": "createPost response when draft: true",
        "required": [
          "post_id",
          "status",
          "created_at"
        ],
        "properties": {
          "post_id": {
            "type": "string",
            "format": "uuid"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "deprecated": true
          },
          "status": {
            "type": "string",
            "const": "draft"
          },
          "post_status": {
            "type": "string",
            "const": "draft"
          },
          "content": {
            "type": [
              "string",
              "null"
            ]
          },
          "account_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "scheduled_for": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Pre-filled schedule if one was supplied, else null"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "message": {
            "type": "string"
          },
          "next_steps": {
            "type": "object",
            "properties": {
              "review": {
                "type": "string"
              },
              "schedule": {
                "type": "string"
              }
            }
          }
        }
      },
      "BulkResultEnvelope": {
        "type": "object",
        "required": [
          "data",
          "error",
          "meta"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "summary",
              "results"
            ],
            "properties": {
              "summary": {
                "type": "object",
                "required": [
                  "total",
                  "succeeded",
                  "failed"
                ],
                "properties": {
                  "total": {
                    "type": "integer"
                  },
                  "succeeded": {
                    "type": "integer"
                  },
                  "failed": {
                    "type": "integer"
                  }
                }
              },
              "results": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "index",
                    "ok"
                  ],
                  "properties": {
                    "index": {
                      "type": "integer"
                    },
                    "ok": {
                      "type": "boolean"
                    },
                    "post": {
                      "description": "The createPost response data (when ok=true)",
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/PostCreated"
                        },
                        {
                          "$ref": "#/components/schemas/DraftCreated"
                        }
                      ]
                    },
                    "error": {
                      "type": "object",
                      "description": "Present when ok=false",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "status": {
                          "type": "integer"
                        },
                        "details": {}
                      }
                    }
                  }
                }
              }
            }
          },
          "error": {
            "type": "null"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "SocialAccount": {
        "type": "object",
        "required": [
          "id",
          "platform",
          "account_name",
          "is_active",
          "created_at",
          "health"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Integer account ID — use this in account_ids and path params"
          },
          "platform": {
            "type": "string",
            "description": "Known values: instagram, facebook, x, linkedin, youtube, tiktok, threads, pinterest, bluesky, telegram, discord"
          },
          "account_name": {
            "type": "string"
          },
          "avatar_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "health": {
            "type": "object",
            "required": [
              "status",
              "can_post"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "healthy",
                  "unhealthy",
                  "expired",
                  "needs_reconnection"
                ],
                "description": "needs_reconnection appears only in listAccounts (getAccount reports healthy/unhealthy/expired)"
              },
              "can_post": {
                "type": "boolean"
              },
              "needs_reconnection": {
                "type": "boolean",
                "description": "Present in listAccounts only"
              }
            }
          }
        }
      },
      "AccountHealthReport": {
        "type": "object",
        "required": [
          "account_id",
          "platform",
          "status",
          "can_post",
          "reasons",
          "is_active",
          "needs_reconnection",
          "token",
          "activity"
        ],
        "properties": {
          "account_id": {
            "type": "integer"
          },
          "platform": {
            "type": "string"
          },
          "account_name": {
            "type": "string"
          },
          "platform_account_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The account's ID on the platform itself"
          },
          "status": {
            "type": "string",
            "enum": [
              "healthy",
              "warning",
              "broken"
            ]
          },
          "can_post": {
            "type": "boolean"
          },
          "reasons": {
            "type": "array",
            "description": "Why status is not healthy",
            "items": {
              "type": "string",
              "enum": [
                "account_inactive",
                "needs_reconnection",
                "token_expired",
                "recent_permanent_failures",
                "token_expiring_soon"
              ]
            }
          },
          "is_active": {
            "type": "boolean"
          },
          "needs_reconnection": {
            "type": "boolean"
          },
          "needs_reconnection_since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "token": {
            "type": "object",
            "properties": {
              "expires_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "last_refresh": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "expired": {
                "type": "boolean"
              },
              "expiring_soon": {
                "type": "boolean",
                "description": "Expires within 7 days"
              }
            }
          },
          "activity": {
            "type": "object",
            "properties": {
              "last_published_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "failures_last_7d": {
                "type": "integer"
              },
              "permanent_failures_last_7d": {
                "type": "integer"
              },
              "queued_count": {
                "type": "integer"
              }
            }
          },
          "connected_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MediaStatus": {
        "type": "string",
        "description": "uploading until /complete is called; it never flips to ready on its own",
        "enum": [
          "uploading",
          "ready",
          "failed"
        ]
      },
      "MediaItem": {
        "type": "object",
        "description": "Media library item (listMedia shape)",
        "required": [
          "media_id",
          "media_status",
          "type",
          "created_at"
        ],
        "properties": {
          "media_id": {
            "type": "string",
            "format": "uuid"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "deprecated": true,
            "description": "Deprecated alias of media_id"
          },
          "media_status": {
            "$ref": "#/components/schemas/MediaStatus"
          },
          "type": {
            "type": "string",
            "enum": [
              "image",
              "video",
              "document"
            ]
          },
          "mime_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "file_size": {
            "type": [
              "integer",
              "null"
            ],
            "deprecated": true
          },
          "file_size_bytes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "original_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "dimensions": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "width": {
                "type": "integer"
              },
              "height": {
                "type": "integer"
              }
            }
          },
          "width": {
            "type": [
              "integer",
              "null"
            ]
          },
          "height": {
            "type": [
              "integer",
              "null"
            ]
          },
          "aspect_ratio": {
            "type": [
              "number",
              "null"
            ]
          },
          "orientation": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "portrait",
              "landscape",
              "square",
              null
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MediaDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/MediaItem"
          },
          {
            "type": "object",
            "description": "Additional fields on getMedia",
            "properties": {
              "duration_seconds": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Video duration (null for non-video)"
              },
              "url": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "description": "Permanent viewable URL (populated once ready)"
              },
              "thumbnail_url": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              },
              "ready": {
                "type": "boolean"
              },
              "action_required": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Next step when not ready (step=complete or step=reupload)",
                "properties": {
                  "step": {
                    "type": "string",
                    "enum": [
                      "complete",
                      "reupload"
                    ]
                  },
                  "method": {
                    "type": "string"
                  },
                  "endpoint": {
                    "type": "string"
                  },
                  "why": {
                    "type": "string"
                  }
                }
              },
              "message": {
                "type": "string"
              },
              "next_step": {
                "type": "string"
              }
            }
          }
        ]
      },
      "MediaUploadTicket": {
        "type": "object",
        "description": "initMediaUpload response — where and how to send the file bytes",
        "required": [
          "media_id",
          "media_status",
          "type",
          "upload_url",
          "upload_method",
          "expires_at"
        ],
        "properties": {
          "media_id": {
            "type": "string",
            "format": "uuid"
          },
          "media_ids": {
            "type": "array",
            "description": "Round-trip convenience — copy into createPost after /complete",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "media_status": {
            "type": "string",
            "const": "uploading"
          },
          "type": {
            "type": "string",
            "enum": [
              "image",
              "video",
              "document"
            ]
          },
          "upload_url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned URL to send the file to (Cloudflare Images for images, R2 for video/PDF)"
          },
          "upload_method": {
            "type": "object",
            "description": "Images: {method: POST, content_type: multipart/form-data, field_name: file}. Video/PDF: {method: PUT, headers: {Content-Type: <declared type>}}.",
            "properties": {
              "method": {
                "type": "string",
                "enum": [
                  "POST",
                  "PUT"
                ]
              },
              "content_type": {
                "type": "string"
              },
              "field_name": {
                "type": "string"
              },
              "headers": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          },
          "provider": {
            "type": "string",
            "enum": [
              "images",
              "files"
            ]
          },
          "expires_in": {
            "type": "integer",
            "deprecated": true,
            "description": "Seconds (use expires_at)"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "max_size": {
            "type": "integer",
            "deprecated": true,
            "description": "Bytes (use max_size_bytes)"
          },
          "max_size_bytes": {
            "type": "integer"
          },
          "next_step": {
            "type": "string"
          },
          "complete_url": {
            "type": "string",
            "description": "Path to POST after uploading, e.g. /v1/media/{media_id}/complete"
          }
        }
      },
      "MediaCompleted": {
        "type": "object",
        "description": "completeMediaUpload response",
        "required": [
          "media_id",
          "media_status",
          "type"
        ],
        "properties": {
          "media_id": {
            "type": "string",
            "format": "uuid"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "deprecated": true
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "media_status": {
            "type": "string",
            "const": "ready"
          },
          "type": {
            "type": "string",
            "enum": [
              "image",
              "video",
              "document"
            ]
          },
          "message": {
            "type": "string"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "GeneratedImage": {
        "type": "object",
        "description": "generateImage response — the image is already in the media library",
        "required": [
          "media_id",
          "media_status",
          "type",
          "credits_used",
          "credits_remaining"
        ],
        "properties": {
          "media_id": {
            "type": "string",
            "format": "uuid"
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "media_status": {
            "type": "string",
            "const": "ready"
          },
          "type": {
            "type": "string",
            "const": "image"
          },
          "mime_type": {
            "type": "string",
            "const": "image/png"
          },
          "dimensions": {
            "type": "object",
            "properties": {
              "width": {
                "type": "integer"
              },
              "height": {
                "type": "integer"
              }
            }
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "aspect_ratio": {
            "type": "string"
          },
          "orientation": {
            "type": "string",
            "enum": [
              "portrait",
              "landscape",
              "square"
            ]
          },
          "file_size_bytes": {
            "type": "integer"
          },
          "ai": {
            "type": "object",
            "properties": {
              "model": {
                "type": "string"
              },
              "prompt": {
                "type": "string"
              },
              "aspect_ratio": {
                "type": "string"
              },
              "generation_time_ms": {
                "type": "integer"
              }
            }
          },
          "model": {
            "type": "string",
            "deprecated": true,
            "description": "Use ai.model"
          },
          "credits_used": {
            "type": "integer"
          },
          "credits_remaining": {
            "type": "integer"
          },
          "message": {
            "type": "string"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "AnalyticsSummary": {
        "type": "object",
        "required": [
          "period",
          "range",
          "posts",
          "by_platform",
          "metrics",
          "ai_credits"
        ],
        "properties": {
          "period": {
            "type": "string",
            "enum": [
              "today",
              "week",
              "month",
              "all",
              "custom"
            ]
          },
          "range": {
            "type": "object",
            "properties": {
              "from": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "null for period=all"
              },
              "to": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "posts": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "draft": {
                "type": "integer"
              },
              "scheduled": {
                "type": "integer"
              },
              "publishing": {
                "type": "integer"
              },
              "published": {
                "type": "integer"
              },
              "partially_failed": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              }
            }
          },
          "by_platform": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "platform": {
                  "type": "string"
                },
                "total": {
                  "type": "integer"
                },
                "scheduled": {
                  "type": "integer"
                },
                "published": {
                  "type": "integer"
                },
                "failed": {
                  "type": "integer"
                }
              }
            }
          },
          "metrics": {
            "description": "Aggregate engagement for published destinations in the window. Coverage varies by platform beca use each social API returns a different subset: YouTube and TikTok report views, LinkedIn and I nstagram report engagement but no view count, Facebook and Threads currently report nothing pen ding Meta App Review. A metric a platform does not report aggregates as 0 here and is null on t he individual destination. There is no reach metric and no follower data on any platform. Full  table: https://developers.posteverywhere.ai/analytics-summary",
            "type": "object",
            "properties": {
              "views": {
                "type": "integer"
              },
              "likes": {
                "type": "integer"
              },
              "comments": {
                "type": "integer"
              },
              "shares": {
                "type": "integer"
              },
              "impressions": {
                "type": "integer"
              },
              "clicks": {
                "type": "integer"
              }
            }
          },
          "ai_credits": {
            "type": "object",
            "properties": {
              "used_this_period": {
                "type": "integer"
              },
              "bonus": {
                "type": "integer"
              }
            }
          }
        }
      },
      "Campaign": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "post_count",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "color": {
            "type": [
              "string",
              "null"
            ],
            "description": "Hex color, e.g. #3b82f6"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ]
          },
          "post_count": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "enum": [
          "post.scheduled",
          "post.publishing",
          "post.published",
          "post.failed",
          "post.partially_failed",
          "post.updated",
          "post.deleted",
          "account.connected",
          "account.disconnected",
          "account.reconnect_needed",
          "media.uploaded",
          "media.deleted",
          "post.approval_requested",
          "post.approved",
          "post.changes_requested"
        ]
      },
      "Webhook": {
        "type": "object",
        "required": [
          "id",
          "url",
          "events",
          "is_active",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_active": {
            "type": "boolean"
          },
          "consecutive_failures": {
            "type": "integer"
          },
          "auto_disabled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set when repeated delivery failures auto-disabled the webhook"
          },
          "last_delivery_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_success_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_failure_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ConnectedAccountResult": {
        "type": "object",
        "required": [
          "status",
          "account"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "completed"
          },
          "account": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "platform": {
                "type": "string"
              },
              "account_name": {
                "type": "string"
              },
              "avatar_url": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "is_update": {
                "type": "boolean",
                "description": "true when an existing connection was refreshed rather than newly created"
              }
            }
          }
        }
      },
      "ConnectCompletedEnvelope": {
        "type": "object",
        "required": [
          "data",
          "error",
          "meta"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ConnectedAccountResult"
          },
          "error": {
            "type": "null"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "scopes",
          "user",
          "organization",
          "workspace",
          "quota",
          "stats"
        ],
        "properties": {
          "api_key": {
            "type": [
              "object",
              "null"
            ],
            "description": "null when authenticated via session cookie",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "key_prefix": {
                "type": "string"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "last_used_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "expires_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "write",
                "ai"
              ]
            }
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "email": {
                "type": "string"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "organization": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "subscription_plan": {
                "type": "string",
                "description": "Uppercased plan key: STARTER, GROWTH, PRO or TRIAL"
              },
              "subscription_status": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "subscription_source": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "trial_end": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "current_period_end": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "cancel_at_period_end": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "entitled": {
                "type": "boolean",
                "description": "true when subscription_status is active or trialing"
              }
            }
          },
          "workspace": {
            "type": "object",
            "properties": {
              "id": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "quota": {
            "type": "object",
            "properties": {
              "accounts": {
                "type": "object",
                "properties": {
                  "used": {
                    "type": "integer"
                  },
                  "limit": {
                    "type": "integer"
                  }
                }
              },
              "ai_credits": {
                "type": "object",
                "properties": {
                  "used": {
                    "type": "integer"
                  },
                  "limit": {
                    "type": "integer"
                  },
                  "bonus": {
                    "type": "integer"
                  }
                }
              },
              "storage_bytes": {
                "type": "object",
                "properties": {
                  "used": {
                    "type": "integer"
                  },
                  "limit": {
                    "type": "integer"
                  }
                }
              },
              "team_seats": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer"
                  }
                }
              }
            }
          },
          "stats": {
            "type": "object",
            "properties": {
              "posts_last_30d": {
                "type": "integer"
              },
              "total_posts": {
                "type": "integer"
              }
            }
          }
        }
      },
      "PlatformRule": {
        "type": "object",
        "description": "Composer rules for one platform",
        "required": [
          "characterLimit",
          "mediaTypes",
          "maxImages",
          "maxVideoBytes",
          "features"
        ],
        "properties": {
          "characterLimit": {
            "type": "integer"
          },
          "mediaTypes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "maxImages": {
            "type": "integer"
          },
          "maxVideoBytes": {
            "type": "integer"
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}