{
  "openapi": "3.1.0",
  "info": {
    "title": "Postatron API",
    "version": "v1",
    "summary": "Schedule, publish and measure posts on every major social platform from one request.",
    "description": "Postatron schedules and publishes one post to every social account it is given: X, LinkedIn, Instagram, Facebook, Threads, Bluesky, TikTok, YouTube and Pinterest.\nThis is its public REST API, version 1, as the API serves it.\nThe remote MCP server (`https://api.postatron.com/mcp`) and the CLI (`github.com/henrwal/postatron-cli`) are built on these routes and behave the same way.\n\n## Authentication\n\nCreate an API key in the dashboard under API & MCP (https://postatron.com/dashboard/api).\nKeys start `ptn_`, are shown once, and carry the scopes chosen when the key is made: `posts:read`, `posts:write`, `accounts:read` and `usage:read`.\nSend the key as `Authorization: Bearer ptn_...`, or as `X-API-Key: ptn_...`.\nEach operation's security requirement names the scope it needs, and a key without it gets `403 insufficient_scope`.\n\nOAuth access tokens (`pto_...`), which MCP clients obtain for themselves through OAuth 2.1, work only on the MCP endpoint: sent to these routes they get `401 unauthorized`.\n\nEvery operation except `GET /v1/openapi.json` needs an active paid plan, reads included; without one the answer is `403 subscription_required`, with `details.plans_url` saying where to choose one.\n\nA key made in a team workspace works in the owner's account, sees only the profiles its holder was granted, and is held to the owner's plan.\nMembership is checked on every request, so once its holder leaves the team, or the owner's plan stops including teams, it gets `403 workspace_forbidden`.\n\n## Profiles\n\nA profile groups connected accounts, usually one per brand or client, and holds at most one account per platform.\nWherever a route takes a `profile`, it accepts the profile's `id` or its name, ignoring case; an unknown one is `422 validation_error` with `details.profiles` listing those that exist.\n\n## Quota\n\nUsage is metered in destination-posts: one post delivered to one account, so a post to X, LinkedIn and Bluesky is three.\nThey are counted when the post is made, capped per calendar month (UTC), and reported by `GET /v1/usage`.\nX posts with a link also have their own monthly cap.\nEvery cap is a hard stop: a request past one gets `402 quota_exceeded` and nothing is published or billed.\n\n## Rate limits\n\nEach key is limited per minute, in fixed one-minute windows: writes (`POST`, `PATCH` and `DELETE`), reads (`GET`) and upload slots (`POST /v1/media/uploads`) are counted separately, and each operation names its class in `x-postatron-rate-limit`.\nEvery plan allows 30 writes, 60 reads and 120 upload slots a minute today; `GET /v1/usage` reports the limits in force, so read them from there.\nA response that got as far as the rate limiter carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` for its class.\nOver the limit the answer is `429 rate_limited`, with `Retry-After` in seconds.\n\n## Errors\n\nEvery non-2xx answer is the same envelope, `{\"error\": {\"code\": \"...\", \"message\": \"...\", \"details\": {...}}}`.\n`code` is stable and meant for programs, `message` is a sentence for a person, and `details`, when present, names the fields and values involved.\n\n## Idempotency\n\n`POST /v1/posts` takes an `Idempotency-Key` header, so a request whose answer was lost can be sent again without making a second post.\n\n## Webhooks\n\nA webhook POSTs the account's events, signed, to a URL its owner registers: see the `webhooks` section of this document for each event's body and headers, and the Webhooks routes for managing them.\n",
    "termsOfService": "https://postatron.com/terms",
    "contact": {
      "name": "Postatron support",
      "url": "https://postatron.com",
      "email": "support@postatron.com"
    }
  },
  "externalDocs": {
    "description": "The API reference in prose, with signature checks in Go, Node and Python.",
    "url": "https://github.com/henrwal/postatron-cli/blob/main/docs/api.md"
  },
  "servers": [
    {
      "url": "https://api.postatron.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKey": []
    },
    {
      "ApiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "Posts",
      "description": "Create, schedule, change, publish, retry and delete posts."
    },
    {
      "name": "Events",
      "description": "The account's event log."
    },
    {
      "name": "Accounts",
      "description": "Connected social accounts, and links that connect more."
    },
    {
      "name": "Profiles",
      "description": "Groups of accounts, one per brand or client."
    },
    {
      "name": "Queues",
      "description": "Weekly posting schedules."
    },
    {
      "name": "Usage",
      "description": "Quota and rate limits."
    },
    {
      "name": "Analytics",
      "description": "How posts performed."
    },
    {
      "name": "Media",
      "description": "Upload slots for files on a person's own device."
    },
    {
      "name": "Webhooks",
      "description": "Signed events sent to your own server."
    },
    {
      "name": "Meta",
      "description": "This description."
    }
  ],
  "paths": {
    "/v1/posts": {
      "post": {
        "operationId": "createPost",
        "summary": "Create a post",
        "description": "Creates a post on one or more connected accounts: now, at `scheduled_at`, or in a queue's next free slot (`queue`).\n\nChoose the accounts with `account_ids` or `platforms` (narrowed by `profile`). Platform options (`x`, `instagram`, `tiktok`, `youtube`, `pinterest`) are the dashboard composer's settings, checked by the same rules, and each needs an account on its platform among the post's accounts.\n\nMedia: up to four items across `media_urls` and `media_ids`, never images and video together. Instagram and TikTok need an image or a video, YouTube a video, Pinterest an image (two to four for a carousel) or one video.\n\nThe post is checked as a whole before anything is stored or counted, and refused with `422` when it breaks a rule: text over a platform's limit, media a platform cannot take, `scheduled_at` less than 5 minutes ahead, an account that needs reconnecting (`reconnect_required`), an unknown Pinterest board, TikTok settings the creator's account does not allow.\n\nEach account is one destination-post against the monthly quota, counted when the post is made, whether it goes now or later; each tweet of an X thread counts as one. Send an `Idempotency-Key` to make the request safe to repeat.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "description": "The post.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePostRequest"
              },
              "examples": {
                "scheduled": {
                  "value": {
                    "content": "Launch day! https://postatron.com",
                    "platforms": [
                      "x",
                      "instagram"
                    ],
                    "profile": "Acme",
                    "media_urls": [
                      "https://example.com/launch.jpg"
                    ],
                    "scheduled_at": "2026-09-10T09:00:00Z"
                  }
                },
                "thread_and_reel": {
                  "value": {
                    "content": "1/ Tea or coffee?",
                    "platforms": [
                      "x",
                      "instagram"
                    ],
                    "media_urls": [
                      "https://example.com/reel.mp4"
                    ],
                    "x": {
                      "content": "1/ Tea or coffee? Vote below",
                      "thread": [
                        {
                          "content": "2/ Results on Friday",
                          "media_urls": [
                            "https://example.com/chart.png"
                          ]
                        }
                      ],
                      "reply_settings": "following"
                    },
                    "instagram": {
                      "post_type": "reel",
                      "first_comment": "#tea #coffee",
                      "trial_reel": "manual"
                    }
                  }
                },
                "pinterest": {
                  "value": {
                    "content": "Autumn soup in 20 minutes",
                    "platforms": [
                      "pinterest"
                    ],
                    "media_urls": [
                      "https://example.com/soup.jpg"
                    ],
                    "pinterest": {
                      "board": "Recipes",
                      "title": "Autumn soup",
                      "link": "https://example.com/soup"
                    }
                  }
                },
                "queued": {
                  "value": {
                    "content": "Whenever there's room",
                    "platforms": [
                      "x"
                    ],
                    "queue": "Weekday mornings"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The post. `status` is `pending` for one published now (it goes out within seconds) and `scheduled` otherwise, with `scheduled_at` the time it was given. The deliveries in this answer read `pending`; `GET /v1/posts/{post_id}` shows a scheduled post's as `scheduled`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                },
                "example": {
                  "id": "0AbCdEfGhIjKlMnOpQrS",
                  "status": "scheduled",
                  "content": "Launch day! https://postatron.com",
                  "media_type": "image",
                  "contains_link": true,
                  "scheduled_at": "2026-09-10T09:00:00Z",
                  "created_at": "2026-09-05T10:12:03Z",
                  "deliveries": [
                    {
                      "account_id": "1234567890",
                      "platform": "x",
                      "username": "acmehq",
                      "status": "pending"
                    },
                    {
                      "account_id": "abc-def",
                      "platform": "instagram",
                      "username": "acme",
                      "status": "pending"
                    }
                  ],
                  "source": "api",
                  "created_by": {
                    "id": "user-1",
                    "name": "Henry"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      },
      "get": {
        "operationId": "listPosts",
        "summary": "List posts",
        "description": "Lists the account's posts, newest first: by `scheduled_at` where a post has one, otherwise by `created_at`. Filter by status, platform, profile and time, and page with `cursor`. A team workspace key sees only posts to its profiles.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only posts with this status.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "scheduled",
                "pending",
                "processing",
                "published",
                "failed",
                "partial"
              ]
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Only posts with a delivery on this platform.",
            "schema": {
              "$ref": "#/components/schemas/Platform"
            }
          },
          {
            "name": "profile",
            "in": "query",
            "required": false,
            "description": "A profile id or name: only posts to its accounts.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "RFC 3339. Applies to `scheduled_at` for scheduled posts and `created_at` otherwise.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "RFC 3339, not before `from`.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of posts.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostList"
                },
                "example": {
                  "data": [
                    {
                      "id": "0AbCdEfGhIjKlMnOpQrS",
                      "status": "scheduled",
                      "content": "Launch day! https://postatron.com",
                      "media_type": "image",
                      "contains_link": true,
                      "scheduled_at": "2026-09-10T09:00:00Z",
                      "created_at": "2026-09-05T10:12:03Z",
                      "deliveries": [
                        {
                          "account_id": "1234567890",
                          "platform": "x",
                          "username": "acmehq",
                          "status": "scheduled"
                        },
                        {
                          "account_id": "abc-def",
                          "platform": "instagram",
                          "username": "acme",
                          "status": "scheduled"
                        }
                      ],
                      "source": "api",
                      "created_by": {
                        "id": "user-1",
                        "name": "Henry"
                      }
                    }
                  ],
                  "next_cursor": "eyJvIjoyNX0"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/posts/{post_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PostId"
        }
      ],
      "get": {
        "operationId": "getPost",
        "summary": "Get a post",
        "description": "One post, with a delivery per account saying how it went there: `url` once published, `error` and `error_code` when it failed.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "responses": {
          "200": {
            "description": "The post.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updatePost",
        "summary": "Change a post",
        "description": "Changes a draft or a scheduled post: its text, its time, its accounts or its platform options, or schedules or publishes a draft. Adding a platform to a post already scheduled (\"put it on Instagram too\") is this, not a second post.\n\nThe post is checked as a whole, as a new one would be. Only `draft` and `scheduled` posts change; once one has started publishing the answer is `409 conflict`. A draft stays a draft until `scheduled_at` schedules it or `publish_now` sends it, and needs an account by then. `publish_now` also brings a scheduled post forward. Unless the post stays a draft, an account that cannot post until it is connected again is refused with `422 reconnect_required`.\n\nWhatever the change newly reaches is counted against the quota: an added account, or new text with a link on a post to X. Nothing is refunded when accounts are removed. A draft was counted when it was saved, so sending it costs nothing more unless the same request adds accounts.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "requestBody": {
          "description": "What to change.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdatePostRequest"
              },
              "examples": {
                "add_instagram": {
                  "value": {
                    "add_platforms": [
                      "instagram"
                    ],
                    "profile": "Acme"
                  }
                },
                "move": {
                  "value": {
                    "scheduled_at": "2026-10-08T09:00:00+01:00"
                  }
                },
                "publish_now": {
                  "value": {
                    "publish_now": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The post as it now is; `processing` after `publish_now`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      },
      "delete": {
        "operationId": "deletePost",
        "summary": "Delete a post",
        "description": "Deletes a post from Postatron, by the dashboard's rule. A draft, or a scheduled post more than two minutes from its time, is removed and never goes out (`cancelled: true` for a scheduled one). A published, failed or partly published post is removed from Postatron only: nothing is ever deleted on a platform, and `message` names where it is still live. A post being published right now, or due within two minutes, is left alone: the answer is still `200`, with `deleted: false` and the reason.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "responses": {
          "200": {
            "description": "What deleting it did.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletePostResponse"
                },
                "example": {
                  "id": "0AbCdEfGhIjKlMnOpQrS",
                  "status": "published",
                  "deleted": true,
                  "cancelled": false,
                  "message": "Removed from Postatron only. It is still live on X and LinkedIn; delete it there if it should go."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/posts/bulk-delete": {
      "post": {
        "operationId": "deletePosts",
        "summary": "Delete several posts",
        "description": "Deletes up to 100 posts, each by the rule of `DELETE /v1/posts/{post_id}`. Every id comes back in exactly one of `deleted` and `failed`, in the order asked; duplicates are dropped.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "requestBody": {
          "description": "The posts.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeletePostsRequest"
              },
              "examples": {
                "two": {
                  "value": {
                    "ids": [
                      "0AbCd",
                      "1EfGh"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Which were deleted and which were not.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletePostsResponse"
                },
                "example": {
                  "deleted": [
                    "0AbCd"
                  ],
                  "failed": [
                    {
                      "id": "1EfGh",
                      "error": "Not deleted: it is about to be published."
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/posts/{post_id}/retry": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PostId"
        }
      ],
      "post": {
        "operationId": "retryPost",
        "summary": "Retry a failed post",
        "description": "Sends a `failed` or `partial` post again, to the accounts it failed on only: every one, or those `account_ids` names. An account it already published to is never posted to twice. It is not counted against the quota again. Poll `GET /v1/posts/{post_id}` for the outcome, which also appears in `GET /v1/events`.\n\nRefused with `409 conflict` when the post is not `failed` or `partial`, nothing on it failed, or a named account did not fail (`details.reason`); with `422 validation_error` when a named account is not on the post; and with `422 reconnect_required` when an account to retry cannot post until it is connected again, or is no longer connected, in which case nothing is sent.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "requestBody": {
          "description": "Optional: which accounts to send again. An empty body sends every account that failed.",
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RetryPostRequest"
              },
              "examples": {
                "one": {
                  "value": {
                    "account_ids": [
                      "1784140000000"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The post, now `processing`, with the retried deliveries `pending`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "503": {
            "description": "`internal_error`: retrying this post is not available right now. Try again later.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "examples": {
                  "unavailable": {
                    "value": {
                      "error": {
                        "code": "internal_error",
                        "message": "retrying this post is not available right now; try again later"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/events": {
      "get": {
        "operationId": "listEvents",
        "summary": "Read the event log",
        "description": "The account's event log, newest first: the dashboard's Logs page. Each post's outcome on every account it went to, accounts connected, disconnected or needing a reconnect, and calls to the API or MCP that changed something or were refused (successful reads are not logged). Events are kept for 90 days. A team workspace key sees what the Logs page shows that member: events about their profiles, and what they did themselves.",
        "tags": [
          "Events"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "How far back, ending now.",
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d",
                "90d"
              ],
              "default": "7d"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "`ok`, `warning` or `error`; several separated by commas.",
            "schema": {
              "type": "string"
            },
            "example": "warning,error"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "`posts`, `accounts` or `api`; several separated by commas.",
            "schema": {
              "type": "string"
            },
            "example": "posts"
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Only events about an account on these platforms; several separated by commas.",
            "schema": {
              "type": "string"
            },
            "example": "instagram"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of events.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/accounts": {
      "get": {
        "operationId": "listAccounts",
        "summary": "List connected accounts",
        "description": "The connected social accounts and the profile each is in. An account with `status: reconnect_required` cannot post until the person signs it in again: `POST /v1/accounts/connect` with its `account_id` gives the link.",
        "tags": [
          "Accounts"
        ],
        "security": [
          {
            "ApiKey": [
              "accounts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "accounts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "responses": {
          "200": {
            "description": "The accounts.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountList"
                },
                "example": {
                  "data": [
                    {
                      "id": "1234567890",
                      "platform": "x",
                      "username": "acmehq",
                      "display_name": "Acme",
                      "status": "connected",
                      "profile_id": "default",
                      "profile_name": "Default",
                      "connected_at": "2026-08-30T18:02:11Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/accounts/connect": {
      "post": {
        "operationId": "connectAccount",
        "summary": "Get a link to connect an account",
        "description": "Returns a link that connects one of the person's social accounts, or signs one in again. Platforms' sign-ins happen in the person's own browser, so an agent hands over the link and the person finishes there; nothing is connected until they do. The link only works for the person whose key asked for it, and a team workspace key's link opens the dashboard in that workspace.\n\nRefused with `402 quota_exceeded` when the plan's accounts are all connected, and `409 conflict` when the profile already has an account on that platform (pass `account_id` to sign that one in again). A Pinterest account must be a Pinterest business account; a free one will do.",
        "tags": [
          "Accounts"
        ],
        "security": [
          {
            "ApiKey": [
              "accounts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "accounts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "requestBody": {
          "description": "What to connect.",
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectAccountRequest"
              },
              "examples": {
                "new": {
                  "value": {
                    "platform": "x",
                    "profile": "Acme"
                  }
                },
                "reconnect": {
                  "value": {
                    "account_id": "1784140000000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The link.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectLink"
                },
                "example": {
                  "platform": "x",
                  "connect_url": "https://postatron.com/dashboard/socials?connect=x&for=3f6c1d2e8a4b4f0e&profile=8f2c1a9e",
                  "profile": "Acme",
                  "hint": "Open the link and finish signing in to X; it goes into the Acme profile. Tell me when it is done and I will check with list_accounts."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/profiles": {
      "get": {
        "operationId": "listProfiles",
        "summary": "List profiles",
        "description": "The profiles and the accounts in each. A profile groups connected accounts, usually one per brand or client, and holds at most one account per platform. Every account has a Default profile; plans allow more (Starter 2, Growth 5, Scale 20). Wherever the API takes a `profile`, it accepts a profile's `id` or its name, ignoring case. A team workspace key sees only the profiles its holder was granted.",
        "tags": [
          "Profiles"
        ],
        "security": [
          {
            "ApiKey": [
              "accounts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "accounts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "responses": {
          "200": {
            "description": "The profiles.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileList"
                },
                "example": {
                  "data": [
                    {
                      "id": "default",
                      "name": "Default",
                      "is_default": true,
                      "accounts": [
                        {
                          "id": "1234567890",
                          "platform": "x",
                          "username": "acmehq",
                          "display_name": "Acme",
                          "status": "connected",
                          "profile_id": "default",
                          "profile_name": "Default",
                          "connected_at": "2026-08-30T18:02:11Z"
                        }
                      ]
                    },
                    {
                      "id": "8f2c1a9e",
                      "name": "Acme",
                      "is_default": false,
                      "accounts": []
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/queues": {
      "get": {
        "operationId": "listQueues",
        "summary": "List queues",
        "description": "The posting queues, each with its profile, weekly slots and the next free slot. Pass a queue's `name` or `id` as `queue` to `POST /v1/posts` to give a post that slot.",
        "tags": [
          "Queues"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "responses": {
          "200": {
            "description": "The queues.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueueList"
                },
                "example": {
                  "data": [
                    {
                      "id": "q7k2m9xw4a",
                      "name": "Weekday mornings",
                      "profile_id": "default",
                      "profile_name": "Default",
                      "timezone": "Europe/London",
                      "slots": [
                        {
                          "day": 1,
                          "time": "09:00"
                        },
                        {
                          "day": 3,
                          "time": "09:00"
                        }
                      ],
                      "active": true,
                      "next_slot": "2026-10-12T08:00:00Z",
                      "queued": 3
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createQueue",
        "summary": "Create a queue",
        "description": "Creates a weekly posting schedule for one profile. Each plan has a number of queues, paused ones included: Starter 1, Growth 5, Scale unlimited (up to 100). Past it, the answer is `402 quota_exceeded` with `details.resource` `queues`.",
        "tags": [
          "Queues"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "requestBody": {
          "description": "The queue.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateQueueRequest"
              },
              "examples": {
                "weekday": {
                  "value": {
                    "name": "Weekday mornings",
                    "profile": "Acme",
                    "timezone": "Europe/London",
                    "slots": [
                      {
                        "day": 1,
                        "time": "09:00"
                      },
                      {
                        "day": 3,
                        "time": "09:00"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The queue, as `GET /v1/queues` shows it.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Queue"
                },
                "example": {
                  "id": "q7k2m9xw4a",
                  "name": "Weekday mornings",
                  "profile_id": "default",
                  "profile_name": "Default",
                  "timezone": "Europe/London",
                  "slots": [
                    {
                      "day": 1,
                      "time": "09:00"
                    },
                    {
                      "day": 3,
                      "time": "09:00"
                    }
                  ],
                  "active": true,
                  "next_slot": "2026-10-12T08:00:00Z",
                  "queued": 3
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/queues/{queue_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/QueueId"
        }
      ],
      "patch": {
        "operationId": "updateQueue",
        "summary": "Change a queue",
        "description": "Changes, pauses or resumes a queue. Posts it already placed keep their times unless `move_posts` is set and the slots or timezone change: then its upcoming posts take the new slots, soonest first, in the order they were going out, and the answer carries `rescheduled` (`moved` and `kept`) or `reschedule_error`. The change to the queue is saved either way.",
        "tags": [
          "Queues"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "requestBody": {
          "description": "What to change.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateQueueRequest"
              },
              "examples": {
                "new_slots": {
                  "value": {
                    "slots": [
                      {
                        "day": 2,
                        "time": "08:30"
                      }
                    ],
                    "move_posts": true
                  }
                },
                "pause": {
                  "value": {
                    "active": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The queue as it now is.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Queue"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteQueue",
        "summary": "Delete a queue",
        "description": "Deletes a queue. Posts it already placed stay scheduled at their times.",
        "tags": [
          "Queues"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "responses": {
          "200": {
            "description": "It is gone.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteQueueResponse"
                },
                "example": {
                  "id": "q7k2m9xw4a",
                  "deleted": true,
                  "message": "Posts it had already placed stay scheduled at their times."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Read usage and quota",
        "description": "This month's quota (UTC calendar month) and what has been used, posts per platform, and the rate limits in force for the key. Read limits from here rather than hardcoding them. Usage is metered in destination-posts: one post delivered to one account. X posts with a link have their own monthly cap (`x_link_posts`). Every cap is a hard stop; nothing is billed beyond the plan.",
        "tags": [
          "Usage"
        ],
        "security": [
          {
            "ApiKey": [
              "usage:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "usage:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "responses": {
          "200": {
            "description": "The report.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageReport"
                },
                "example": {
                  "period": {
                    "start": "2026-09-01T00:00:00Z",
                    "end": "2026-10-01T00:00:00Z"
                  },
                  "plan": {
                    "code": "GROWTH",
                    "name": "Growth",
                    "interval": "month",
                    "status": "ACTIVE",
                    "connected_accounts": 6,
                    "max_connected_accounts": 15
                  },
                  "destination_posts": {
                    "used": 412,
                    "limit": 1500,
                    "remaining": 1088
                  },
                  "link_posts": {
                    "used": 96
                  },
                  "x_link_posts": {
                    "used": 11,
                    "limit": 80,
                    "remaining": 69
                  },
                  "by_platform": {
                    "x": 128,
                    "linkedin": 104,
                    "bluesky": 71,
                    "threads": 58,
                    "facebook": 51
                  },
                  "rate_limits": {
                    "writes_per_minute": 30,
                    "reads_per_minute": 60,
                    "uploads_per_minute": 120,
                    "writes_per_hour": 1800
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/analytics": {
      "get": {
        "operationId": "getAnalytics",
        "summary": "Read analytics",
        "description": "Engagement, reach, followers and the best posts over a window: the numbers behind the dashboard's Analytics page, from the same service and cache. Each account is read from its platform at most once an hour; `last_sync_at` and `next_sync_at` say when, so asking sooner returns the same report. X and Pinterest are not included. YouTube's numbers are in `per_platform` as YouTube reports them, but its API policies keep them out of engagement totals, rates and `top_posts`.",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "The window, ending now.",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "30d",
                "90d",
                "365d"
              ],
              "default": "30d"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Only this platform.",
            "schema": {
              "$ref": "#/components/schemas/Platform"
            }
          },
          {
            "name": "account_id",
            "in": "query",
            "required": false,
            "description": "Only this connected account.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "profile",
            "in": "query",
            "required": false,
            "description": "Only accounts in this profile (id or name).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "`all`, or `postatron` for posts made with Postatron only.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "postatron"
              ],
              "default": "all"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsReport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/media/uploads": {
      "post": {
        "operationId": "createUpload",
        "summary": "Open an upload slot",
        "description": "For an agent that cannot send a file itself: it gets a link, gives it to the person, and they open it signed in to Postatron and choose the file. The link carries no credential, so someone who intercepts it gets a sign-in page. It works for 30 minutes. Then `GET /v1/media/uploads/{upload_id}` says whether the file has arrived, and a `READY` upload's id goes in `media_ids`.",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "upload",
        "requestBody": {
          "description": "Optional: what the file is for.",
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUploadRequest"
              },
              "examples": {
                "photo": {
                  "value": {
                    "purpose": "photo for Tuesday's post"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The slot.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Upload"
                },
                "example": {
                  "id": "u_8f2c1a9e",
                  "status": "PENDING",
                  "upload_url": "https://postatron.com/dashboard/upload?u=u_8f2c1a9e",
                  "expires_at": "2026-10-07T08:30:00Z",
                  "hint": "Send this link to the person and wait for them to say they have uploaded the file. Then call get_upload, and pass the id to create_post as media_ids."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/media/uploads/{upload_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UploadId"
        }
      ],
      "get": {
        "operationId": "getUpload",
        "summary": "Check an upload",
        "description": "Whether the person has uploaded the file yet. An expired slot is refused with `422 validation_error`; open a new one.",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "responses": {
          "200": {
            "description": "The slot.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Upload"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "summary": "List webhooks",
        "description": "The account's webhooks, oldest first, each with how its last delivery went. Secrets are never listed. Webhooks belong to the account's owner, because they carry every post in it across every profile, so a team workspace key is refused with `403 workspace_forbidden`.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "read",
        "responses": {
          "200": {
            "description": "The webhooks.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookList"
                },
                "example": {
                  "data": [
                    {
                      "id": "wh_5c0f3a9e2b7d4c1e8f6a0b9d",
                      "url": "https://example.com/webhooks/postatron",
                      "events": [
                        "*"
                      ],
                      "enabled": true,
                      "created_at": "2026-11-01T08:00:00Z",
                      "last_attempt": {
                        "at": "2026-11-01T09:00:01Z",
                        "event": "post.partial",
                        "succeeded": true,
                        "status_code": 200,
                        "duration_ms": 182
                      },
                      "consecutive_failures": 0
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "summary": "Add a webhook",
        "description": "Adds an endpoint that receives the account's events, switched on. An account can have five; a sixth is refused with `422 validation_error`. The URL must be `https`, with no user name, password or fragment, and its host must resolve only to public addresses, checked now and again before every delivery. The answer carries the signing `secret`, which is never shown again. Choosing what a webhook receives also needs the `posts:read` scope, since it will receive the account's posts. Webhooks belong to the account's owner, because they carry every post in it across every profile, so a team workspace key is refused with `403 workspace_forbidden`.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write",
              "posts:read"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write",
              "posts:read"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "requestBody": {
          "description": "Where to send events, and which.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookRequest"
              },
              "examples": {
                "failures": {
                  "value": {
                    "url": "https://example.com/webhooks/postatron",
                    "events": [
                      "post.failed",
                      "post.partial",
                      "account.needs_attention"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The webhook, with its `secret`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/webhooks/{webhook_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookId"
        }
      ],
      "patch": {
        "operationId": "updateWebhook",
        "summary": "Change a webhook",
        "description": "Changes a webhook's URL or events, or switches it off or on; switching it on clears its failure count. A new URL is checked like a first one. Changing `url` or `events` also needs the `posts:read` scope. Webhooks belong to the account's owner, because they carry every post in it across every profile, so a team workspace key is refused with `403 workspace_forbidden`.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "requestBody": {
          "description": "What to change.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWebhookRequest"
              },
              "examples": {
                "enable": {
                  "value": {
                    "enabled": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The webhook as it now is.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Delete a webhook",
        "description": "Deletes a webhook. Nothing more is sent to it, including deliveries already waiting. Webhooks belong to the account's owner, because they carry every post in it across every profile, so a team workspace key is refused with `403 workspace_forbidden`.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "responses": {
          "200": {
            "description": "It is gone.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteWebhookResponse"
                },
                "example": {
                  "id": "wh_5c0f3a9e2b7d4c1e8f6a0b9d",
                  "deleted": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/webhooks/{webhook_id}/test": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookId"
        }
      ],
      "post": {
        "operationId": "testWebhook",
        "summary": "Send a test event",
        "description": "Sends a signed `webhook.test` event there and then, even to a webhook that is switched off, and answers with what the endpoint said. A failed test is recorded as the webhook's last attempt but does not count towards switching it off. Webhooks belong to the account's owner, because they carry every post in it across every profile, so a team workspace key is refused with `403 workspace_forbidden`.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "responses": {
          "200": {
            "description": "What the endpoint answered.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResult"
                },
                "example": {
                  "delivery_id": "whd_3f6c1d2e8a4b4f0e9c1d7b2a5e6f8d90",
                  "delivered": false,
                  "status_code": 500,
                  "error": "The endpoint answered 500 Internal Server Error.",
                  "duration_ms": 87
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/webhooks/{webhook_id}/rotate-secret": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookId"
        }
      ],
      "post": {
        "operationId": "rotateWebhookSecret",
        "summary": "Rotate a webhook's secret",
        "description": "Gives the webhook a new signing secret, in this answer only. The old secret keeps signing beside the new one for 24 hours, so a receiver can switch over without refusing events. Webhooks belong to the account's owner, because they carry every post in it across every profile, so a team workspace key is refused with `403 workspace_forbidden`.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "ApiKey": [
              "posts:write"
            ]
          },
          {
            "ApiKeyHeader": [
              "posts:write"
            ]
          }
        ],
        "x-postatron-rate-limit": "write",
        "responses": {
          "200": {
            "description": "The webhook, with its new `secret`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/openapi.json": {
      "get": {
        "operationId": "getOpenAPI",
        "summary": "This description",
        "description": "This OpenAPI document. It needs no key, plan or scope, and is not rate limited per key. The same document ships in the Go module as `apiv1.OpenAPI`.",
        "tags": [
          "Meta"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 description of the API.",
            "headers": {
              "Cache-Control": {
                "description": "`public, max-age=3600`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "post.published": {
      "post": {
        "operationId": "webhook_post_published",
        "summary": "A post went out on every account",
        "description": "`data.post` is the post as `GET /v1/posts/{post_id}` describes it, read when the event is sent.\n\nAnswer with any 2xx status within 10 seconds, and do slow work afterwards. Anything else, a redirect included, is a failure, and the delivery is tried again after about 1 minute, 10 minutes, 1 hour, 6 hours and 12 hours, six attempts in all, each with the same `Postatron-Delivery`. After 20 deliveries in a row fail every attempt the webhook is switched off.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PostatronSignature"
          },
          {
            "name": "Postatron-Event",
            "in": "header",
            "required": true,
            "description": "The event's type, the same as the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "post.published"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/PostatronDelivery"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPostEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx status acknowledges the delivery."
          },
          "default": {
            "description": "Anything else, or no answer within 10 seconds, is a failure, and the delivery is tried again."
          }
        }
      }
    },
    "post.partial": {
      "post": {
        "operationId": "webhook_post_partial",
        "summary": "A post went out on some accounts and failed on others",
        "description": "`data.post` is the post as `GET /v1/posts/{post_id}` describes it, read when the event is sent. Each failed delivery has `error` and `error_code`.\n\nAnswer with any 2xx status within 10 seconds, and do slow work afterwards. Anything else, a redirect included, is a failure, and the delivery is tried again after about 1 minute, 10 minutes, 1 hour, 6 hours and 12 hours, six attempts in all, each with the same `Postatron-Delivery`. After 20 deliveries in a row fail every attempt the webhook is switched off.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PostatronSignature"
          },
          {
            "name": "Postatron-Event",
            "in": "header",
            "required": true,
            "description": "The event's type, the same as the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "post.partial"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/PostatronDelivery"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPostEvent"
              },
              "example": {
                "id": "whd_9f2c4e1b7a3d5e6f8091a2b3c4d5e6f7",
                "type": "post.partial",
                "created_at": "2026-11-01T09:00:00Z",
                "data": {
                  "post": {
                    "id": "0AbCdEfGhIjKlMnOpQrS",
                    "status": "partial",
                    "content": "Launch day!",
                    "media_type": "text",
                    "contains_link": false,
                    "created_at": "2026-11-01T08:59:58Z",
                    "source": "api",
                    "deliveries": [
                      {
                        "account_id": "1234567890",
                        "platform": "x",
                        "username": "acmehq",
                        "status": "published",
                        "url": "https://x.com/acmehq/status/1"
                      },
                      {
                        "account_id": "1784140000000",
                        "platform": "instagram",
                        "username": "acme",
                        "status": "failed",
                        "error": "Instagram needs an image or a video.",
                        "error_code": "validation"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx status acknowledges the delivery."
          },
          "default": {
            "description": "Anything else, or no answer within 10 seconds, is a failure, and the delivery is tried again."
          }
        }
      }
    },
    "post.failed": {
      "post": {
        "operationId": "webhook_post_failed",
        "summary": "A post failed on every account",
        "description": "`data.post` is the post as `GET /v1/posts/{post_id}` describes it, read when the event is sent. Each delivery has `error` and `error_code`.\n\nAnswer with any 2xx status within 10 seconds, and do slow work afterwards. Anything else, a redirect included, is a failure, and the delivery is tried again after about 1 minute, 10 minutes, 1 hour, 6 hours and 12 hours, six attempts in all, each with the same `Postatron-Delivery`. After 20 deliveries in a row fail every attempt the webhook is switched off.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PostatronSignature"
          },
          {
            "name": "Postatron-Event",
            "in": "header",
            "required": true,
            "description": "The event's type, the same as the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "post.failed"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/PostatronDelivery"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPostEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx status acknowledges the delivery."
          },
          "default": {
            "description": "Anything else, or no answer within 10 seconds, is a failure, and the delivery is tried again."
          }
        }
      }
    },
    "post.unpublished": {
      "post": {
        "operationId": "webhook_post_unpublished",
        "summary": "A scheduled post went back to the drafts",
        "description": "A scheduled post came due while the account had no plan, so it went back to the drafts instead of out; `data.post.unpublished` says when it was due. `data.post` is the post as `GET /v1/posts/{post_id}` describes it, read when the event is sent.\n\nAnswer with any 2xx status within 10 seconds, and do slow work afterwards. Anything else, a redirect included, is a failure, and the delivery is tried again after about 1 minute, 10 minutes, 1 hour, 6 hours and 12 hours, six attempts in all, each with the same `Postatron-Delivery`. After 20 deliveries in a row fail every attempt the webhook is switched off.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PostatronSignature"
          },
          {
            "name": "Postatron-Event",
            "in": "header",
            "required": true,
            "description": "The event's type, the same as the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "post.unpublished"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/PostatronDelivery"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPostEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx status acknowledges the delivery."
          },
          "default": {
            "description": "Anything else, or no answer within 10 seconds, is a failure, and the delivery is tried again."
          }
        }
      }
    },
    "account.connected": {
      "post": {
        "operationId": "webhook_account_connected",
        "summary": "A social account was connected",
        "description": "`data.account` is the account as `GET /v1/accounts` describes it.\n\nAnswer with any 2xx status within 10 seconds, and do slow work afterwards. Anything else, a redirect included, is a failure, and the delivery is tried again after about 1 minute, 10 minutes, 1 hour, 6 hours and 12 hours, six attempts in all, each with the same `Postatron-Delivery`. After 20 deliveries in a row fail every attempt the webhook is switched off.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PostatronSignature"
          },
          {
            "name": "Postatron-Event",
            "in": "header",
            "required": true,
            "description": "The event's type, the same as the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "account.connected"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/PostatronDelivery"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookAccountEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx status acknowledges the delivery."
          },
          "default": {
            "description": "Anything else, or no answer within 10 seconds, is a failure, and the delivery is tried again."
          }
        }
      }
    },
    "account.disconnected": {
      "post": {
        "operationId": "webhook_account_disconnected",
        "summary": "A social account was removed",
        "description": "`data.account` has `status: disconnected`, and `data.reason` says how it went: `user`, `expired` or `revoked`. When Postatron lost access with posts scheduled to it, `data.at_risk` lists them.\n\nAnswer with any 2xx status within 10 seconds, and do slow work afterwards. Anything else, a redirect included, is a failure, and the delivery is tried again after about 1 minute, 10 minutes, 1 hour, 6 hours and 12 hours, six attempts in all, each with the same `Postatron-Delivery`. After 20 deliveries in a row fail every attempt the webhook is switched off.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PostatronSignature"
          },
          {
            "name": "Postatron-Event",
            "in": "header",
            "required": true,
            "description": "The event's type, the same as the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "account.disconnected"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/PostatronDelivery"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookAccountEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx status acknowledges the delivery."
          },
          "default": {
            "description": "Anything else, or no answer within 10 seconds, is a failure, and the delivery is tried again."
          }
        }
      }
    },
    "account.needs_attention": {
      "post": {
        "operationId": "webhook_account_needs_attention",
        "summary": "An account must be connected again",
        "description": "Scheduled posts to the account will fail until the person connects it again; `data.at_risk` lists up to ten, soonest first, and `data.at_risk_count` counts them all. `POST /v1/accounts/connect` with its `account_id` gives the link.\n\nAnswer with any 2xx status within 10 seconds, and do slow work afterwards. Anything else, a redirect included, is a failure, and the delivery is tried again after about 1 minute, 10 minutes, 1 hour, 6 hours and 12 hours, six attempts in all, each with the same `Postatron-Delivery`. After 20 deliveries in a row fail every attempt the webhook is switched off.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PostatronSignature"
          },
          {
            "name": "Postatron-Event",
            "in": "header",
            "required": true,
            "description": "The event's type, the same as the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "account.needs_attention"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/PostatronDelivery"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookAccountEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx status acknowledges the delivery."
          },
          "default": {
            "description": "Anything else, or no answer within 10 seconds, is a failure, and the delivery is tried again."
          }
        }
      }
    },
    "webhook.test": {
      "post": {
        "operationId": "webhook_webhook_test",
        "summary": "A test event",
        "description": "Sent by `POST /v1/webhooks/{webhook_id}/test`.\n\nAnswer with any 2xx status within 10 seconds, and do slow work afterwards. Anything else, a redirect included, is a failure, and the delivery is tried again after about 1 minute, 10 minutes, 1 hour, 6 hours and 12 hours, six attempts in all, each with the same `Postatron-Delivery`. After 20 deliveries in a row fail every attempt the webhook is switched off.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/PostatronSignature"
          },
          {
            "name": "Postatron-Event",
            "in": "header",
            "required": true,
            "description": "The event's type, the same as the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "webhook.test"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/PostatronDelivery"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookTestEvent"
              },
              "example": {
                "id": "whd_3f6c1d2e8a4b4f0e9c1d7b2a5e6f8d90",
                "type": "webhook.test",
                "created_at": "2026-11-01T09:00:00Z",
                "data": {
                  "webhook_id": "wh_5c0f3a9e2b7d4c1e8f6a0b9d",
                  "message": "A test event from Postatron. Check its signature as you would any other event's, then answer with a 2xx status."
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx status acknowledges the delivery."
          },
          "default": {
            "description": "Anything else, or no answer within 10 seconds, is a failure, and the delivery is tried again."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ptn_ API key",
        "description": "An API key as a bearer token: `Authorization: Bearer ptn_...`. Only API keys are accepted here; OAuth access tokens (`pto_...`) are for the MCP endpoint."
      },
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "The same API key in its own header: `X-API-Key: ptn_...`."
      }
    },
    "parameters": {
      "PostId": {
        "name": "post_id",
        "in": "path",
        "required": true,
        "description": "The post's id.",
        "schema": {
          "type": "string"
        }
      },
      "QueueId": {
        "name": "queue_id",
        "in": "path",
        "required": true,
        "description": "The queue's id, or its name.",
        "schema": {
          "type": "string"
        }
      },
      "WebhookId": {
        "name": "webhook_id",
        "in": "path",
        "required": true,
        "description": "The webhook's id.",
        "schema": {
          "type": "string"
        }
      },
      "UploadId": {
        "name": "upload_id",
        "in": "path",
        "required": true,
        "description": "The upload's id.",
        "schema": {
          "type": "string"
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "`next_cursor` from the previous page.",
        "schema": {
          "type": "string"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "How many to return, 1 to 100 (more is treated as 100).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Makes the request safe to send again. 1 to 255 printable ASCII characters, unique to the post you mean to make; a UUID is a good choice. The first request with a key runs as usual and, if it succeeds, its answer is kept for 24 hours: the same request with the same key gets that answer back with `Idempotent-Replayed: true`, creating nothing and using no quota (the body is compared as JSON, so field order and spacing do not matter). The same key with a different request is refused with `422 idempotency_key_reused`; while the first is still running, with `409 idempotency_key_in_use` and `Retry-After`. If the first request never finished, the key is free again two minutes after it started. A refusal is not kept, so the key can be used again once the request is put right. Keys are scoped to the person, so teammates never collide.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255,
          "pattern": "^[\\x20-\\x7E]+$"
        }
      },
      "PostatronSignature": {
        "name": "Postatron-Signature",
        "in": "header",
        "required": true,
        "description": "`t=<unix seconds>,v1=<signature>`. The signature is the hex HMAC-SHA256 of `<t>.<raw body>`, keyed with the webhook's whole secret, `whsec_` included. Check it on the raw body exactly as it arrived, before parsing the JSON, and refuse a `t` more than five minutes from now. For 24 hours after the secret is rotated the header carries a second `v1`, made with the old secret; accept the request if either matches. The Go module's `apiv1.VerifyWebhook` does all of this.",
        "schema": {
          "type": "string"
        },
        "example": "t=1793523600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
      },
      "PostatronDelivery": {
        "name": "Postatron-Delivery",
        "in": "header",
        "required": true,
        "description": "The delivery's id, the same as the body's `id`. Every attempt, and any repeat, carries the same id.",
        "schema": {
          "type": "string"
        }
      },
      "WebhookUserAgent": {
        "name": "User-Agent",
        "in": "header",
        "required": true,
        "description": "Always `Postatron-Webhooks/1`.",
        "schema": {
          "type": "string",
          "enum": [
            "Postatron-Webhooks/1"
          ]
        }
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Requests of this operation's rate limit class (`x-postatron-rate-limit`) the key may make in the current one-minute window. Sent on every 2xx answer, and on refusals from the rate limit check onwards; a `401`, and most `403`s for a scope or a team workspace, come before it and have none.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests of this class left in the current window. Sent on every 2xx answer, and on refusals from the rate limit check onwards; a `401`, and most `403`s for a scope or a team workspace, come before it and have none.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "When the current window ends, in unix seconds. Sent on every 2xx answer, and on refusals from the rate limit check onwards; a `401`, and most `403`s for a scope or a team workspace, come before it and have none.",
        "schema": {
          "type": "integer"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before sending the request again.",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "Idempotent-Replayed": {
        "description": "`true` when this is the answer kept from an earlier request with the same `Idempotency-Key`: nothing was created this time, and no quota was used.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`validation_error`: a media link (`media_urls`, a thread tweet's media or a YouTube thumbnail) could not be used. It is not a public `https` address, the file is too large, it is not an image or a video, it redirected too often, or it could not be downloaded. Give another link.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "private_address": {
                "value": {
                  "error": {
                    "code": "validation_error",
                    "message": "media_urls[0] (http://10.0.0.5/a.jpg): must be a public https URL. Addresses inside a private network are refused."
                  }
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`unauthorized`: the API key is missing, malformed, unknown or revoked. Send it as `Authorization: Bearer ptn_...` or `X-API-Key: ptn_...`. OAuth access tokens (`pto_...`) are refused here too: they are for the MCP endpoint only.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "missing": {
                "value": {
                  "error": {
                    "code": "unauthorized",
                    "message": "missing API key: send it as 'Authorization: Bearer ptn_...'"
                  }
                }
              },
              "revoked": {
                "value": {
                  "error": {
                    "code": "unauthorized",
                    "message": "API key has been revoked"
                  }
                }
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "`quota_exceeded`: the request would go past one of the plan's hard caps, and nothing was done. `details.resource` names it: `destination_posts` or `x_link_posts` (monthly, with `used`, `limit`, `requested` and `resets_at`), `connected_accounts`, or `queues` (with `limit`).",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "destination_posts": {
                "value": {
                  "error": {
                    "code": "quota_exceeded",
                    "message": "destination-post limit reached: 300 of 300 used this month, 2 requested; resets 2026-10-01T00:00:00Z. Upgrade your plan or wait for the next period.",
                    "details": {
                      "resource": "destination_posts",
                      "used": 300,
                      "limit": 300,
                      "requested": 2,
                      "resets_at": "2026-10-01T00:00:00Z"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Refused for who is asking:\n\n- `insufficient_scope`: the key lacks the scope the operation needs; `details.required_scope` names it and `details.key_scopes` lists the key's.\n- `subscription_required`: the account has no active plan, which every operation needs, reads included; `details.plans_url` is where to choose one.\n- `workspace_forbidden`: the key works in a team workspace its holder can no longer work in (they left or were removed, or the owner's plan no longer includes teams), or asks for something only the owner may do, such as managing webhooks.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "insufficient_scope": {
                "value": {
                  "error": {
                    "code": "insufficient_scope",
                    "message": "this API key does not have the posts:write scope",
                    "details": {
                      "required_scope": "posts:write",
                      "key_scopes": [
                        "posts:read"
                      ]
                    }
                  }
                }
              },
              "subscription_required": {
                "value": {
                  "error": {
                    "code": "subscription_required",
                    "message": "this Postatron account has no active plan, which the API, MCP server and CLI need; choose one at https://postatron.com/dashboard/plans",
                    "details": {
                      "plans_url": "https://postatron.com/dashboard/plans"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "`not_found`: there is no such post, account, queue, upload or webhook in this account, or none the key may see.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "post": {
                "value": {
                  "error": {
                    "code": "not_found",
                    "message": "post not found"
                  }
                }
              }
            }
          }
        }
      },
      "Conflict": {
        "description": "The resource is not in a state the request can act on, and nothing was changed. `conflict`: the post has started publishing, a queue is paused or cannot take the post, a retry found nothing that failed (`details.reason` is `not_retryable`, `not_failed` or `conflict`), or a profile already has an account on the platform. `idempotency_key_in_use`: a request with the same `Idempotency-Key` is still being handled; send it again after `Retry-After`.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "Retry-After": {
            "description": "With `idempotency_key_in_use`: seconds to wait before sending the request again.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "published": {
                "value": {
                  "error": {
                    "code": "conflict",
                    "message": "only draft and scheduled posts can change; this one is published",
                    "details": {
                      "status": "published"
                    }
                  }
                }
              },
              "queue_paused": {
                "value": {
                  "error": {
                    "code": "validation_error",
                    "message": "the queue \"Weekday mornings\" is paused; resume it (update_queue with active true) or give scheduled_at instead"
                  }
                }
              },
              "idempotency_key_in_use": {
                "value": {
                  "error": {
                    "code": "idempotency_key_in_use",
                    "message": "a request with this Idempotency-Key is still being handled; wait a moment, then send it again"
                  }
                }
              }
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "The request cannot be carried out as it stands:\n\n- `validation_error`: bad input; the message says what and `details` names the field. For example an unknown profile (`details.profiles` lists them), a platform with accounts in two profiles and no `profile` (`details.platforms`, `details.profiles`), text over a platform's limit (`details.platform`, `limit`, `length`), a platform that needs media, an unknown Pinterest board (`details.boards`), or a TikTok setting the creator's account does not allow.\n- `reconnect_required`: an account the post would go to cannot post until it is connected again; `details.account_ids` names them, and `POST /v1/accounts/connect` with each `account_id` gives the link.\n- `idempotency_key_reused`: the `Idempotency-Key` was used in the last 24 hours for a different request; use a new key.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "content": {
                "value": {
                  "error": {
                    "code": "validation_error",
                    "message": "content is required"
                  }
                }
              },
              "too_long": {
                "value": {
                  "error": {
                    "code": "validation_error",
                    "message": "content is 312 characters; X allows 280 for @acmehq",
                    "details": {
                      "platform": "x",
                      "account_id": "1234567890",
                      "limit": 280,
                      "length": 312
                    }
                  }
                }
              },
              "reconnect": {
                "value": {
                  "error": {
                    "code": "reconnect_required",
                    "message": "Reconnect your Instagram account @acme before scheduling to it: Instagram has refused its access. connect_account with its account_id (POST /v1/accounts/connect) gives a link that signs it in again.",
                    "details": {
                      "account_ids": [
                        "1784140000000"
                      ],
                      "platforms": [
                        "instagram"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited`: the key has made this class's limit of requests this minute. Wait `Retry-After` seconds; `details` has `limit`, `class`, `retry_after` and `reset_at`.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "writes": {
                "value": {
                  "error": {
                    "code": "rate_limited",
                    "message": "rate limit of 30 write requests per minute exceeded; retry in 17 seconds",
                    "details": {
                      "limit": 30,
                      "class": "write",
                      "retry_after": 17,
                      "reset_at": "2026-09-05T10:01:00Z"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "`internal_error`: something broke on Postatron's side. It is safe to send a read again; send a create again with the same `Idempotency-Key`.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "internal": {
                "value": {
                  "error": {
                    "code": "internal_error",
                    "message": "internal error"
                  }
                }
              }
            }
          }
        }
      },
      "BadGateway": {
        "description": "`internal_error`: a platform Postatron had to ask before accepting the post (TikTok for the creator's settings, Pinterest for the account's boards) did not answer as expected. Try again in a moment.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "tiktok": {
                "value": {
                  "error": {
                    "code": "internal_error",
                    "message": "TikTok did not answer as expected; try again in a moment"
                  }
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Platform": {
        "type": "string",
        "description": "A social platform, in lower case. Requests also accept `twitter` for X, and any capitalisation.",
        "enum": [
          "x",
          "linkedin",
          "bluesky",
          "threads",
          "facebook",
          "instagram",
          "tiktok",
          "youtube",
          "pinterest"
        ]
      },
      "DeliveryErrorCode": {
        "type": "string",
        "description": "Why a delivery failed, and so what to do about it:\n\n- `reconnect`: the account's access has gone. Get a link with `POST /v1/accounts/connect` and its `account_id`, give it to the person, then retry.\n- `rate_limited`: the platform asked Postatron to slow down, or a daily limit was reached. Retry later.\n- `platform_unavailable`: the platform failed or timed out. Retry.\n- `postatron`: something went wrong on Postatron's side. Retry.\n- `media_rejected`: the platform would not take the image or video. A retry sends the same file; make a new post with another.\n- `validation`: the post breaks a platform rule, such as a duplicate or a caption too long. Make a new post with the change.\n- `policy`: the platform's rules refused it, such as TikTok posting only to private accounts for an unaudited app, or there was no plan when a retry ran. `error` says what would change it.\n- `unknown`: a refusal Postatron does not recognise. `error` has the platform's words.",
        "enum": [
          "reconnect",
          "rate_limited",
          "platform_unavailable",
          "media_rejected",
          "policy",
          "validation",
          "postatron",
          "unknown"
        ]
      },
      "WebhookEventType": {
        "type": "string",
        "description": "An event a webhook can subscribe to.",
        "enum": [
          "post.published",
          "post.partial",
          "post.failed",
          "post.unpublished",
          "account.connected",
          "account.disconnected",
          "account.needs_attention"
        ]
      },
      "ErrorBody": {
        "type": "object",
        "description": "The envelope of every non-2xx answer from a `/v1` route.",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorDetail"
          }
        },
        "required": [
          "error"
        ]
      },
      "ErrorDetail": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "A stable, machine-readable reason. Programs should branch on this, never on `message`.",
            "enum": [
              "unauthorized",
              "insufficient_scope",
              "subscription_required",
              "workspace_forbidden",
              "rate_limited",
              "quota_exceeded",
              "validation_error",
              "not_found",
              "internal_error",
              "reconnect_required",
              "conflict",
              "idempotency_key_reused",
              "idempotency_key_in_use"
            ]
          },
          "message": {
            "type": "string",
            "description": "What went wrong and what to do, in a sentence for a person."
          },
          "details": {
            "type": "object",
            "description": "The fields or values involved, when there are any: for example `required_scope`, `plans_url`, `account_ids`, `profiles`, `boards`, `resource`, `limit`, `used`, `retry_after` or `reset_at`.",
            "additionalProperties": true
          }
        },
        "required": [
          "code",
          "message"
        ]
      },
      "XOptions": {
        "type": "object",
        "description": "Settings only X has.",
        "properties": {
          "content": {
            "type": "string",
            "description": "Replaces the post's text on X only. Measured against X's limit in place of `content`."
          },
          "thread": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/XThreadTweet"
            },
            "description": "Replies under the first tweet, in order: `thread[0]` is the second tweet. Each tweet counts as its own destination-post, and as an X link post if it has a link. A thread cannot go into a Community."
          },
          "community": {
            "type": "string",
            "description": "The X Community to post into: its id, or its link (`https://x.com/i/communities/<id>`)."
          },
          "share_with_followers": {
            "type": "boolean",
            "description": "Also show a Community post on followers' timelines."
          },
          "reply_settings": {
            "type": "string",
            "description": "Who can reply. Left out (or `everyone`), everyone can.",
            "enum": [
              "everyone",
              "following",
              "mentioned_users",
              "subscribers",
              "verified"
            ]
          },
          "long_post": {
            "type": "boolean",
            "description": "Allow tweets over 280 characters, up to 25,000, which X accepts from Premium accounts only."
          },
          "poll": {
            "$ref": "#/components/schemas/XPoll"
          }
        }
      },
      "XThreadTweet": {
        "type": "object",
        "description": "One reply in an X thread. Up to four media items, and X takes JPEG, PNG and GIF images here.",
        "properties": {
          "content": {
            "type": "string",
            "description": "The tweet's text."
          },
          "media_urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "Public `https` links to images or a video for this tweet, as on the post itself. When a post is read back, this holds the media the tweet carries."
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Upload ids from `POST /v1/media/uploads` for this tweet."
          }
        },
        "required": [
          "content"
        ]
      },
      "XPoll": {
        "type": "object",
        "description": "A poll on the first tweet. A poll cannot carry media.",
        "properties": {
          "options": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 25
            },
            "description": "2 to 4 different options, each up to 25 characters.",
            "minItems": 2,
            "maxItems": 4
          },
          "duration_minutes": {
            "type": "integer",
            "description": "How long the poll is open: 5 minutes to 7 days (10,080). Left out, one day.",
            "minimum": 5,
            "maximum": 10080
          }
        },
        "required": [
          "options"
        ]
      },
      "InstagramOptions": {
        "type": "object",
        "description": "Settings only Instagram has.",
        "properties": {
          "post_type": {
            "type": "string",
            "description": "What to publish. `auto` (the default) chooses from the media.",
            "enum": [
              "auto",
              "feed",
              "story",
              "reel",
              "carousel"
            ]
          },
          "caption": {
            "type": "string",
            "description": "Replaces the post's text on Instagram only, up to 2,200 characters."
          },
          "first_comment": {
            "type": "string",
            "description": "Posted as the first comment, often for hashtags. Not on stories. If Instagram refuses it, the post still publishes and the delivery carries a warning."
          },
          "collaborators": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to 3 Instagram usernames invited to co-author the post.",
            "maxItems": 3
          },
          "user_tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InstagramUserTag"
            },
            "description": "People tagged on images."
          },
          "ai_generated": {
            "type": "boolean",
            "description": "Ask Instagram to show its AI-content label."
          },
          "trial_reel": {
            "type": "string",
            "description": "Share a reel with non-followers first: `manual` (graduate it by hand) or `performance` (Instagram graduates it if it does well).",
            "enum": [
              "manual",
              "performance"
            ]
          }
        }
      },
      "InstagramUserTag": {
        "type": "object",
        "description": "A username tagged on one image.",
        "properties": {
          "username": {
            "type": "string",
            "description": "The Instagram username to tag."
          },
          "x": {
            "type": "number",
            "description": "Where across the image, from 0 (left) to 1 (right).",
            "minimum": 0,
            "maximum": 1
          },
          "y": {
            "type": "number",
            "description": "Where down the image, from 0 (top) to 1 (bottom).",
            "minimum": 0,
            "maximum": 1
          },
          "media_index": {
            "type": "integer",
            "description": "Which of the post's media the tag is on, counting from 0.",
            "minimum": 0
          }
        },
        "required": [
          "username",
          "x",
          "y"
        ]
      },
      "TikTokOptions": {
        "type": "object",
        "description": "Settings TikTok asks for. With `privacy` the post is published directly, and TikTok then needs every one of `privacy`, `disable_comment`, `commercial_content` and `music_usage_confirmed`, plus `disable_duet` and `disable_stitch` for a video, and `your_brand` or `branded_content` when `commercial_content` is true. The choices are checked against the creator's TikTok settings before the post is accepted.\n\nWith `draft: true`, or with the whole object left out, the post goes to the creator's TikTok inbox instead, and they finish it in the app. A draft takes only `title` and `description` on photos, and a video draft takes neither; the other settings are refused. A delivery to the inbox has no `url`.",
        "properties": {
          "title": {
            "type": "string",
            "description": "A video's caption, up to 2,200 characters, in place of `content`; or a photo post's title, up to 90. TikTok counts in UTF-16, so an emoji counts as two."
          },
          "description": {
            "type": "string",
            "description": "A photo post's text, up to 4,000 characters, in place of `content`."
          },
          "draft": {
            "type": "boolean",
            "description": "Send the post to the creator's TikTok inbox, to finish in the TikTok app, instead of publishing it."
          },
          "privacy": {
            "type": "string",
            "description": "Who can see the post. Giving it publishes directly, and then TikTok's other settings are required too (see the object's description). Only the levels the creator's TikTok account allows are accepted.",
            "enum": [
              "public_to_everyone",
              "mutual_follow_friends",
              "follower_of_creator",
              "self_only"
            ]
          },
          "disable_comment": {
            "type": "boolean",
            "description": "Turn comments off."
          },
          "disable_duet": {
            "type": "boolean",
            "description": "Turn Duet off (videos)."
          },
          "disable_stitch": {
            "type": "boolean",
            "description": "Turn Stitch off (videos)."
          },
          "commercial_content": {
            "type": "boolean",
            "description": "Whether the post promotes a brand, product or service. When true, one of `your_brand` or `branded_content` must be true."
          },
          "your_brand": {
            "type": "boolean",
            "description": "The post promotes the creator's own business."
          },
          "branded_content": {
            "type": "boolean",
            "description": "The post is a paid partnership. It cannot be `self_only`."
          },
          "music_usage_confirmed": {
            "type": "boolean",
            "description": "The person accepts TikTok's declaration, which TikTok requires before it publishes: \"By posting, you agree to TikTok's Music Usage Confirmation\", or with `branded_content` \"By posting, you agree to TikTok's Branded Content Policy and Music Usage Confirmation\". Show it to them; never agree for them."
          },
          "auto_add_music": {
            "type": "boolean",
            "description": "Let TikTok add recommended music to a photo post."
          },
          "ai_generated": {
            "type": "boolean",
            "description": "Ask TikTok to label the post as AI-generated."
          }
        }
      },
      "YouTubeOptions": {
        "type": "object",
        "description": "Settings only YouTube has. Leave the object out to upload a public video titled with the first line of `content`, described by `content`, in People & Blogs. A thumbnail cannot fail the post: the video is up before it is tried, so a refusal shows as the delivery's `error` with `warning: true`.",
        "properties": {
          "title": {
            "type": "string",
            "description": "Up to 100 characters, without `<` or `>`. Left out, the first line of `content`.",
            "maxLength": 100
          },
          "description": {
            "type": "string",
            "description": "Replaces `content` as the video's description: up to 5,000 bytes (not characters), without `<` or `>`. Left out, `content` is the description, so it must meet the same rules."
          },
          "privacy": {
            "type": "string",
            "description": "`public` (the default), `unlisted` or `private`.",
            "enum": [
              "public",
              "unlisted",
              "private"
            ]
          },
          "made_for_kids": {
            "type": "boolean",
            "description": "The uploader's declaration that the video is made for children, which YouTube asks of every upload. Ask the person rather than choosing. Left out, nothing is declared and the channel's default applies."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to 500 characters in all, counting a comma between tags and the quotes around a tag with a space. No `<`, `>` or commas inside a tag."
          },
          "category_id": {
            "type": "string",
            "description": "A YouTube category: `1` Film & Animation, `2` Autos & Vehicles, `10` Music, `15` Pets & Animals, `17` Sports, `19` Travel & Events, `20` Gaming, `22` People & Blogs (the default), `23` Comedy, `24` Entertainment, `25` News & Politics, `26` Howto & Style, `27` Education, `28` Science & Technology or `29` Nonprofits & Activism.",
            "enum": [
              "1",
              "2",
              "10",
              "15",
              "17",
              "19",
              "20",
              "22",
              "23",
              "24",
              "25",
              "26",
              "27",
              "28",
              "29"
            ]
          },
          "first_comment": {
            "type": "string",
            "description": "Not available yet: refused with `validation_error` until Google verifies the permission it needs."
          },
          "thumbnail_url": {
            "type": "string",
            "description": "A public `https` link to a JPEG or PNG for a custom thumbnail, which YouTube allows only on verified channels. When a post is read back, this holds the thumbnail's address, and sending that address back keeps it.",
            "format": "uri"
          },
          "thumbnail_id": {
            "type": "string",
            "description": "An upload id from `POST /v1/media/uploads` for the thumbnail, in place of `thumbnail_url`."
          }
        }
      },
      "PinterestOptions": {
        "type": "object",
        "description": "Settings a Pin has. Every Pin is saved to one of the account's boards: the one named here, or else the account's default board (`default_board_id` in `GET /v1/accounts`). An unknown board, or a Pinterest account with no board named and no default, is refused with `422 validation_error`, and `details.boards` lists the account's board names, so ask the person which one they mean. The account must be a Pinterest business account (a free one will do).",
        "properties": {
          "board": {
            "type": "string",
            "description": "The board to save the Pin to, by id or by name (ignoring case), for every Pinterest account in the post. Left out, each account's default board."
          },
          "boards": {
            "type": "object",
            "description": "For a post to several Pinterest accounts: each account id (from `GET /v1/accounts`) to its board, by id or name. Wins over `board` for the accounts it names. When a post is read back, this holds each account's board id, and `board` too when there is one Pinterest account.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "title": {
            "type": "string",
            "description": "The Pin's title, up to 100 characters. Left out, the first line of `content`, cut to 100.",
            "maxLength": 100
          },
          "description": {
            "type": "string",
            "description": "Replaces `content` as the Pin's description, up to 800 characters. Left out, `content` is the description, so it must fit in 800.",
            "maxLength": 800
          },
          "link": {
            "type": "string",
            "description": "Where the Pin leads when someone taps it: a full `http` or `https` address, up to 2,048 characters.",
            "format": "uri",
            "maxLength": 2048
          }
        }
      },
      "CreatePostRequest": {
        "type": "object",
        "description": "A new post. Instagram and TikTok need an image or a video, YouTube a video, and Pinterest an image (two to four make a carousel) or one video. Each platform options object needs an account on its platform among the post's accounts.",
        "properties": {
          "content": {
            "type": "string",
            "description": "The post's text, required. Each platform has its own limit, judged on the text it receives (`x.content`, `instagram.caption`, `tiktok.title` on a video, `tiktok.description` on photos and `pinterest.description` count in place of `content`): X 280 (25,000 with `x.long_post` or an X Premium account), Bluesky 300, Threads 500, LinkedIn 3,000, Facebook 63,206, Instagram 2,200, TikTok 2,200 for a video and 4,000 for photos (counted in UTF-16), Pinterest 800. Links in it count towards `link_posts`, and on X towards `x_link_posts`.",
            "minLength": 1,
            "maxLength": 63206
          },
          "account_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Connected accounts to post to, by `id` from `GET /v1/accounts`. Give this or `platforms`, not both."
          },
          "platforms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Platform"
            },
            "description": "Platforms to post to: the account on each platform in `profile`. Without `profile`, the account on each platform across every profile, which is only allowed while each platform named has one account; otherwise the request is refused with `422` and `details.profiles` lists the candidates. Give this or `account_ids`, not both."
          },
          "profile": {
            "type": "string",
            "description": "A profile id or name (ignoring case) from `GET /v1/profiles`. Narrows `platforms` to that profile's accounts; with `account_ids`, every account must be in it."
          },
          "media_urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "Public `https` links to images or one video, which the server downloads. Addresses inside a private network are refused. Up to four media items across `media_urls` and `media_ids`, and a post cannot mix images and video."
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Upload ids from `POST /v1/media/uploads` whose status is `READY`, for files on a person's own device."
          },
          "scheduled_at": {
            "type": "string",
            "description": "When to publish, at least 5 minutes ahead. Left out (and no `queue`), the post is published now.",
            "format": "date-time"
          },
          "queue": {
            "type": "string",
            "description": "A queue's name or id from `GET /v1/queues`: the post takes that queue's next free slot instead of `scheduled_at`, which cannot be given with it. Every account must be in the queue's profile, and without `profile`, `platforms` means that profile's accounts."
          },
          "x": {
            "$ref": "#/components/schemas/XOptions"
          },
          "instagram": {
            "$ref": "#/components/schemas/InstagramOptions"
          },
          "tiktok": {
            "$ref": "#/components/schemas/TikTokOptions"
          },
          "youtube": {
            "$ref": "#/components/schemas/YouTubeOptions"
          },
          "pinterest": {
            "$ref": "#/components/schemas/PinterestOptions"
          }
        },
        "required": [
          "content"
        ]
      },
      "UpdatePostRequest": {
        "type": "object",
        "description": "Changes to a draft or a scheduled post. Every field is optional; what is left out keeps its value.",
        "properties": {
          "content": {
            "type": "string",
            "description": "Replaces the post's text, checked against every account's limit as on create.",
            "minLength": 1,
            "maxLength": 63206
          },
          "scheduled_at": {
            "type": "string",
            "description": "Moves the post, or schedules a draft, at least 5 minutes ahead. Cannot be combined with `publish_now`.",
            "format": "date-time"
          },
          "publish_now": {
            "type": "boolean",
            "description": "Publish straight away, after the other changes in the same request, for a draft or a scheduled post. The post comes back `processing`."
          },
          "add_platforms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Platform"
            },
            "description": "Adds the account on each platform, chosen the way `platforms` chooses them on create and narrowed by `profile`."
          },
          "add_account_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Adds specific connected accounts."
          },
          "remove_account_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Takes accounts off the post. At least one must remain unless the post stays a draft."
          },
          "profile": {
            "type": "string",
            "description": "A profile id or name: which brand `add_platforms` means."
          },
          "x": {
            "$ref": "#/components/schemas/XOptions",
            "description": "Replaces this platform's options as a whole; `{}` clears them."
          },
          "instagram": {
            "$ref": "#/components/schemas/InstagramOptions",
            "description": "Replaces this platform's options as a whole; `{}` clears them."
          },
          "tiktok": {
            "$ref": "#/components/schemas/TikTokOptions",
            "description": "Replaces this platform's options as a whole; `{}` clears them."
          },
          "youtube": {
            "$ref": "#/components/schemas/YouTubeOptions",
            "description": "Replaces this platform's options as a whole; `{}` clears them."
          },
          "pinterest": {
            "$ref": "#/components/schemas/PinterestOptions",
            "description": "Replaces this platform's options as a whole; `{}` clears them."
          }
        }
      },
      "RetryPostRequest": {
        "type": "object",
        "description": "Which failed deliveries to send again.",
        "properties": {
          "account_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The failed accounts to send again. Left out, every account that failed goes again."
          }
        }
      },
      "DeletePostsRequest": {
        "type": "object",
        "properties": {
          "ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to 100 post ids.",
            "minItems": 1,
            "maxItems": 100
          }
        },
        "required": [
          "ids"
        ]
      },
      "Delivery": {
        "type": "object",
        "description": "A post's state on one account.",
        "properties": {
          "account_id": {
            "type": "string",
            "description": "The connected account's id."
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "username": {
            "type": "string",
            "description": "The account's handle on the platform."
          },
          "status": {
            "type": "string",
            "description": "`draft` or `scheduled` while the post is (an account is not attempted until the post goes out), then `pending`, `processing`, `published` or `failed`.",
            "enum": [
              "draft",
              "scheduled",
              "pending",
              "processing",
              "published",
              "failed"
            ]
          },
          "url": {
            "type": "string",
            "description": "The post on the platform, once published. None for a TikTok inbox draft.",
            "format": "uri"
          },
          "error": {
            "type": "string",
            "description": "Why it failed, in words for the person. On a `published` delivery, what did not go with it, such as an Instagram first comment or a YouTube thumbnail, with `warning` true."
          },
          "error_code": {
            "$ref": "#/components/schemas/DeliveryErrorCode",
            "description": "On a `failed` delivery, what kind of failure it was, so a program can act without reading `error`."
          },
          "warning": {
            "type": "boolean",
            "description": "True on a `published` delivery that carries an `error`: the post is live, and the error is about something after it."
          }
        },
        "required": [
          "account_id",
          "platform",
          "username",
          "status"
        ]
      },
      "PostAuthor": {
        "type": "object",
        "description": "A person on the account.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The person's Postatron user id."
          },
          "name": {
            "type": "string",
            "description": "Their name; empty once they have left the team or deleted their account."
          }
        },
        "required": [
          "id"
        ]
      },
      "PostQueue": {
        "type": "object",
        "description": "The queue that chose a post's time.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The queue's id."
          },
          "name": {
            "type": "string",
            "description": "Its name; empty once the queue has been deleted."
          }
        },
        "required": [
          "id"
        ]
      },
      "PostUnpublished": {
        "type": "object",
        "description": "Set on a draft that came due and went back to the drafts instead of out. Nothing is lost: schedule or publish it again with `PATCH /v1/posts/{post_id}`, which clears this.",
        "properties": {
          "reason": {
            "type": "string",
            "description": "Why it went back to the drafts. `no_plan`: the account had no plan when the post was due.",
            "enum": [
              "no_plan"
            ]
          },
          "due_at": {
            "type": "string",
            "description": "When it was due.",
            "format": "date-time"
          }
        },
        "required": [
          "reason",
          "due_at"
        ]
      },
      "Post": {
        "type": "object",
        "description": "A post and its state on every account it goes to.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The post's id."
          },
          "status": {
            "type": "string",
            "description": "`draft` (saved without a time), `scheduled`, `pending` (about to publish), `processing` (publishing), `published`, `failed` (on every account) or `partial` (published on some accounts, failed on others).",
            "enum": [
              "draft",
              "scheduled",
              "pending",
              "processing",
              "published",
              "failed",
              "partial"
            ]
          },
          "content": {
            "type": "string",
            "description": "The post's text."
          },
          "media_type": {
            "type": "string",
            "description": "What the post carries.",
            "enum": [
              "text",
              "image",
              "video"
            ]
          },
          "contains_link": {
            "type": "boolean",
            "description": "Whether the text contains a link: `http(s)://` addresses, `www.` hosts and bare domains with a common top-level domain."
          },
          "scheduled_at": {
            "type": "string",
            "description": "When it is, or was, due. Left out when it has no time, such as a post published at once.",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "description": "When it was made.",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "description": "When it was last changed, if it has been.",
            "format": "date-time"
          },
          "deliveries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Delivery"
            },
            "description": "One entry per account the post goes to."
          },
          "source": {
            "type": "string",
            "description": "Where it was made. Left out on posts from before this was recorded.",
            "enum": [
              "dashboard",
              "api",
              "mcp"
            ]
          },
          "created_by": {
            "$ref": "#/components/schemas/PostAuthor",
            "description": "The person who made it."
          },
          "updated_by": {
            "$ref": "#/components/schemas/PostAuthor",
            "description": "Whoever last changed it, when someone has. In a team workspace they can differ from `created_by`."
          },
          "queue": {
            "$ref": "#/components/schemas/PostQueue",
            "description": "The queue that chose its time, when one did."
          },
          "unpublished": {
            "$ref": "#/components/schemas/PostUnpublished"
          },
          "x": {
            "$ref": "#/components/schemas/XOptions",
            "description": "The options it was saved with, when it has any."
          },
          "instagram": {
            "$ref": "#/components/schemas/InstagramOptions",
            "description": "The options it was saved with, when it has any."
          },
          "tiktok": {
            "$ref": "#/components/schemas/TikTokOptions",
            "description": "The options it was saved with, when it has any."
          },
          "youtube": {
            "$ref": "#/components/schemas/YouTubeOptions",
            "description": "The options it was saved with, when it has any."
          },
          "pinterest": {
            "$ref": "#/components/schemas/PinterestOptions",
            "description": "The options it was saved with, when it has any."
          }
        },
        "required": [
          "id",
          "status",
          "content",
          "media_type",
          "contains_link",
          "created_at",
          "deliveries"
        ]
      },
      "PostList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Post"
            },
            "description": "Posts, newest first: by `scheduled_at` where they have one, otherwise by `created_at`."
          },
          "next_cursor": {
            "type": "string",
            "description": "Pass as `cursor` for the next page. Left out on the last page."
          }
        },
        "required": [
          "data"
        ]
      },
      "DeletePostResponse": {
        "type": "object",
        "description": "What deleting a post did. A post being published, or due within two minutes, is not deleted: `deleted` is false and `message` says why.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The post's id."
          },
          "status": {
            "type": "string",
            "description": "The post's status when it was deleted, or `cancelled` for a scheduled post that will now never go out.",
            "enum": [
              "draft",
              "scheduled",
              "pending",
              "processing",
              "published",
              "failed",
              "partial",
              "cancelled"
            ]
          },
          "deleted": {
            "type": "boolean",
            "description": "True when the post is gone from Postatron."
          },
          "cancelled": {
            "type": "boolean",
            "description": "True when a scheduled post was deleted, so it will never go out."
          },
          "message": {
            "type": "string",
            "description": "For a published post, where it is still live; for a refusal, why it was not deleted."
          }
        },
        "required": [
          "id",
          "status",
          "deleted",
          "cancelled"
        ]
      },
      "DeletePostsResponse": {
        "type": "object",
        "description": "Every id asked for is in exactly one of `deleted` and `failed`.",
        "properties": {
          "deleted": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The ids deleted, in the order asked."
          },
          "failed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeletePostsError"
            },
            "description": "The ids not deleted, and why, in the order asked."
          },
          "message": {
            "type": "string",
            "description": "Says how many deleted posts had been published, and so are still live on the platforms."
          }
        },
        "required": [
          "deleted",
          "failed"
        ]
      },
      "DeletePostsError": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The post's id."
          },
          "error": {
            "type": "string",
            "description": "Why it was not deleted."
          }
        },
        "required": [
          "id",
          "error"
        ]
      },
      "Account": {
        "type": "object",
        "description": "A connected social account. Every account is in exactly one profile, and a profile holds at most one account per platform.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The account's id: use it in `account_ids`."
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "username": {
            "type": "string",
            "description": "The account's handle."
          },
          "display_name": {
            "type": "string",
            "description": "The account's name on the platform."
          },
          "status": {
            "type": "string",
            "description": "`connected`, or `reconnect_required`: no sign-in stored, one the platform refused, or one expired with nothing to renew it. A post to a `reconnect_required` account is refused; `POST /v1/accounts/connect` with its `account_id` gives the link that fixes it. In webhook events, `disconnected` for an account that has been removed.",
            "enum": [
              "connected",
              "reconnect_required",
              "disconnected"
            ]
          },
          "profile_id": {
            "type": "string",
            "description": "The profile it is in."
          },
          "profile_name": {
            "type": "string",
            "description": "That profile's name."
          },
          "connected_at": {
            "type": "string",
            "description": "When it was connected.",
            "format": "date-time"
          },
          "default_board_id": {
            "type": "string",
            "description": "A Pinterest account's default board: where its Pins go when a post names none. Left out when it has none."
          }
        },
        "required": [
          "id",
          "platform",
          "username",
          "display_name",
          "status",
          "profile_id",
          "profile_name",
          "connected_at"
        ]
      },
      "AccountList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Account"
            },
            "description": "Accounts sorted by platform, then username."
          }
        },
        "required": [
          "data"
        ]
      },
      "ConnectAccountRequest": {
        "type": "object",
        "description": "Either `platform` to connect an account, or `account_id` to sign one in again. Asking for a `platform` whose account in `profile` needs reconnecting also signs that one in again.",
        "properties": {
          "platform": {
            "type": "string",
            "description": "The platform to connect: `x` (or `twitter`), `instagram`, `facebook`, `linkedin`, `tiktok`, `youtube`, `threads` or `bluesky`, and, while Postatron has them switched on, `linkedin_page` (a LinkedIn company Page) and `pinterest` (a Pinterest business account). An unknown one is refused, and the message lists what this server offers. May be left out with `account_id`."
          },
          "profile": {
            "type": "string",
            "description": "The profile (id or name) the new account goes into. Left out, Default."
          },
          "account_id": {
            "type": "string",
            "description": "Sign this connected account in again instead, for one whose access has gone (`status: reconnect_required`, or a delivery with `error_code: reconnect`). It keeps its profile and does not count against the plan's allowance."
          }
        }
      },
      "ConnectLink": {
        "type": "object",
        "description": "A link the person opens to connect an account. Nothing is connected until they finish the platform's sign-in.",
        "properties": {
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "connect_url": {
            "type": "string",
            "description": "The link to give the person. It opens Postatron and starts the platform's sign-in, and only works for the person whose key asked for it.",
            "format": "uri"
          },
          "profile": {
            "type": "string",
            "description": "The name of the profile the account goes into."
          },
          "hint": {
            "type": "string",
            "description": "What to tell the person, in a sentence."
          }
        },
        "required": [
          "platform",
          "connect_url",
          "hint"
        ]
      },
      "Profile": {
        "type": "object",
        "description": "A group of connected accounts, usually one brand or client.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The profile's id. Default's is `default`."
          },
          "name": {
            "type": "string",
            "description": "Its name."
          },
          "is_default": {
            "type": "boolean",
            "description": "True for the Default profile."
          },
          "accounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Account"
            },
            "description": "Its connected accounts, sorted by platform."
          }
        },
        "required": [
          "id",
          "name",
          "is_default",
          "accounts"
        ]
      },
      "ProfileList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Profile"
            },
            "description": "Default first, then the rest oldest first."
          }
        },
        "required": [
          "data"
        ]
      },
      "QueueSlot": {
        "type": "object",
        "description": "A weekly posting time.",
        "properties": {
          "day": {
            "type": "integer",
            "description": "0 for Sunday to 6 for Saturday.",
            "minimum": 0,
            "maximum": 6
          },
          "time": {
            "type": "string",
            "description": "`HH:MM` on a 24-hour clock, in the queue's timezone.",
            "pattern": "^[0-2][0-9]:[0-5][0-9]$",
            "examples": [
              "09:30"
            ]
          }
        },
        "required": [
          "day",
          "time"
        ]
      },
      "Queue": {
        "type": "object",
        "description": "A weekly posting schedule for one profile. A post added to it goes out in its next free slot, at least five minutes ahead and not on a slot another scheduled post of the profile already has.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The queue's id."
          },
          "name": {
            "type": "string",
            "description": "Its name."
          },
          "profile_id": {
            "type": "string",
            "description": "The profile it posts for."
          },
          "profile_name": {
            "type": "string",
            "description": "That profile's name."
          },
          "timezone": {
            "type": "string",
            "description": "An IANA timezone such as `Europe/London`. Slot times are wall-clock times there, across daylight saving."
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QueueSlot"
            },
            "description": "Its weekly slots, sorted by day and time."
          },
          "active": {
            "type": "boolean",
            "description": "False while the queue is paused and takes no new posts."
          },
          "next_slot": {
            "type": "string",
            "description": "When a post added now would go out. Left out when none would, with `next_slot_error` saying why.",
            "format": "date-time"
          },
          "next_slot_error": {
            "type": "string",
            "description": "Why no post would be placed: the queue is paused, has no slots, is full for the next year, or is past the plan's number of queues."
          },
          "queued": {
            "type": "integer",
            "description": "How many of its posts are still to go out."
          },
          "rescheduled": {
            "$ref": "#/components/schemas/QueueRescheduled"
          },
          "reschedule_error": {
            "type": "string",
            "description": "On an update with `move_posts`, why the posts did not move. The change to the queue was saved."
          }
        },
        "required": [
          "id",
          "name",
          "profile_id",
          "profile_name",
          "timezone",
          "slots",
          "active",
          "queued"
        ]
      },
      "QueueRescheduled": {
        "type": "object",
        "description": "What moving a queue's posts onto its new slots did. Only on the answer to an update with `move_posts` that changed the slots or timezone.",
        "properties": {
          "moved": {
            "type": "integer",
            "description": "How many posts were given a new time."
          },
          "kept": {
            "type": "integer",
            "description": "How many kept theirs: ones given a time of their own, due within five minutes, or changed while they were being moved."
          }
        },
        "required": [
          "moved",
          "kept"
        ]
      },
      "QueueList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Queue"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "CreateQueueRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Up to 60 characters.",
            "minLength": 1,
            "maxLength": 60
          },
          "profile": {
            "type": "string",
            "description": "The profile (id or name) the queue posts for. Left out, Default."
          },
          "timezone": {
            "type": "string",
            "description": "An IANA timezone such as `Europe/London`."
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QueueSlot"
            },
            "description": "Up to 168 weekly slots; sorted, and duplicates dropped.",
            "maxItems": 168
          },
          "paused": {
            "type": "boolean",
            "description": "Create the queue paused, taking no posts until resumed."
          }
        },
        "required": [
          "name",
          "timezone"
        ]
      },
      "UpdateQueueRequest": {
        "type": "object",
        "description": "Changes to a queue. What is left out keeps its value. Posts the queue already placed keep their times unless `move_posts` is set, and pausing never moves them.",
        "properties": {
          "name": {
            "type": "string",
            "description": "A new name, up to 60 characters.",
            "minLength": 1,
            "maxLength": 60
          },
          "profile": {
            "type": "string",
            "description": "Move the queue to another profile (id or name)."
          },
          "timezone": {
            "type": "string",
            "description": "A new IANA timezone."
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QueueSlot"
            },
            "description": "Replaces every slot.",
            "maxItems": 168
          },
          "active": {
            "type": "boolean",
            "description": "`false` pauses the queue; `true` resumes it."
          },
          "move_posts": {
            "type": "boolean",
            "description": "When the slots or timezone change, give the queue's upcoming posts the new slots, soonest first, in the order they were going out. Posts given a time of their own, and any due within five minutes, keep their times."
          }
        }
      },
      "DeleteQueueResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The queue's id."
          },
          "deleted": {
            "type": "boolean",
            "description": "True."
          },
          "message": {
            "type": "string",
            "description": "Says that posts it placed stay scheduled."
          }
        },
        "required": [
          "id",
          "deleted"
        ]
      },
      "UsageReport": {
        "type": "object",
        "description": "This month's quota and what has been used. The dashboard shows the same counters.",
        "properties": {
          "period": {
            "$ref": "#/components/schemas/UsagePeriod"
          },
          "plan": {
            "$ref": "#/components/schemas/UsagePlan"
          },
          "destination_posts": {
            "$ref": "#/components/schemas/UsageCounter",
            "description": "Destination-posts this month: one post delivered to one account."
          },
          "link_posts": {
            "$ref": "#/components/schemas/UsageLinkCounter"
          },
          "x_link_posts": {
            "$ref": "#/components/schemas/UsageCounter",
            "description": "X posts with a link this month, which have their own cap."
          },
          "by_platform": {
            "type": "object",
            "description": "Destination-posts this month on each platform.",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "rate_limits": {
            "$ref": "#/components/schemas/UsageRateLimits"
          }
        },
        "required": [
          "period",
          "plan",
          "destination_posts",
          "link_posts",
          "x_link_posts",
          "by_platform",
          "rate_limits"
        ]
      },
      "UsagePeriod": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "description": "The first moment of the calendar month (UTC).",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "description": "The first moment of the next month, when the counters reset.",
            "format": "date-time"
          }
        },
        "required": [
          "start",
          "end"
        ]
      },
      "UsagePlan": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "The plan, such as `STARTER`, `GROWTH` or `SCALE`."
          },
          "name": {
            "type": "string",
            "description": "Its name."
          },
          "interval": {
            "type": "string",
            "description": "How it is billed, such as `month` or `year`."
          },
          "status": {
            "type": "string",
            "description": "The subscription's status, such as `ACTIVE`."
          },
          "connected_accounts": {
            "type": "integer",
            "description": "Accounts connected now."
          },
          "max_connected_accounts": {
            "type": "integer",
            "description": "Accounts the plan allows."
          }
        },
        "required": [
          "code",
          "name",
          "interval",
          "status",
          "connected_accounts",
          "max_connected_accounts"
        ]
      },
      "UsageCounter": {
        "type": "object",
        "description": "A capped monthly counter.",
        "properties": {
          "used": {
            "type": "integer"
          },
          "limit": {
            "type": "integer",
            "description": "What the plan includes each month, and the hard stop: there is no overage on any plan."
          },
          "remaining": {
            "type": "integer"
          }
        },
        "required": [
          "used",
          "limit",
          "remaining"
        ]
      },
      "UsageLinkCounter": {
        "type": "object",
        "description": "Destination-posts with a link this month, on every platform. Not capped.",
        "properties": {
          "used": {
            "type": "integer"
          }
        },
        "required": [
          "used"
        ]
      },
      "UsageRateLimits": {
        "type": "object",
        "description": "The per-key rate limits in force.",
        "properties": {
          "writes_per_minute": {
            "type": "integer"
          },
          "reads_per_minute": {
            "type": "integer"
          },
          "uploads_per_minute": {
            "type": "integer"
          },
          "writes_per_hour": {
            "type": "integer",
            "description": "`writes_per_minute` times 60, kept for older clients.",
            "deprecated": true
          }
        },
        "required": [
          "writes_per_minute",
          "reads_per_minute",
          "uploads_per_minute",
          "writes_per_hour"
        ]
      },
      "AnalyticsReport": {
        "type": "object",
        "description": "The numbers behind the dashboard's Analytics page. A metric a platform does not report is `null`, never a negative number. X and Pinterest are not included.",
        "properties": {
          "range": {
            "$ref": "#/components/schemas/AnalyticsRange"
          },
          "summary": {
            "$ref": "#/components/schemas/AnalyticsSummary"
          },
          "per_platform": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsPlatformRow"
            },
            "description": "Totals for each platform with posts in the window."
          },
          "top_posts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsPost"
            },
            "description": "At most ten of the best performing posts. YouTube's are left out, as its policies ask."
          },
          "accounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsAccount"
            },
            "description": "Whether each account's numbers could be read."
          },
          "generated_at": {
            "type": "string",
            "description": "When the report was put together.",
            "format": "date-time"
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Things to know when reading it, such as X being left out."
          },
          "last_sync_at": {
            "type": "string",
            "description": "When the oldest numbers in the report were read from a platform.",
            "format": "date-time"
          },
          "next_sync_at": {
            "type": "string",
            "description": "When they will next be read. Each account is read at most once an hour, so asking sooner returns the same numbers.",
            "format": "date-time"
          }
        },
        "required": [
          "range",
          "summary",
          "per_platform",
          "top_posts",
          "accounts",
          "generated_at"
        ]
      },
      "AnalyticsRange": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "The range asked for.",
            "enum": [
              "7d",
              "30d",
              "90d",
              "365d"
            ]
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "key",
          "start",
          "end"
        ]
      },
      "AnalyticsSummary": {
        "type": "object",
        "description": "The headline numbers. YouTube's are left out of engagements and the rate, as its API policies ask.",
        "properties": {
          "engagement_rate_percent": {
            "type": "number",
            "description": "Engagements as a percentage of `engagement_rate_basis`."
          },
          "engagement_rate_basis": {
            "type": "string",
            "description": "What the rate is measured against, such as `reach`."
          },
          "engagements": {
            "type": "integer"
          },
          "reach": {
            "type": "integer"
          },
          "followers": {
            "type": "integer"
          },
          "posts_this_period": {
            "type": "integer"
          }
        },
        "required": [
          "engagement_rate_percent",
          "engagement_rate_basis",
          "engagements",
          "reach",
          "followers",
          "posts_this_period"
        ]
      },
      "AnalyticsPlatformRow": {
        "type": "object",
        "properties": {
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "posts": {
            "type": "integer"
          },
          "likes": {
            "type": "integer"
          },
          "comments": {
            "type": "integer"
          },
          "shares": {
            "type": "integer"
          },
          "views": {
            "type": "integer"
          }
        },
        "required": [
          "platform",
          "posts",
          "likes",
          "comments",
          "shares",
          "views"
        ]
      },
      "AnalyticsPost": {
        "type": "object",
        "description": "One of the best performing posts in the window.",
        "properties": {
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "username": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "text": {
            "type": "string",
            "description": "The start of the post's text, up to 280 characters."
          },
          "published_at": {
            "type": "string",
            "format": "date-time"
          },
          "likes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when the platform does not report it."
          },
          "comments": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when the platform does not report it."
          },
          "shares": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when the platform does not report it."
          },
          "views": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when the platform does not report it."
          },
          "reach": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when the platform does not report it."
          },
          "saves": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when the platform does not report it."
          },
          "impressions": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when the platform does not report it."
          },
          "clicks": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when the platform does not report it."
          },
          "follows": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when the platform does not report it."
          },
          "engagement": {
            "type": "integer",
            "description": "The post's engagements."
          },
          "posted_with_postatron": {
            "type": "boolean",
            "description": "Whether the post was made with Postatron."
          }
        },
        "required": [
          "platform",
          "username",
          "url",
          "text",
          "published_at",
          "likes",
          "comments",
          "shares",
          "views",
          "reach",
          "saves",
          "impressions",
          "clicks",
          "follows",
          "engagement",
          "posted_with_postatron"
        ]
      },
      "AnalyticsAccount": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "username": {
            "type": "string"
          },
          "followers": {
            "type": "integer"
          },
          "supported": {
            "type": "boolean",
            "description": "Whether Postatron reads analytics for this platform."
          },
          "status": {
            "type": "string",
            "description": "Whether its numbers could be read.",
            "enum": [
              "ok",
              "unsupported",
              "reconnect_required",
              "unavailable",
              "error"
            ]
          },
          "reason": {
            "type": "string",
            "description": "Why not, when they could not."
          }
        },
        "required": [
          "id",
          "platform",
          "username",
          "followers",
          "supported",
          "status"
        ]
      },
      "CreateUploadRequest": {
        "type": "object",
        "properties": {
          "purpose": {
            "type": "string",
            "description": "Shown to the person on the upload page, such as \"the launch video\". Cut to 140 characters."
          }
        }
      },
      "Upload": {
        "type": "object",
        "description": "A slot a person drops a file into from the dashboard, for an agent that cannot send the bytes itself.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The upload's id: pass it in `media_ids` once `status` is `READY`."
          },
          "status": {
            "type": "string",
            "description": "`PENDING` until the file arrives, then `READY`.",
            "enum": [
              "PENDING",
              "READY"
            ]
          },
          "upload_url": {
            "type": "string",
            "description": "The link the person opens, signed in to Postatron, to choose the file. It carries no credential. Left out once the file has arrived.",
            "format": "uri"
          },
          "filename": {
            "type": "string",
            "description": "The file's name, once it has arrived."
          },
          "expires_at": {
            "type": "string",
            "description": "When the link stops working: 30 minutes after it was made.",
            "format": "date-time"
          },
          "hint": {
            "type": "string",
            "description": "What to do next, in a sentence."
          }
        },
        "required": [
          "id",
          "status",
          "expires_at"
        ]
      },
      "Event": {
        "type": "object",
        "description": "One thing that happened in the account. It is the dashboard's Logs page.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The event's id."
          },
          "at": {
            "type": "string",
            "description": "When it happened.",
            "format": "date-time"
          },
          "kind": {
            "type": "string",
            "description": "What happened: a post's outcome (`post.published`, `post.partial`, `post.failed`, `post.unpublished` for one that came due without a plan), an account change (`account.connected`, `account.disconnected`, `account.needs_attention`), or `api.request`, a call to the API or MCP that changed something or was refused.",
            "enum": [
              "post.published",
              "post.partial",
              "post.failed",
              "post.unpublished",
              "account.connected",
              "account.disconnected",
              "account.needs_attention",
              "api.request"
            ]
          },
          "status": {
            "type": "string",
            "description": "How it went.",
            "enum": [
              "ok",
              "warning",
              "error"
            ]
          },
          "source": {
            "type": "string",
            "description": "Where a post or request came from. Left out for what Postatron did by itself.",
            "enum": [
              "dashboard",
              "api",
              "mcp"
            ]
          },
          "actor_id": {
            "type": "string",
            "description": "The person behind it: the owner, or a team member."
          },
          "profile_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The profiles it concerns."
          },
          "post_id": {
            "type": "string",
            "description": "The post it concerns."
          },
          "excerpt": {
            "type": "string",
            "description": "The start of the post's text."
          },
          "message": {
            "type": "string",
            "description": "What happened, in a sentence."
          },
          "accounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventAccount"
            },
            "description": "The accounts it concerns; on a post's outcome, one per delivery."
          },
          "request": {
            "$ref": "#/components/schemas/EventRequest"
          },
          "reason": {
            "type": "string",
            "description": "Why an account was disconnected: `user` (in the dashboard), `expired` (the platform refused to renew its sign-in) or `revoked` (access withdrawn on the platform).",
            "enum": [
              "user",
              "expired",
              "revoked"
            ]
          },
          "at_risk": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventPostRef"
            },
            "description": "Scheduled posts an account problem will stop going out, soonest first and at most ten."
          },
          "at_risk_count": {
            "type": "integer",
            "description": "How many scheduled posts are at risk in all."
          }
        },
        "required": [
          "id",
          "at",
          "kind",
          "status"
        ]
      },
      "EventAccount": {
        "type": "object",
        "description": "An account an event concerns.",
        "properties": {
          "id": {
            "type": "string"
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "username": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "On a post's outcome, how this delivery went.",
            "enum": [
              "ok",
              "warning",
              "error"
            ]
          },
          "message": {
            "type": "string",
            "description": "What happened on this account."
          },
          "url": {
            "type": "string",
            "description": "The post on the platform, once published.",
            "format": "uri"
          },
          "error_code": {
            "$ref": "#/components/schemas/DeliveryErrorCode",
            "description": "A failed delivery's code, as on `Delivery`."
          }
        },
        "required": [
          "id",
          "platform"
        ]
      },
      "EventRequest": {
        "type": "object",
        "description": "The call behind an `api.request` event.",
        "properties": {
          "method": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "status_code": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "description": "The API's error code on a refusal."
          },
          "duration_ms": {
            "type": "integer"
          },
          "tool": {
            "type": "string",
            "description": "The MCP tool that made the call."
          },
          "key_name": {
            "type": "string",
            "description": "The API key or connected app that called."
          }
        },
        "required": [
          "method",
          "path",
          "status_code",
          "duration_ms"
        ]
      },
      "EventPostRef": {
        "type": "object",
        "description": "A scheduled post an account problem puts at risk.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The post's id."
          },
          "at": {
            "type": "string",
            "description": "When it is due.",
            "format": "date-time"
          },
          "excerpt": {
            "type": "string",
            "description": "The start of its text."
          }
        },
        "required": [
          "id",
          "at"
        ]
      },
      "EventList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            },
            "description": "Newest first."
          },
          "next_cursor": {
            "type": "string",
            "description": "Pass as `cursor` for the next page. Left out on the last page."
          }
        },
        "required": [
          "data"
        ]
      },
      "Webhook": {
        "type": "object",
        "description": "An endpoint Postatron sends the account's events to, signed with its secret.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The webhook's id, `wh_` and 24 hex characters."
          },
          "url": {
            "type": "string",
            "description": "Where events are POSTed.",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/WebhookEventType"
                },
                {
                  "type": "string",
                  "enum": [
                    "*"
                  ]
                }
              ]
            },
            "description": "The events it receives, or `[\"*\"]` for every event, including ones added later."
          },
          "enabled": {
            "type": "boolean",
            "description": "False while it is switched off and sent nothing."
          },
          "disabled_reason": {
            "type": "string",
            "description": "Why Postatron switched it off: it does so after 20 deliveries in a row fail every attempt. Switch it back on with `PATCH` and `enabled: true`."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_attempt": {
            "$ref": "#/components/schemas/WebhookAttempt"
          },
          "consecutive_failures": {
            "type": "integer",
            "description": "Deliveries in a row that failed every attempt; one that succeeds resets it."
          },
          "secret": {
            "type": "string",
            "description": "The signing secret: `whsec_` and 43 URL-safe base64 characters, used whole as the HMAC key. Only in the answers to creating a webhook and rotating its secret, and never shown again."
          }
        },
        "required": [
          "id",
          "url",
          "events",
          "enabled",
          "created_at",
          "consecutive_failures"
        ]
      },
      "WebhookAttempt": {
        "type": "object",
        "description": "The latest attempt to deliver to a webhook, a test or a real event.",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "event": {
            "type": "string",
            "description": "The type of the event it carried."
          },
          "succeeded": {
            "type": "boolean",
            "description": "True when the endpoint answered with a 2xx status."
          },
          "status_code": {
            "type": "integer",
            "description": "The endpoint's answer; left out when it never answered."
          },
          "error": {
            "type": "string",
            "description": "Why it failed."
          },
          "duration_ms": {
            "type": "integer"
          }
        },
        "required": [
          "at",
          "succeeded",
          "duration_ms"
        ]
      },
      "WebhookList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Webhook"
            },
            "description": "Oldest first, without their secrets."
          }
        },
        "required": [
          "data"
        ]
      },
      "CreateWebhookRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "An `https` address on port 443 or another port given in it, with no user name, password or `#fragment`, up to 2,048 characters, whose host resolves only to public addresses.",
            "format": "uri",
            "maxLength": 2048
          },
          "events": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/WebhookEventType"
                },
                {
                  "type": "string",
                  "enum": [
                    "*"
                  ]
                }
              ]
            },
            "description": "The events it receives. Left out, every event."
          }
        },
        "required": [
          "url"
        ]
      },
      "UpdateWebhookRequest": {
        "type": "object",
        "description": "Changes to a webhook. What is left out keeps its value; at least one field is needed.",
        "properties": {
          "url": {
            "type": "string",
            "description": "A new address, checked like a first one.",
            "format": "uri",
            "maxLength": 2048
          },
          "events": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/WebhookEventType"
                },
                {
                  "type": "string",
                  "enum": [
                    "*"
                  ]
                }
              ]
            },
            "description": "Replaces the events it receives; at least one, or `[\"*\"]`."
          },
          "enabled": {
            "type": "boolean",
            "description": "Switch it off or on. Switching it on clears its failure count."
          }
        }
      },
      "DeleteWebhookResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "deleted": {
            "type": "boolean",
            "description": "True."
          }
        },
        "required": [
          "id",
          "deleted"
        ]
      },
      "WebhookTestResult": {
        "type": "object",
        "description": "What the endpoint said to a `webhook.test` event sent there and then. A failed test is shown on the webhook but not counted towards switching it off.",
        "properties": {
          "delivery_id": {
            "type": "string",
            "description": "The test delivery's id, as sent in `Postatron-Delivery`."
          },
          "delivered": {
            "type": "boolean",
            "description": "True when the endpoint answered with a 2xx status."
          },
          "status_code": {
            "type": "integer",
            "description": "The endpoint's answer; left out when it never answered."
          },
          "error": {
            "type": "string",
            "description": "Why it failed."
          },
          "duration_ms": {
            "type": "integer"
          }
        },
        "required": [
          "delivery_id",
          "delivered",
          "duration_ms"
        ]
      },
      "WebhookEvent": {
        "type": "object",
        "description": "The JSON body of every webhook request.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The delivery's id, `whd_` and 32 hex characters, the same as the `Postatron-Delivery` header. Every attempt at a delivery, and any repeat, carries the same id, so use it to ignore one already handled."
          },
          "type": {
            "type": "string",
            "description": "The event's type, the same as the `Postatron-Event` header.",
            "enum": [
              "post.published",
              "post.partial",
              "post.failed",
              "post.unpublished",
              "account.connected",
              "account.disconnected",
              "account.needs_attention",
              "webhook.test"
            ]
          },
          "created_at": {
            "type": "string",
            "description": "When the event happened. Deliveries can arrive out of order; use this to order them.",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "description": "The event's data: `WebhookPostData` for the `post.*` events, `WebhookAccountData` for the `account.*` events and `WebhookTestData` for `webhook.test`."
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ]
      },
      "WebhookPostData": {
        "type": "object",
        "description": "The data of the `post.*` events.",
        "properties": {
          "post": {
            "$ref": "#/components/schemas/Post",
            "description": "The post exactly as `GET /v1/posts/{post_id}` describes it, read when the event was sent, with every delivery and its `error_code`."
          }
        },
        "required": [
          "post"
        ]
      },
      "WebhookAccountData": {
        "type": "object",
        "description": "The data of the `account.*` events.",
        "properties": {
          "account": {
            "$ref": "#/components/schemas/Account",
            "description": "The account as `GET /v1/accounts` describes it. On `account.disconnected` its `status` is `disconnected`."
          },
          "reason": {
            "type": "string",
            "description": "Why it was disconnected: `user`, `expired` or `revoked`.",
            "enum": [
              "user",
              "expired",
              "revoked"
            ]
          },
          "message": {
            "type": "string",
            "description": "What happened, when Postatron lost access or the account needs attention."
          },
          "at_risk": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventPostRef"
            },
            "description": "Scheduled posts that will fail until the account is connected again, soonest first and at most ten."
          },
          "at_risk_count": {
            "type": "integer",
            "description": "How many there are in all."
          }
        },
        "required": [
          "account"
        ]
      },
      "WebhookTestData": {
        "type": "object",
        "description": "The data of a `webhook.test` event.",
        "properties": {
          "webhook_id": {
            "type": "string",
            "description": "The webhook tested."
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "webhook_id",
          "message"
        ]
      },
      "WebhookPostEvent": {
        "description": "A `post.*` event.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEvent"
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "post.published",
                  "post.partial",
                  "post.failed",
                  "post.unpublished"
                ]
              },
              "data": {
                "$ref": "#/components/schemas/WebhookPostData"
              }
            }
          }
        ]
      },
      "WebhookAccountEvent": {
        "description": "An `account.*` event.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEvent"
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "account.connected",
                  "account.disconnected",
                  "account.needs_attention"
                ]
              },
              "data": {
                "$ref": "#/components/schemas/WebhookAccountData"
              }
            }
          }
        ]
      },
      "WebhookTestEvent": {
        "description": "A `webhook.test` event.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEvent"
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "webhook.test"
                ]
              },
              "data": {
                "$ref": "#/components/schemas/WebhookTestData"
              }
            }
          }
        ]
      }
    }
  }
}
