{
  "openapi": "3.1.0",
  "info": {
    "title": "Posti Public API",
    "version": "1.0.0",
    "summary": "Schedule and publish social media posts from your own systems.",
    "description": "Posti is a social media auto-posting service: it schedules, generates and\npublishes content to VK, Telegram, Instagram, Facebook, YouTube, TikTok,\nPinterest, OK, Dzen and MAX.\n\nThis document describes the public, machine-to-machine surface of Posti.\nAuthenticate with an organization API key (`Authorization: Bearer pk_...`),\ncreated in the organization settings of the Posti dashboard.\n\n## Pagination\n\nList endpoints return one page at a time. Pass `limit` (1–200, default 50)\nand `offset`. Every list response carries `limit`, `offset`, `total` and\n`has_more`, so a client can walk the whole collection without guessing.\n\n## Rate limits\n\nRequests are limited per client IP and, on the public endpoints, per\norganization. Every response carries the state of the window that applied:\n`RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds\nuntil the window resets, not a timestamp), mirrored in the legacy\n`X-RateLimit-*` names. When a limit is exceeded the API answers `429` with\n`Retry-After` (seconds) — honour it before retrying, otherwise the window\nstays closed. A daily write cap also applies. Responses are never\ncacheable by shared caches.\n\n## Permissions\n\nKeys are scoped. Each key carries an explicit set of permissions and every\noperation requires one of them (`x-required-scopes` on each operation):\n\n- `posts:read` — List the organization's posts.\n- `posts:write` — Create, schedule and delete posts.\n- `pages:read` — List the organization's connected social pages.\n\nA key without the required permission gets `403` with `required_scope` and\n`granted_scopes` in the body. Keys are managed per organization in the\ndashboard, and a key can be revoked without touching the others. This is\nnot OAuth: there is no authorization server, no refresh tokens and no\nthird-party delegation — the key is a long-lived organization credential.\n\n## Versioning and deprecation\n\nThe major version lives in the path (`/api/v1`). Additive changes — new\noptional request fields, new response fields, new endpoints — ship inside\n`v1` without notice; clients must ignore unknown fields. A breaking change\ngets a new path (`/api/v2`), and the previous major version keeps working\nfor at least 6 months after its successor is announced.\n\nAny endpoint scheduled for removal is marked with `Deprecation`\n(RFC 9745, `@` + unix seconds) and `Sunset` (RFC 8594, HTTP-date) response\nheaders and is listed under a \"Deprecations\" heading in `/llms.txt`. No\nendpoint is deprecated today. `info.version` tracks this document, not the\nAPI surface.\n\n## Errors\n\nEvery error is JSON with a single `error` string, documented per status\ncode: `400` invalid input, `401` missing or invalid key, `402` plan quota\nexhausted, `404` unknown resource, `413` payload too large, `429` rate\nlimited, `500` unexpected failure. Clients should branch on the status\ncode, not on the message text.\n\n## Discovery\n\nMCP server metadata is published as a Server Card (SEP-2127) at\n`https://posti.nexusbots.ru/.well-known/mcp/server-card.json` and referenced from the AI\nCatalog at `https://posti.nexusbots.ru/.well-known/ai-catalog.json`.",
    "contact": {
      "name": "Posti support",
      "email": "nexusbots.studio@gmail.com",
      "url": "https://posti.nexusbots.ru/contact"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://posti.nexusbots.ru/terms"
    }
  },
  "servers": [
    {
      "url": "https://posti.nexusbots.ru/api/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "posts",
      "description": "Create, list and delete posts for your organization."
    },
    {
      "name": "pages",
      "description": "Connected social accounts that can be used as post targets."
    },
    {
      "name": "system",
      "description": "Service endpoints without authentication."
    }
  ],
  "x-api-scopes": {
    "posts:read": "List the organization's posts.",
    "posts:write": "Create, schedule and delete posts.",
    "pages:read": "List the organization's connected social pages."
  },
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "system"
        ],
        "summary": "Service healthcheck",
        "description": "Returns `ok` when the API process is serving requests.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/public/pages": {
      "get": {
        "operationId": "listPublicPages",
        "tags": [
          "pages"
        ],
        "summary": "List connected social pages",
        "description": "Returns the social accounts of your organization. Use their `id` values as `targets` when creating a post.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "pages:read"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (1–200). Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of items to skip before the page.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of connected pages.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialPageList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/public/posts": {
      "get": {
        "operationId": "listPublicPosts",
        "tags": [
          "posts"
        ],
        "summary": "List posts",
        "description": "Returns one page of your organization's posts, oldest first. Use `limit`/`offset` together with `has_more` to walk the whole history.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "posts:read"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by post status.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "scheduled",
                "queued",
                "published",
                "error",
                "archived"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (1–200). Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of items to skip before the page.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of organization posts.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "post": {
        "operationId": "createPublicPost",
        "tags": [
          "posts"
        ],
        "summary": "Create a post",
        "description": "Creates a draft, schedules it for `scheduled_for`, or publishes it immediately with `publish_now: true` (which requires at least one target).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "posts:write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePostRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Post created.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/QuotaExceeded"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/public/posts/{id}": {
      "delete": {
        "operationId": "deletePublicPost",
        "tags": [
          "posts"
        ],
        "summary": "Delete a post",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-required-scopes": [
          "posts:write"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Post deleted.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests allowed in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pk_*",
        "description": "Organization API key, created in the Posti dashboard. A key carries an explicit set of permissions (scopes) — see `x-api-scopes` at the document root and `x-required-scopes` on each operation. This is a long-lived organization credential, not OAuth: there is no authorization server and no delegation."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "headers": {
          "WWW-Authenticate": {
            "description": "Bearer challenge (RFC 6750). Carries `error=\"invalid_token\"` when a key was supplied but rejected, and nothing when no key was supplied at all.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid request body.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit exceeded. Read `Retry-After` (seconds) before retrying; retrying earlier keeps the window closed.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the current window resets.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          "RateLimit-Limit": {
            "description": "Requests allowed in the current window.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window resets.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          "X-RateLimit-Limit": {
            "description": "Alias of `RateLimit-Limit`.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Alias of `RateLimit-Remaining`.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          "X-RateLimit-Reset": {
            "description": "Alias of `RateLimit-Reset` (seconds, not a timestamp).",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "QuotaExceeded": {
        "description": "The organization's plan quota is exhausted. The same check runs when creating a post from the dashboard.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/QuotaExceededError"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body exceeded the server limit. The limit covers the whole multipart payload, not just the media file.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InsufficientScope": {
        "description": "The key is valid but lacks the permission this operation requires. `required_scope` names what is missing and `granted_scopes` shows what the key actually has — grant the permission or use another key.",
        "headers": {
          "WWW-Authenticate": {
            "description": "Bearer challenge with `error=\"insufficient_scope\"` and the missing permission in `scope` (RFC 6750).",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/InsufficientScopeError"
            }
          }
        }
      },
      "InternalServerError": {
        "description": "Unexpected failure. The body never carries internals — retry with backoff and report the time if it persists.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "headers": {
      "Deprecation": {
        "description": "Marks a deprecated endpoint. Value is `@` followed by the unix timestamp when deprecation started (RFC 9745).",
        "schema": {
          "type": "string",
          "examples": [
            "@1798761600"
          ]
        }
      },
      "Sunset": {
        "description": "HTTP-date after which a deprecated endpoint stops responding (RFC 8594).",
        "schema": {
          "type": "string",
          "examples": [
            "Sat, 01 May 2027 00:00:00 GMT"
          ]
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "examples": [
              "Unauthorized"
            ]
          }
        }
      },
      "QuotaUsage": {
        "type": "object",
        "required": [
          "posts",
          "maxPosts",
          "diskBytes",
          "maxDiskBytes"
        ],
        "properties": {
          "posts": {
            "type": "integer",
            "description": "Active draft/scheduled/queued posts."
          },
          "maxPosts": {
            "type": "integer"
          },
          "diskBytes": {
            "type": "integer"
          },
          "maxDiskBytes": {
            "type": "integer"
          }
        }
      },
      "QuotaExceededError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "quota": {
            "$ref": "#/components/schemas/QuotaUsage"
          }
        }
      },
      "InsufficientScopeError": {
        "type": "object",
        "required": [
          "error",
          "required_scope",
          "granted_scopes"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "required_scope": {
            "type": "string",
            "description": "Permission this operation needs.",
            "examples": [
              "posts:write"
            ]
          },
          "granted_scopes": {
            "type": "array",
            "description": "Permissions the presented key actually has.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          }
        }
      },
      "DeleteResult": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "PostList": {
        "type": "object",
        "required": [
          "posts",
          "limit",
          "offset",
          "total",
          "has_more"
        ],
        "properties": {
          "posts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Post"
            }
          },
          "limit": {
            "type": "integer",
            "description": "Page size actually applied."
          },
          "offset": {
            "type": "integer",
            "description": "Offset actually applied."
          },
          "total": {
            "type": "integer",
            "description": "Items in the collection."
          },
          "has_more": {
            "type": "boolean",
            "description": "Whether more items remain after this page."
          }
        }
      },
      "SocialPageList": {
        "type": "object",
        "required": [
          "pages",
          "limit",
          "offset",
          "total",
          "has_more"
        ],
        "properties": {
          "pages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SocialPage"
            }
          },
          "limit": {
            "type": "integer",
            "description": "Page size actually applied."
          },
          "offset": {
            "type": "integer",
            "description": "Offset actually applied."
          },
          "total": {
            "type": "integer",
            "description": "Items in the collection."
          },
          "has_more": {
            "type": "boolean",
            "description": "Whether more items remain after this page."
          }
        }
      },
      "PostCreated": {
        "type": "object",
        "required": [
          "post"
        ],
        "properties": {
          "post": {
            "$ref": "#/components/schemas/Post"
          },
          "published": {
            "type": "integer",
            "description": "Targets published successfully."
          },
          "failed": {
            "type": "integer",
            "description": "Targets that failed to publish."
          },
          "error": {
            "type": "string",
            "description": "Publishing error, if any."
          }
        }
      },
      "SocialPage": {
        "type": "object",
        "required": [
          "id",
          "medium"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "medium": {
            "type": "string",
            "description": "Social network code.",
            "examples": [
              "vk",
              "telegram",
              "instagram",
              "facebook",
              "youtube"
            ]
          },
          "type": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "screen_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_connected": {
            "type": "boolean"
          }
        }
      },
      "Post": {
        "type": "object",
        "required": [
          "id",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "scheduled",
              "queued",
              "published",
              "error",
              "archived"
            ]
          },
          "text": {
            "type": [
              "string",
              "null"
            ]
          },
          "media_urls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "video_urls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "post_type": {
            "type": "string",
            "enum": [
              "feed",
              "reels"
            ]
          },
          "scheduled_for": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "repeat_days": {
            "type": [
              "integer",
              "null"
            ]
          },
          "repeat_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "targets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "medium": {
                  "type": "string"
                },
                "name": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string"
                },
                "color": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreatePostRequest": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "Post body."
          },
          "media_urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "video_urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "post_type": {
            "type": "string",
            "enum": [
              "feed",
              "reels"
            ],
            "default": "feed",
            "description": "Post format. `reels` publishes the single video as an Instagram Reel (`media_type=REELS`) instead of a feed video; other networks ignore it."
          },
          "template_id": {
            "type": "string"
          },
          "scheduled_for": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 date-time for scheduled publishing."
          },
          "publish_now": {
            "type": "boolean",
            "default": false,
            "description": "Publish immediately. Requires `targets`."
          },
          "first_comment": {
            "type": "string"
          },
          "targets": {
            "type": "array",
            "description": "Social page ids to publish to.",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "watermark_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "disable_watermark": {
            "type": "boolean"
          },
          "disable_compression": {
            "type": "boolean"
          },
          "repeat_days": {
            "type": "integer",
            "minimum": 1,
            "description": "Repeat the post every N days."
          },
          "repeat_until": {
            "type": "string",
            "format": "date-time"
          },
          "tags": {
            "type": "array",
            "description": "Tag names; missing tags are created automatically.",
            "items": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}