openapi: 3.2.0 info: title: PosteAhora Public Posts API version: 1.0.0 description: 'Key-authenticated REST gateway over the same publishing pipeline as the PosteAhora web app. It powers the MCP server, the CLI, the n8n node, and any third-party integration. This is the frozen **v1** contract: additive changes only. Human-readable docs: https://posteahora.com/docs/api' contact: name: PosteAhora url: https://posteahora.com/docs/api servers: - url: https://api.posteahora.com/functions/v1/api description: Production. The /functions/v1/ segment is part of the stable URL — do not strip it. security: - ApiKey: [] tags: - name: Posts paths: /posts: get: tags: - Posts summary: List posts parameters: - name: status in: query schema: type: string enum: - draft - scheduled - queued - published - partial_published - failed - name: limit in: query schema: type: integer default: 50 maximum: 200 responses: '200': description: OK content: application/json: schema: type: object properties: posts: type: array items: $ref: '#/components/schemas/Post' operationId: getPosts x-operation-id-source: derived post: tags: - Posts summary: Create a draft, schedule, or publish now (controlled by status) description: Multi-platform posts fan out into one row per platform at publish time. Publishing is asynchronous — a "published" create returns status "queued"; poll GET /posts/{id} and read platform_results. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreatePostRequest' examples: multiPlatformVideo: summary: One video → YouTube Shorts + TikTok + Instagram Reels, per-platform metadata value: caption: Fallback text if a platform override is missing mediaUrls: - https://cdn.posteahora.com/…/clip.mp4 mediaType: video postType: reel status: published accountMappings: - platform: youtube accountId: - platform: tiktok accountId: - platform: instagram accountId: platformCaptions: youtube: Full YouTube description — links, timestamps, etc. tiktok: 'TikTok caption #fyp' instagram: Reel caption for IG ✨ platformOptions: youtube: title: My video title categoryId: '22' madeForKids: false privacyStatus: public tags: - demo - api tiktok: privacyLevel: PUBLIC_TO_EVERYONE disableComment: false brandContentToggle: false brandOrganicToggle: false isAigc: false instagram: shareToFeed: true coverUrl: https://cdn.posteahora.com/…/cover.jpg responses: '201': description: Created content: application/json: schema: $ref: '#/components/schemas/CreatePostResult' '400': $ref: '#/components/responses/BadRequest' '402': description: Monthly post quota reached content: application/json: schema: $ref: '#/components/schemas/Error' '403': $ref: '#/components/responses/Forbidden' operationId: postPosts x-operation-id-source: derived /posts/{id}: parameters: - $ref: '#/components/parameters/Id' get: tags: - Posts summary: Read one post (includes connected_account_id and platform_results) responses: '200': description: OK content: application/json: schema: type: object properties: post: $ref: '#/components/schemas/Post' '404': $ref: '#/components/responses/NotFound' operationId: getPostsById x-operation-id-source: derived patch: tags: - Posts summary: Edit a draft or scheduled post (only sent fields are touched) requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdatePostRequest' responses: '200': description: OK content: application/json: schema: type: object properties: post: $ref: '#/components/schemas/Post' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' operationId: patchPostsById x-operation-id-source: derived delete: tags: - Posts summary: Soft-delete a post responses: '200': $ref: '#/components/responses/Deleted' '404': $ref: '#/components/responses/NotFound' operationId: deletePostsById x-operation-id-source: derived /posts/{id}/publish: parameters: - $ref: '#/components/parameters/Id' post: tags: - Posts summary: Publish an existing draft/scheduled post immediately description: Enters the async pipeline. Poll GET /posts/{id} for platform_results. responses: '200': description: Queued content: application/json: schema: $ref: '#/components/schemas/CreatePostResult' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' operationId: postPostsByIdPublish x-operation-id-source: derived components: parameters: Id: name: id in: path required: true schema: type: string format: uuid schemas: AccountMapping: type: object properties: platform: type: string example: youtube accountId: type: string description: connected_accounts.id. Required for every network except a few that don't need account selection. required: - platform DiscordOptions: type: object properties: channelId: type: string UpdatePostRequest: type: object description: Edit a draft or scheduled post. Only sent fields are touched. properties: title: type: string caption: type: string hashtags: type: array items: type: string mediaUrls: type: array items: type: string format: uri mediaType: type: string enum: - image - video postType: type: string enum: - post - reel - story platforms: type: array items: type: string connectedAccountId: type: string platformOptions: $ref: '#/components/schemas/PlatformOptions' status: type: string enum: - draft - scheduled scheduledAt: type: string format: date-time Post: type: object properties: id: type: string format: uuid title: type: - string - 'null' caption: type: - string - 'null' platforms: type: array items: type: string status: type: string enum: - draft - scheduled - queued - published - partial_published - failed scheduled_at: type: - string - 'null' format: date-time published_at: type: - string - 'null' format: date-time media_urls: type: array items: type: string format: uri media_type: type: - string - 'null' enum: - image - video - null connected_account_id: type: - string - 'null' format: uuid platform_results: type: array items: $ref: '#/components/schemas/PlatformResult' created_at: type: string format: date-time PlatformOptions: type: object description: Per-network settings, keyed by platform id. properties: youtube: $ref: '#/components/schemas/YouTubeOptions' tiktok: $ref: '#/components/schemas/TikTokOptions' instagram: $ref: '#/components/schemas/InstagramOptions' threads: $ref: '#/components/schemas/ThreadsOptions' bluesky: $ref: '#/components/schemas/BlueskyOptions' mastodon: $ref: '#/components/schemas/MastodonOptions' telegram: $ref: '#/components/schemas/TelegramOptions' discord: $ref: '#/components/schemas/DiscordOptions' pinterest: $ref: '#/components/schemas/PinterestOptions' additionalProperties: true PlatformResult: type: object properties: platform: type: string success: type: boolean error: type: string remote_post_id: type: string remote_publish_id: type: string remote_permalink: type: string format: uri required: - platform - success Error: type: object properties: error: type: string required: - error PinterestOptions: type: object description: boardId is REQUIRED and validated on create (400 without it). A Pin needs at least one image; 2–5 images make a carousel. required: - boardId properties: boardId: type: string description: Board id from GET /accounts/{id}/boards boardName: type: string description: Display only title: type: string maxLength: 100 link: type: string format: uri altText: type: string maxLength: 500 coverUrl: type: string format: uri description: 'Video Pins: 2:3 cover image URL' thumbOffsetMs: type: integer minimum: 0 description: 'Video Pins: keyframe used as cover when coverUrl is absent (ms)' ThreadsOptions: type: object properties: replyControl: type: string enum: - everyone - accounts_you_follow - mentioned_only - parent_post_author_only - followers_only topic: type: string altText: type: string linkAttachment: type: string format: uri quotePostId: type: string locationId: type: string allowlistedCountryCodes: type: array items: type: string chainItems: type: array items: type: object properties: text: type: string mediaUrl: type: string format: uri mediaType: type: string enum: - image - video altText: type: string required: - text CreatePostResult: type: object properties: postIds: type: array items: type: string format: uuid status: type: string enum: - draft - scheduled - queued dispatchDeferred: type: boolean description: When true, rows are stored but the worker wasn't reached synchronously; the cron sweep will publish them. YouTubeOptions: type: object description: 'The video description is the post caption (platformCaptions.youtube, ≤5000 chars). The video title is the required `title` field. There is no "Shorts" flag — a vertical clip ≤3 min is auto-classified as a Short. ' properties: title: type: string maxLength: 100 categoryId: type: string example: '22' madeForKids: type: boolean privacyStatus: type: string enum: - public - unlisted - private default: private tags: type: array items: type: string defaultLanguage: type: string containsSyntheticMedia: type: boolean embeddable: type: boolean publicStatsViewable: type: boolean license: type: string enum: - youtube - creativeCommon thumbnailUrl: type: string format: uri required: - title - categoryId - madeForKids CreatePostRequest: type: object properties: title: type: string caption: type: string description: Shared caption. Per-network override via platformCaptions. accountMappings: type: array items: $ref: '#/components/schemas/AccountMapping' description: Canonical channel selection. Prefer this over platforms+accountId. platforms: type: array items: type: string description: Convenience form for a single-channel post (combine with accountId). accountId: type: string description: Used only with the platforms convenience form. mediaUrls: type: array items: type: string format: uri mediaType: type: string enum: - image - video postType: type: string enum: - post - reel - story default: post hashtags: type: array items: type: string platformCaptions: type: object additionalProperties: type: string description: Per-platform caption/description override, keyed by platform id. platformOptions: $ref: '#/components/schemas/PlatformOptions' status: type: string enum: - draft - scheduled - published default: draft scheduledAt: type: string format: date-time description: Required (and must be in the future) when status = scheduled. required: - accountMappings InstagramOptions: type: object description: Set postType "reel" (top level) for a Reel, "story" for a Story. properties: shareToFeed: type: boolean coverUrl: type: string format: uri thumbOffsetMs: type: number audioName: type: string collaborators: type: array items: type: string locationId: type: string userTags: type: array items: type: object properties: username: type: string x: type: number y: type: number required: - username altText: type: string isAiGenerated: type: boolean isTrialReel: type: boolean description: reel only trialGraduationStrategy: type: string enum: - MANUAL - SS_PERFORMANCE description: reel only TikTokOptions: type: object properties: privacyLevel: type: string enum: - PUBLIC_TO_EVERYONE - MUTUAL_FOLLOW_FRIENDS - FOLLOWER_OF_CREATOR - SELF_ONLY disableComment: type: boolean brandContentToggle: type: boolean brandOrganicToggle: type: boolean isAigc: type: boolean postMode: type: string enum: - DIRECT_POST - MEDIA_UPLOAD default: DIRECT_POST disableDuet: type: boolean description: video only disableStitch: type: boolean description: video only videoCoverTimestampMs: type: number description: video only autoAddMusic: type: boolean description: photo only photoCoverIndex: type: number description: photo only photoTitle: type: string maxLength: 90 description: photo only required: - privacyLevel - disableComment - brandContentToggle - brandOrganicToggle - isAigc TelegramOptions: type: object properties: disableNotification: type: boolean protectContent: type: boolean MastodonOptions: type: object properties: visibility: type: string enum: - public - unlisted - private - direct spoilerText: type: string sensitive: type: boolean language: type: string BlueskyOptions: type: object properties: langs: type: array items: type: string responses: Deleted: description: Deleted content: application/json: schema: type: object properties: id: type: string deleted: type: boolean Forbidden: description: Key lacks the required scope (or viewer-role key attempting a write) content: application/json: schema: $ref: '#/components/schemas/Error' BadRequest: description: Invalid request content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Resource not found (or not in the key's workspace) content: application/json: schema: $ref: '#/components/schemas/Error' securitySchemes: ApiKey: type: http scheme: bearer description: 'A PosteAhora API key: `Authorization: Bearer pah_live_…` (or `pah_test_…`). Keys are created in the app under /api and carry scopes (ideas:read/write, posts:read/write, analytics:read, accounts:read, media:write). A missing scope returns 403. '