{"openapi":"3.1.0","info":{"title":"cometpost API","version":"v1","summary":"Schedule and publish social posts from software.","description":"The public `/v1` API of cometpost. A Workspace connects social accounts (Channels); an agent or program drafts, schedules and tracks Posts against them. The dashboard and the MCP server are clients of this same API.\n\n**Authentication.** Send a bearer credential: `Authorization: Bearer cp_live_...` (a Credential created in the dashboard) or an OAuth access token from an assistant a person connected. A credential in a URL is refused. Every route here except `GET /v1/openapi.json` requires one.\n\n**Credentials are constrained server-side.** `may_schedule` (default off) means Posts a Credential creates are drafts a person must publish in the dashboard. A Channel scope bounds reads as well as writes: a Channel, Post or Delivery outside it answers 404, never 403.\n\n**Errors.** `{ \"error\": { \"code\", \"message\", \"retryable\" } }` (ADR-0004): `code` is stable, `message` states the next action, `retryable` says whether to send the same request again. A few validation refusals still answer a bare `{ \"error\": \"sentence\" }`; those are the `LegacyError` schema.\n\n**Daily allowance headers.** Every response to a Credential or connected assistant carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds) for its daily request allowance; a 429 adds `Retry-After`. A signed-in person has no daily allowance and gets none.\n\n**Versioning.** `/v1` is additive-only: no field changes meaning or is removed within a major version.\n\n**Also available:** the same capabilities as MCP tools at https://mcp.cometpost.app/mcp; prose contract in docs/api/v1.md.\n\nOnly routes a Credential or OAuth grant can call are described. Routes that need a signed-in person (publishing a draft, deleting, billing, credentials) are intentionally absent."},"servers":[{"url":"https://api.cometpost.app","description":"Production"}],"tags":[{"name":"Platforms"},{"name":"Channels"},{"name":"Posts"},{"name":"Media"},{"name":"Webhooks","description":"Signed publish-result events (Standard Webhooks) and their delivery log."},{"name":"Meta"}],"paths":{"/v1/platforms":{"get":{"operationId":"listPlatforms","tags":["Platforms"],"summary":"What each platform demands","description":"Caption limits, media constraints, Publish Options and cometpost's own media intake ceilings. Static between deploys: cached for an hour with an ETag. This is what a platform demands of everybody; what one Channel may do right now is `GET /v1/channels/{id}/prepare`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"The ETag of a previous answer; an unchanged answer is 304."}],"responses":{"200":{"description":"The platforms this deployment offers.","content":{"application/json":{"schema":{"type":"object","properties":{"mediaIntake":{"type":"object","properties":{"directUpload":{"$ref":"#/components/schemas/MediaIntakePath"},"urlPull":{"$ref":"#/components/schemas/MediaIntakePath"},"inline":{"$ref":"#/components/schemas/MediaIntakePath"}},"required":["directUpload","urlPull","inline"]},"platforms":{"type":"array","items":{"$ref":"#/components/schemas/PlatformCapabilities"}}},"required":["mediaIntake","platforms"]}}},"headers":{"ETag":{"schema":{"type":"string"}},"Cache-Control":{"schema":{"type":"string"}}}},"304":{"description":"Unchanged since the ETag you sent."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/channels":{"get":{"operationId":"listChannels","tags":["Channels"],"summary":"List the Channels this caller may use","description":"Only Channels inside the Credential's scope are listed; there is no 'usable' flag because every listed Channel is usable. Connecting a Channel is not part of the API: it needs a person at a browser.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Channels in scope.","content":{"application/json":{"schema":{"type":"object","properties":{"channels":{"type":"array","items":{"$ref":"#/components/schemas/Channel"}}},"required":["channels"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/channels/{id}/prepare":{"get":{"operationId":"prepareChannel","tags":["Channels"],"summary":"What one Channel can do right now","description":"Asks the platform, with this Channel's own token. A problem is not an error: the answer is 200 with `problems` filled in. An empty `problems` means nothing is known, never that the Channel is ready. It costs a call to the platform, so ask when a person is choosing Channels, not on every render.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Channel id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Legal options, known problems, and quota where the platform volunteers it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrepareChannelResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The Channel is not connected. Reconnect it in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegacyError"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/posts":{"get":{"operationId":"listPosts","tags":["Posts"],"summary":"List and search Posts, newest first","description":"Compact rows. Follow `next_cursor` until it is null; a `status` filter may return a short or empty page with a cursor to continue. Every filter narrows the Credential's Channel scope and none replaces it.","security":[{"bearerAuth":[]}],"parameters":[{"name":"status","in":"query","required":false,"description":"Only Posts in this derived state.","schema":{"type":"string","enum":["draft","scheduled","unscheduled","publishing","published","sent_to_drafts","partially_published","failed","cancelled"]}},{"name":"channel_id","in":"query","required":false,"description":"Only Posts with a Delivery on this Channel. A Channel outside the Credential's scope matches nothing.","schema":{"type":"string","minLength":1,"maxLength":128}},{"name":"from","in":"query","required":false,"description":"Inclusive lower bound, ISO 8601 with Z or an offset. Must be before `to`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Exclusive upper bound, ISO 8601 with Z or an offset.","schema":{"type":"string","format":"date-time"}},{"name":"range_on","in":"query","required":false,"description":"What from/to apply to: the Post's creation time (default) or any of its Deliveries' effectiveAt.","schema":{"default":"created","type":"string","enum":["created","scheduled"]}},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring of the base caption, up to 200 characters. Platform variants are not searched.","schema":{"type":"string","maxLength":200}},{"name":"limit","in":"query","required":false,"description":"Rows per page, 1 to 100, default 50.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"The previous page's next_cursor. Opaque: do not build or parse one.","schema":{"type":"string","minLength":1,"maxLength":512}}],"responses":{"200":{"description":"A page of Posts.","content":{"application/json":{"schema":{"type":"object","properties":{"posts":{"type":"array","items":{"$ref":"#/components/schemas/PostSummary"}},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["posts","next_cursor"]}}}},"400":{"description":"invalid_request or invalid_cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createPost","tags":["Posts"],"summary":"Create a Post","description":"Composes once and fans out into one Delivery per Channel. A Credential without `may_schedule` (the default) gets a draft that a person must publish in the dashboard; one with it schedules immediately (`scheduledFor` omitted means now). A Post naming any Channel outside the Credential's scope is refused whole with 403. Send `Idempotency-Key` whenever a lost reply would leave you unsure whether the Post exists. Run `POST /v1/posts/validate` first to learn whether it would be accepted. Publish Options a platform does not list in `GET /v1/platforms` are rejected (`publish_options_unknown`).","security":[{"bearerAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"1 to 255 printable ASCII characters, no spaces; a random UUID is right. The same key and body from the same caller within 24 hours replays the original response as 200 with `Idempotent-Replay: true`. Same key, different body is 409 idempotency_key_reused.","schema":{"type":"string","minLength":1,"maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostRequest"}}}},"responses":{"200":{"description":"Idempotent replay of an earlier 201: the same body.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ScheduledPostResponse"},{"$ref":"#/components/schemas/DraftPostResponse"}]}}},"headers":{"Idempotent-Replay":{"schema":{"type":"string","enum":["true"]}}}},"201":{"description":"Created. Scheduled for a schedule-capable caller, draft otherwise.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ScheduledPostResponse"},{"$ref":"#/components/schemas/DraftPostResponse"}]}}}},"400":{"description":"Invalid body, unknown Channel or media, or content a platform would refuse (`content_rejected`, with `problems`; `publish_options_*`).","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ContentRejected"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/LegacyError"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The plan refuses this: no Slot, unpaid subscription, or a video the plan does not allow.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"channel_out_of_scope: a named Channel is outside this Credential's scope. Nothing was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A Channel is disconnected, or idempotency_key_reused / idempotency_key_in_progress.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/LegacyError"}]}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"scheduling_unavailable: the scheduler was unreachable and nothing was created. Retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/posts/validate":{"post":{"operationId":"validatePost","tags":["Posts"],"summary":"Check a Post without creating it","description":"The same body and the same checks as `POST /v1/posts`, writing nothing. A refusal is exactly the response create would give. It cannot tell you whether the platform will accept the content when it is published. It counts against the daily allowance like any request.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostRequest"}}}},"responses":{"200":{"description":"Would be accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidatePostResponse"}}}},"400":{"description":"Same refusals as create.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ContentRejected"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/LegacyError"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Plan refusal, as create.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"channel_out_of_scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Channel or media state, as create.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/LegacyError"}]}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/posts/{id}":{"get":{"operationId":"getPost","tags":["Posts"],"summary":"One Post with every Delivery and Attempt","description":"Per-Delivery status, effective time, resolved content, platform post id, permalink, and each Attempt's own platform status and error code.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Post id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The Post.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"editPost","tags":["Posts"],"summary":"Edit a draft or pending Post","description":"Removing a pending destination cancels it; adding one creates a Delivery. Publishing, published and failed destinations keep their frozen caption and options. A drafts-only Credential can edit drafts but not scheduled Posts, and cannot widen the Post's Channels.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Post id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditPostRequest"}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"updated":{"type":"boolean","enum":[true]},"failedToStart":{"type":"array","items":{"type":"string"}}},"required":["id","updated"]}}}},"400":{"description":"invalid_post_edit, or content a platform would refuse.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/ContentRejected"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"may_not_schedule or channel_out_of_scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The Post or a destination is in a state that cannot change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/posts/{id}/schedule":{"post":{"operationId":"schedulePost","tags":["Posts"],"summary":"Arm Deliveries whose workflow never started","description":"Safe to repeat: it only touches `unscheduled` Deliveries. Needs `may_schedule`, whoever wrote the Post: a drafts-only Credential cannot arm even a draft a person wrote. Publishing a draft is not this route; that needs a signed-in person.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Post id.","schema":{"type":"string"}}],"responses":{"200":{"description":"How many were armed.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"rearmed":{"type":"integer"},"message":{"type":"string"},"stillFailing":{"type":"array","items":{"type":"string"}}},"required":["id","rearmed"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"may_not_schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"scheduling_unavailable. Nothing changed; retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/posts/{id}/cancel":{"post":{"operationId":"cancelPost","tags":["Posts"],"summary":"Cancel a Post's still-live Deliveries","description":"Stops every Delivery that is `pending` or `unscheduled`. A Delivery already publishing, published or delivered to drafts cannot be recalled and is listed in `pastRecall`. The Post becomes `cancelled` only when at least one Delivery was stopped, so `cancelled: 0` with a non-empty `pastRecall` means the Post is live. Does not need `may_schedule`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Post id.","schema":{"type":"string"}}],"responses":{"200":{"description":"What was stopped and what was past recall.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"state":{"$ref":"#/components/schemas/PostState"},"cancelled":{"type":"integer"},"pastRecall":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"status":{"$ref":"#/components/schemas/DeliveryStatus"}},"required":["id","status"]}}},"required":["id","status","state","cancelled","pastRecall"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/deliveries/{id}/reschedule":{"post":{"operationId":"rescheduleDelivery","tags":["Posts"],"summary":"Move one pending Delivery to a new time","description":"Needs `may_schedule`: time is content. Refused once the Delivery has left `pending`, for a draft (its time is chosen when a person publishes it), and when it is about to publish. Plan scheduling horizon and asset expiry apply.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Delivery id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleRequest"}}}},"responses":{"200":{"description":"The new time.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"effectiveAt":{"type":"string","format":"date-time"}},"required":["id","effectiveAt"]}}}},"400":{"description":"Invalid effectiveAt or a time the plan or an attached asset cannot meet.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/LegacyError"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The plan refuses this.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"may_not_schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The Delivery is no longer pending, is a draft's, or is in flight.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/LegacyError"}]}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/media":{"get":{"operationId":"listMedia","tags":["Media"],"summary":"List the Workspace's media library","description":"The library is Workspace-wide, so a Channel scope does not filter it. A Credential narrowed to some Channels gets `preview_url: null`; a Workspace-wide one gets a 10-minute signed URL per ready asset. Reuse a `ready` id directly.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Assets.","content":{"application/json":{"schema":{"type":"object","properties":{"media":{"type":"array","items":{"allOf":[{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["pending","ready","rejected","expired"]},"filename":{"anyOf":[{"type":"string"},{"type":"null"}]},"content_type":{"type":"string"},"size_bytes":{"type":"integer"},"probe_status":{"anyOf":[{"type":"string","enum":["probed","unprobed"]},{"type":"null"}],"description":"null for an image: there is nothing to probe."},"video":{"anyOf":[{"type":"object","properties":{"container":{"anyOf":[{"type":"string"},{"type":"null"}]},"codec":{"anyOf":[{"type":"string"},{"type":"null"}]},"fps":{"anyOf":[{"type":"number"},{"type":"null"}]},"width":{"anyOf":[{"type":"integer"},{"type":"null"}]},"height":{"anyOf":[{"type":"integer"},{"type":"null"}]},"duration_seconds":{"anyOf":[{"type":"number"},{"type":"null"}]},"has_audio":{"anyOf":[{"type":"boolean"},{"type":"null"}]}}},{"type":"null"}],"description":"Present only for a probed video; null otherwise, so a null codec never means \"did not look\"."},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["id","status","content_type","size_bytes"]},{"type":"object","properties":{"width":{"anyOf":[{"type":"integer"},{"type":"null"}]},"height":{"anyOf":[{"type":"integer"},{"type":"null"}]},"preview_url":{"anyOf":[{"type":"string"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"}}}]}}},"required":["media"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createMedia","tags":["Media"],"summary":"Add media: presigned upload, pull from URL, or inline","description":"Three ways in, chosen by the body. Intake limits: presigned direct upload 512 MiB, pull-from-URL 64 MiB, inline base64 5 MiB.\n**Presigned** (any size up to the cap): send filename, contentType and sizeBytes; `PUT` exactly sizeBytes bytes to the returned upload_url with the same Content-Type; then call `POST /v1/media/{id}/complete`. The asset is `pending` until completion succeeds and only a `ready` asset can be attached to a Post.\n**Pull from URL**: send source_url, a public http(s) URL. The fetch is SSRF-guarded. The asset is `ready` on return.\n**Inline**: send base64 data and content_type. Small images only. `ready` on return.\nVolume is capped per Workspace per day across all three. Free intake needs a connected Channel.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CreateUploadRequest"},{"$ref":"#/components/schemas/PullUploadRequest"},{"$ref":"#/components/schemas/InlineUploadRequest"}]}}}},"responses":{"201":{"description":"A presigned upload slot, or a ready asset.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"id":{"type":"string"},"upload_url":{"type":"string","description":"PUT the bytes here. Valid for 15 minutes."},"expires_at":{"type":"string","format":"date-time"},"instructions":{"type":"string"}},"required":["id","upload_url","expires_at"]},{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["pending","ready","rejected","expired"]},"filename":{"anyOf":[{"type":"string"},{"type":"null"}]},"content_type":{"type":"string"},"size_bytes":{"type":"integer"},"probe_status":{"anyOf":[{"type":"string","enum":["probed","unprobed"]},{"type":"null"}],"description":"null for an image: there is nothing to probe."},"video":{"anyOf":[{"type":"object","properties":{"container":{"anyOf":[{"type":"string"},{"type":"null"}]},"codec":{"anyOf":[{"type":"string"},{"type":"null"}]},"fps":{"anyOf":[{"type":"number"},{"type":"null"}]},"width":{"anyOf":[{"type":"integer"},{"type":"null"}]},"height":{"anyOf":[{"type":"integer"},{"type":"null"}]},"duration_seconds":{"anyOf":[{"type":"number"},{"type":"null"}]},"has_audio":{"anyOf":[{"type":"boolean"},{"type":"null"}]}}},{"type":"null"}],"description":"Present only for a probed video; null otherwise, so a null codec never means \"did not look\"."},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["id","status","content_type","size_bytes"]}]}}}},"400":{"description":"invalid_request, or a fetch the server refused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"free_signups_closed, connected_channel_required or subscription_unpaid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"media_too_large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"media_type_unsupported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited, or the media allowances ingest_cap_exhausted and media_busy (retryable).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/media/{id}/complete":{"post":{"operationId":"completeMedia","tags":["Media"],"summary":"Finish a presigned upload","description":"Send no body. The bytes that actually landed are checked against the declaration and the cap. Safe to repeat on a `ready` asset. 409 media_not_uploaded is retryable once the PUT lands.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Media id from POST /v1/media.","schema":{"type":"string"}}],"responses":{"200":{"description":"The asset, `ready`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["pending","ready","rejected","expired"]},"filename":{"anyOf":[{"type":"string"},{"type":"null"}]},"content_type":{"type":"string"},"size_bytes":{"type":"integer"},"probe_status":{"anyOf":[{"type":"string","enum":["probed","unprobed"]},{"type":"null"}],"description":"null for an image: there is nothing to probe."},"video":{"anyOf":[{"type":"object","properties":{"container":{"anyOf":[{"type":"string"},{"type":"null"}]},"codec":{"anyOf":[{"type":"string"},{"type":"null"}]},"fps":{"anyOf":[{"type":"number"},{"type":"null"}]},"width":{"anyOf":[{"type":"integer"},{"type":"null"}]},"height":{"anyOf":[{"type":"integer"},{"type":"null"}]},"duration_seconds":{"anyOf":[{"type":"number"},{"type":"null"}]},"has_audio":{"anyOf":[{"type":"boolean"},{"type":"null"}]}}},{"type":"null"}],"description":"Present only for a probed video; null otherwise, so a null codec never means \"did not look\"."},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["id","status","content_type","size_bytes"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Plan refusal, as POST /v1/media.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"media_not_uploaded (retryable), media_not_pending or media_changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"media_too_large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"media_type_unsupported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited or ingest_cap_exhausted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks":{"get":{"operationId":"listWebhooks","tags":["Webhooks"],"summary":"List webhook endpoints","description":"Every endpoint in the Workspace, without secrets, and the plan's endpoint limit (free 1, starter 5, growth 10, scale 25). Reading needs a session or a Credential whose Channel scope is the whole Workspace (a narrowed one gets 403 workspace_scope_required).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The endpoints.","content":{"application/json":{"schema":{"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}},"endpoint_limit":{"type":"integer"}},"required":["webhooks","endpoint_limit"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"workspace_scope_required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createWebhook","tags":["Webhooks"],"summary":"Register a webhook endpoint","description":"https only. The URL must resolve only to public addresses (no private, loopback, link-local or metadata ranges), checked now and again on every send; redirects are never followed. `events` defaults to every type. The response carries the signing secret, **once**. Changing needs a session or a cp_live_ key created with manageWebhooks (which requires maySchedule and every Channel). An assistant or any other Credential gets 401.","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"}}}},"responses":{"201":{"description":"The endpoint and its secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointWithSecret"}}}},"400":{"description":"invalid_request, webhook_url_invalid, webhook_url_blocked or webhook_url_unresolved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"webhook_endpoint_limit: the plan's endpoint count is used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/webhooks/{id}":{"get":{"operationId":"getWebhook","tags":["Webhooks"],"summary":"Read one webhook endpoint","description":"Never the secret. Reading needs a session or a Credential whose Channel scope is the whole Workspace (a narrowed one gets 403 workspace_scope_required).","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"workspace_scope_required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateWebhook","tags":["Webhooks"],"summary":"Change a webhook endpoint","description":"Any of url (re-checked), events, description, enabled. A disabled endpoint is sent nothing; deliveries still pending for it are marked failed and can be redelivered once it is enabled again. Changing needs a session or a cp_live_ key created with manageWebhooks (which requires maySchedule and every Channel). An assistant or any other Credential gets 401.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookRequest"}}}},"responses":{"200":{"description":"The endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"400":{"description":"invalid_request or a refused URL, as create.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteWebhook","tags":["Webhooks"],"summary":"Delete a webhook endpoint","description":"Stops sends at once and removes its delivery log. Changing needs a session or a cp_live_ key created with manageWebhooks (which requires maySchedule and every Channel). An assistant or any other Credential gets 401.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}},"required":["id","deleted"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/webhooks/{id}/rotate-secret":{"post":{"operationId":"rotateWebhookSecret","tags":["Webhooks"],"summary":"Rotate the signing secret","description":"Returns the new secret, once. The replaced secret keeps signing too (a second `v1,` signature) for `grace_period_seconds`, default and maximum 86400 (24 hours); send 0 to end it now if it leaked. Changing needs a session or a cp_live_ key created with manageWebhooks (which requires maySchedule and every Channel). An assistant or any other Credential gets 401.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotateWebhookSecretRequest"}}}},"responses":{"200":{"description":"The endpoint and its new secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointWithSecret"}}}},"400":{"description":"invalid_request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/webhooks/{id}/test":{"post":{"operationId":"testWebhook","tags":["Webhooks"],"summary":"Send a test event","description":"Queues one signed `webhook.test` event to this endpoint only, through the same pipeline as real events; watch it in the delivery log. Counts toward the plan's hourly test-and-redelivery limit. Changing needs a session or a cp_live_ key created with manageWebhooks (which requires maySchedule and every Channel). An assistant or any other Credential gets 401.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint id.","schema":{"type":"string"}}],"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"type":"object","properties":{"event_id":{"type":"string"},"delivery_id":{"type":"string"},"status":{"type":"string","enum":["pending"]}},"required":["event_id","delivery_id","status"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"webhook_disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited, or webhook_manual_send_limit (retryable).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhook-deliveries":{"get":{"operationId":"listWebhookDeliveries","tags":["Webhooks"],"summary":"The webhook delivery log","description":"Newest first, kept 30 days. Each row has the event as sent, its status, attempts, the last response code and a short snippet. Reading needs a session or a Credential whose Channel scope is the whole Workspace (a narrowed one gets 403 workspace_scope_required).","security":[{"bearerAuth":[]}],"parameters":[{"name":"endpoint_id","in":"query","required":false,"schema":{"type":"string","maxLength":100}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","succeeded","failed"]}},{"name":"event_type","in":"query","required":false,"schema":{"type":"string","enum":["delivery.published","delivery.delivered_to_drafts","delivery.failed","channel.needs_reconnect","post.cancelled","webhook.test"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","maxLength":200}}],"responses":{"200":{"description":"A page.","content":{"application/json":{"schema":{"type":"object","properties":{"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["deliveries","next_cursor"]}}}},"400":{"description":"invalid_request or invalid_cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"workspace_scope_required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/webhook-deliveries/{id}/redeliver":{"post":{"operationId":"redeliverWebhook","tags":["Webhooks"],"summary":"Send a finished delivery again","description":"A new delivery row for the same event (same `webhook-id`), so the original stays in the log. Only for a delivery that succeeded or failed, to an enabled endpoint. Counts toward the hourly test-and-redelivery limit. Changing needs a session or a cp_live_ key created with manageWebhooks (which requires maySchedule and every Channel). An assistant or any other Credential gets 401.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook delivery id.","schema":{"type":"string"}}],"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"type":"object","properties":{"delivery_id":{"type":"string"},"event_id":{"type":"string"},"redelivery_of":{"type":"string"},"status":{"type":"string","enum":["pending"]}},"required":["delivery_id","event_id","redelivery_of","status"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"webhook_delivery_pending or webhook_disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited, or webhook_manual_send_limit (retryable).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/openapi.json":{"get":{"operationId":"getOpenApiDocument","tags":["Meta"],"summary":"This document","description":"Unauthenticated. Cached for an hour with an ETag.","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object"}}},"headers":{"ETag":{"schema":{"type":"string"}},"Cache-Control":{"schema":{"type":"string"}}}},"304":{"description":"Unchanged since the ETag you sent."}}}}},"webhooks":{"delivery.published":{"post":{"operationId":"webhook_delivery_published","tags":["Webhooks"],"summary":"A Delivery is live on its platform","description":"Sent to every enabled endpoint subscribed to `delivery.published`, at least once. Answer 2xx within 10 seconds; anything else (redirects included: they are not followed) is retried with exponential backoff, up to 15 attempts over about a day and a half, and never more than 72 hours after the event.","parameters":[{"name":"webhook-id","in":"header","required":true,"schema":{"type":"string"},"description":"The event id (evt_…). Dedupe on it."},{"name":"webhook-timestamp","in":"header","required":true,"schema":{"type":"integer"},"description":"Unix seconds of this attempt. Reject one more than 300 seconds from your clock."},{"name":"webhook-signature","in":"header","required":true,"schema":{"type":"string"},"description":"Space-separated `v1,<base64>`: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}` keyed with the base64-decoded secret after `whsec_` (Standard Webhooks). Two during a rotation's grace period; accept either."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryPublishedEvent"}}}},"responses":{"2XX":{"description":"Received. The body is ignored; only the status and a short snippet are logged."}}}},"delivery.delivered_to_drafts":{"post":{"operationId":"webhook_delivery_delivered_to_drafts","tags":["Webhooks"],"summary":"A Delivery was handed to the creator as a draft (not a post)","description":"Sent to every enabled endpoint subscribed to `delivery.delivered_to_drafts`, at least once. Answer 2xx within 10 seconds; anything else (redirects included: they are not followed) is retried with exponential backoff, up to 15 attempts over about a day and a half, and never more than 72 hours after the event.","parameters":[{"name":"webhook-id","in":"header","required":true,"schema":{"type":"string"},"description":"The event id (evt_…). Dedupe on it."},{"name":"webhook-timestamp","in":"header","required":true,"schema":{"type":"integer"},"description":"Unix seconds of this attempt. Reject one more than 300 seconds from your clock."},{"name":"webhook-signature","in":"header","required":true,"schema":{"type":"string"},"description":"Space-separated `v1,<base64>`: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}` keyed with the base64-decoded secret after `whsec_` (Standard Webhooks). Two during a rotation's grace period; accept either."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryDraftedEvent"}}}},"responses":{"2XX":{"description":"Received. The body is ignored; only the status and a short snippet are logged."}}}},"delivery.failed":{"post":{"operationId":"webhook_delivery_failed","tags":["Webhooks"],"summary":"A Delivery failed permanently","description":"Sent to every enabled endpoint subscribed to `delivery.failed`, at least once. Answer 2xx within 10 seconds; anything else (redirects included: they are not followed) is retried with exponential backoff, up to 15 attempts over about a day and a half, and never more than 72 hours after the event.","parameters":[{"name":"webhook-id","in":"header","required":true,"schema":{"type":"string"},"description":"The event id (evt_…). Dedupe on it."},{"name":"webhook-timestamp","in":"header","required":true,"schema":{"type":"integer"},"description":"Unix seconds of this attempt. Reject one more than 300 seconds from your clock."},{"name":"webhook-signature","in":"header","required":true,"schema":{"type":"string"},"description":"Space-separated `v1,<base64>`: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}` keyed with the base64-decoded secret after `whsec_` (Standard Webhooks). Two during a rotation's grace period; accept either."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryFailedEvent"}}}},"responses":{"2XX":{"description":"Received. The body is ignored; only the status and a short snippet are logged."}}}},"channel.needs_reconnect":{"post":{"operationId":"webhook_channel_needs_reconnect","tags":["Webhooks"],"summary":"A Channel needs a human to reconnect it","description":"Sent to every enabled endpoint subscribed to `channel.needs_reconnect`, at least once. Answer 2xx within 10 seconds; anything else (redirects included: they are not followed) is retried with exponential backoff, up to 15 attempts over about a day and a half, and never more than 72 hours after the event.","parameters":[{"name":"webhook-id","in":"header","required":true,"schema":{"type":"string"},"description":"The event id (evt_…). Dedupe on it."},{"name":"webhook-timestamp","in":"header","required":true,"schema":{"type":"integer"},"description":"Unix seconds of this attempt. Reject one more than 300 seconds from your clock."},{"name":"webhook-signature","in":"header","required":true,"schema":{"type":"string"},"description":"Space-separated `v1,<base64>`: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}` keyed with the base64-decoded secret after `whsec_` (Standard Webhooks). Two during a rotation's grace period; accept either."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelNeedsReconnectEvent"}}}},"responses":{"2XX":{"description":"Received. The body is ignored; only the status and a short snippet are logged."}}}},"post.cancelled":{"post":{"operationId":"webhook_post_cancelled","tags":["Webhooks"],"summary":"A Post was cancelled","description":"Sent to every enabled endpoint subscribed to `post.cancelled`, at least once. Answer 2xx within 10 seconds; anything else (redirects included: they are not followed) is retried with exponential backoff, up to 15 attempts over about a day and a half, and never more than 72 hours after the event.","parameters":[{"name":"webhook-id","in":"header","required":true,"schema":{"type":"string"},"description":"The event id (evt_…). Dedupe on it."},{"name":"webhook-timestamp","in":"header","required":true,"schema":{"type":"integer"},"description":"Unix seconds of this attempt. Reject one more than 300 seconds from your clock."},{"name":"webhook-signature","in":"header","required":true,"schema":{"type":"string"},"description":"Space-separated `v1,<base64>`: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}` keyed with the base64-decoded secret after `whsec_` (Standard Webhooks). Two during a rotation's grace period; accept either."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCancelledEvent"}}}},"responses":{"2XX":{"description":"Received. The body is ignored; only the status and a short snippet are logged."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"cp_live_ key or OAuth access token","description":"A Credential (`cp_live_...`, shown once when created in the dashboard) or an OAuth access token. Sent only in the Authorization header."}},"schemas":{"ErrorDetail":{"type":"object","properties":{"code":{"type":"string","description":"Stable and machine-readable. It never changes meaning."},"message":{"type":"string","description":"States the next action, not only the fault."},"retryable":{"type":"boolean","description":"Whether the same request may succeed if sent again."},"retry_after_seconds":{"type":"integer","description":"Only on rate_limited."}},"required":["code","message","retryable"]},"Error":{"type":"object","properties":{"error":{"$ref":"#/components/schemas/ErrorDetail"}},"required":["error"]},"LegacyError":{"type":"object","properties":{"error":{"type":"string","description":"A bare sentence rather than the Error object. A few validation refusals still answer this way."},"issues":{"type":"array","items":{"type":"object"},"description":"zod issues, on `invalid post`."},"channels":{"type":"array","items":{"type":"string"},"description":"Handles, on `cannot schedule to a disconnected channel`."},"message":{"type":"string"}},"required":["error"]},"RateLimit":{"type":"object","properties":{"limit":{"type":"integer"},"remaining":{"type":"integer"},"reset_at":{"type":"string","format":"date-time"},"window":{"type":"string","enum":["day"]}},"required":["limit","remaining","window"]},"Platform":{"type":"string","enum":["threads","instagram","youtube","tiktok","facebook"]},"Channel":{"type":"object","properties":{"id":{"type":"string"},"platform":{"$ref":"#/components/schemas/Platform"},"handle":{"type":"string"},"displayName":{"anyOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["connected","needs_reconnect","disconnected"]},"tokenExpiresAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"connectedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["id","platform","handle","status"]},"PublishOptionSpec":{"type":"object","properties":{"key":{"type":"string"},"required":{"type":"boolean"},"values":{"type":"array","items":{"type":"string"}},"description":{"type":"string"}},"required":["key","required","description"]},"PlatformCapabilities":{"type":"object","properties":{"platform":{"$ref":"#/components/schemas/Platform"},"displayName":{"type":"string"},"characterLimit":{"type":"integer"},"media":{"type":"object","properties":{"requirement":{"type":"string","enum":["required","optional","forbidden"]},"minCount":{"type":"integer"},"maxCount":{"type":"integer"},"contentTypes":{"type":"array","items":{"type":"string"}},"maxBytes":{"type":"integer"},"minAspectRatio":{"type":"number"},"maxAspectRatio":{"type":"number"},"video":{"type":"object","description":"What the Platform demands of a video's contents (codecs, fps, dimensions, duration). Absent when it sets none."}},"required":["requirement","minCount","maxCount","contentTypes","maxBytes"]},"publishOptions":{"type":"array","items":{"$ref":"#/components/schemas/PublishOptionSpec"}}},"required":["platform","displayName","characterLimit","media","publishOptions"]},"MediaIntakePath":{"type":"object","properties":{"maxBytes":{"type":"integer"},"howTo":{"type":"string"},"uploadUrlTtlSeconds":{"type":"integer"}},"required":["maxBytes","howTo"]},"CreatePostRequest":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":10000},"channelIds":{"minItems":1,"type":"array","items":{"type":"string"}},"scheduledFor":{"type":"string","format":"date-time"},"variants":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"body":{"type":"string","minLength":1}},"required":["body"]}},"publishOptions":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"mediaIds":{"type":"array","items":{"type":"string"}}},"required":["body","channelIds"]},"EditPostRequest":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":10000},"variants":{"type":"object","propertyNames":{"type":"string","enum":["threads","instagram","youtube","tiktok","facebook"]},"additionalProperties":{"anyOf":[{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":10000}},"required":["body"]},{"type":"null"}]}},"publishOptions":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"mediaIds":{"type":"array","items":{"type":"string"}},"channelIds":{"minItems":1,"type":"array","items":{"type":"string"}}},"additionalProperties":false,"description":"Any non-empty subset of the fields below; an empty object is refused. A variant value of null reverts that platform to the base caption."},"RescheduleRequest":{"type":"object","properties":{"effectiveAt":{"type":"string","format":"date-time"}},"required":["effectiveAt"]},"CreateUploadRequest":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":200},"contentType":{"type":"string","minLength":3,"maxLength":100},"sizeBytes":{"type":"integer","exclusiveMinimum":0,"maximum":536870912},"width":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"height":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["filename","contentType","sizeBytes"]},"PullUploadRequest":{"type":"object","properties":{"source_url":{"type":"string","minLength":1,"maxLength":2048},"filename":{"type":"string","minLength":1,"maxLength":200},"content_type":{"type":"string","minLength":3,"maxLength":100}},"required":["source_url"]},"InlineUploadRequest":{"type":"object","properties":{"data":{"type":"string","minLength":1},"filename":{"type":"string","minLength":1,"maxLength":200},"content_type":{"type":"string","minLength":3,"maxLength":100}},"required":["data","content_type"]},"PostState":{"type":"string","enum":["draft","scheduled","unscheduled","publishing","published","sent_to_drafts","partially_published","failed","cancelled"],"description":"Derived from the Deliveries, never stored. `sent_to_drafts` means a draft-only platform accepted it as a draft the creator must finish; it is not published."},"DeliveryStatus":{"type":"string","enum":["pending","unscheduled","publishing","published","delivered_to_drafts","failed","cancelled"]},"PostSummary":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"state":{"$ref":"#/components/schemas/PostState"},"deliveries":{"type":"integer","description":"A count; read the Post for the Deliveries themselves."},"nextEffectiveAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["id","body","createdAt","state","deliveries","nextEffectiveAt"]},"Attempt":{"type":"object","properties":{"number":{"type":"integer"},"outcome":{"type":"string"},"platformStatus":{"anyOf":[{"type":"string"},{"type":"null"}]},"platformCode":{"anyOf":[{"type":"string"},{"type":"null"}]},"code":{"anyOf":[{"type":"string"},{"type":"null"}]},"message":{"anyOf":[{"type":"string"},{"type":"null"}]},"retryable":{"anyOf":[{"type":"boolean"},{"type":"null"}]},"detail":{},"startedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"finishedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["number","outcome"]},"Delivery":{"type":"object","properties":{"id":{"type":"string"},"status":{"$ref":"#/components/schemas/DeliveryStatus"},"editable":{"type":"boolean"},"effectiveAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"resolvedBody":{"anyOf":[{"type":"string"},{"type":"null"}]},"resolvedOptions":{"anyOf":[{"type":"object","additionalProperties":{"type":"string"}},{"type":"null"}]},"effectiveOptions":{"type":"object","additionalProperties":{"type":"string"}},"retryOfDeliveryId":{"anyOf":[{"type":"string"},{"type":"null"}]},"retryProblem":{"description":"Why a failed Delivery cannot be retried, when it cannot."},"platformPostId":{"anyOf":[{"type":"string"},{"type":"null"}]},"permalink":{"anyOf":[{"type":"string"},{"type":"null"}]},"error":{"anyOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"type":"null"}],"description":"Read from the Attempt the Delivery points at (ADR-0004's error shape). Null if it has not failed."},"failureReason":{"anyOf":[{"type":"string","description":"Debug string. Prefer `error`."},{"type":"null"}]},"publishedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"channel":{"type":"object","properties":{"id":{"type":"string"},"platform":{"$ref":"#/components/schemas/Platform"},"handle":{"type":"string"},"status":{"type":"string"}}},"attempts":{"type":"array","items":{"$ref":"#/components/schemas/Attempt"}}},"required":["id","status","effectiveAt","channel"]},"PostDetail":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"status":{"type":"string","description":"The stored Post status. Prefer `state`."},"state":{"$ref":"#/components/schemas/PostState"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"provenance":{"description":"Which caller created the Post (a Credential, or the dashboard)."},"platforms":{"type":"array","description":"What each Platform will actually receive. `detached` marks a platform carrying a variant.","items":{"type":"object","properties":{"platform":{"$ref":"#/components/schemas/Platform"},"body":{"type":"string"},"detached":{"type":"boolean"},"targeted":{"type":"boolean"},"publishOptions":{"type":"object","additionalProperties":{"type":"string"}}},"required":["platform","body","detached","targeted"]}},"media":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"filename":{"anyOf":[{"type":"string"},{"type":"null"}]},"contentType":{"type":"string"},"status":{"type":"string"},"previewUrl":{"anyOf":[{"type":"string","description":"Short-lived signed URL."},{"type":"null"}]}}}},"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/Delivery"}}},"required":["id","body","state","deliveries"]},"ScheduledPostResponse":{"type":"object","properties":{"post":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"status":{"type":"string","enum":["scheduled"]}},"required":["id","body","status"]},"deliveries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"channelId":{"type":"string"},"channelStatus":{"type":"string"},"effectiveAt":{"type":"string","format":"date-time"},"status":{"$ref":"#/components/schemas/DeliveryStatus"},"scheduled":{"type":"boolean","description":"false means no workflow is armed; re-arm with POST /v1/posts/{id}/schedule."}},"required":["id","channelId","effectiveAt","status","scheduled"]}},"failedToStart":{"type":"array","items":{"type":"string"},"description":"Delivery ids that could not be armed."},"warning":{"type":"string"}},"required":["post","deliveries"]},"DraftPostResponse":{"type":"object","properties":{"id":{"type":"string"},"state":{"type":"string","enum":["draft"]},"channels":{"type":"array","items":{"type":"string"},"description":"Handles, with a leading @."},"channelStates":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"handle":{"type":"string"},"status":{"type":"string"}}}},"summary":{"type":"string","description":"One sentence to relay to a human."},"warning":{"type":"string"}},"required":["id","state","channels","summary"]},"ValidatePostResponse":{"type":"object","properties":{"valid":{"type":"boolean","enum":[true]},"outcome":{"type":"string","enum":["draft","scheduled"],"description":"draft for a Credential without may_schedule; scheduled otherwise."},"effectiveAt":{"type":"string","format":"date-time"},"channels":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"platform":{"$ref":"#/components/schemas/Platform"},"handle":{"type":"string"},"status":{"type":"string"}}}},"warning":{"type":"string"}},"required":["valid","outcome","effectiveAt","channels"]},"PrepareChannelResponse":{"type":"object","properties":{"options":{"type":"array","items":{"$ref":"#/components/schemas/PublishOptionSpec"}},"problems":{"type":"array","description":"What is known to stand between this Channel and publishing. Empty means nothing known, never ready.","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"quota":{"type":"object","properties":{"used":{"type":"integer"},"total":{"type":"integer"}},"required":["used","total"],"description":"Only where the platform volunteers it (today Instagram)."}},"required":["options","problems"]},"ContentRejected":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"problems":{"type":"array","description":"Each broken rule, with its own stable code (media_required, character_limit, ...).","items":{"type":"object","properties":{"platform":{"$ref":"#/components/schemas/Platform"},"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}}}}]},"WebhookEventType":{"type":"string","enum":["delivery.published","delivery.delivered_to_drafts","delivery.failed","channel.needs_reconnect","post.cancelled"]},"CreateWebhookRequest":{"type":"object","properties":{"url":{"type":"string","minLength":1,"maxLength":2048},"events":{"minItems":1,"maxItems":5,"type":"array","items":{"type":"string","enum":["delivery.published","delivery.delivered_to_drafts","delivery.failed","channel.needs_reconnect","post.cancelled"]}},"description":{"type":"string","maxLength":200}},"required":["url"],"additionalProperties":false},"UpdateWebhookRequest":{"type":"object","properties":{"url":{"type":"string","minLength":1,"maxLength":2048},"events":{"minItems":1,"maxItems":5,"type":"array","items":{"type":"string","enum":["delivery.published","delivery.delivered_to_drafts","delivery.failed","channel.needs_reconnect","post.cancelled"]}},"description":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}]},"enabled":{"type":"boolean"}},"additionalProperties":false},"RotateWebhookSecretRequest":{"type":"object","properties":{"grace_period_seconds":{"type":"integer","minimum":0,"maximum":86400}},"additionalProperties":false},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string","description":"https only; checked to resolve to public addresses only, at registration and on every send."},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventType"}},"enabled":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"secret_rotated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time","description":"Until then the replaced secret signs too (a second v1 signature)."},{"type":"null"}]}},"required":["id","url","events","enabled","created_at"]},"WebhookEndpointWithSecret":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpoint"},{"type":"object","properties":{"secret":{"type":"string","description":"whsec_… signing secret. Shown only in this response."},"warning":{"type":"string"}},"required":["secret"]}]},"WebhookEnvelope":{"type":"object","properties":{"id":{"type":"string","description":"evt_… event id. Also the webhook-id header; identical on retries and redeliveries, so dedupe on it."},"type":{"anyOf":[{"$ref":"#/components/schemas/WebhookEventType"},{"type":"string","enum":["webhook.test"]}]},"created_at":{"type":"string","format":"date-time","description":"When the state change was committed."},"data":{"type":"object","description":"Ids and states only: no tokens, captions or media URLs."}},"required":["id","type","created_at","data"]},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string"},"endpoint_id":{"type":"string"},"event":{"$ref":"#/components/schemas/WebhookEnvelope"},"status":{"type":"string","enum":["pending","succeeded","failed"]},"attempts":{"type":"integer"},"next_attempt_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"last_attempt_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"response_status":{"anyOf":[{"type":"integer"},{"type":"null"}]},"response_snippet":{"anyOf":[{"type":"string","description":"At most 256 characters of the response body, control characters removed. Bodies are not stored."},{"type":"null"}]},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}]},"delivered_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"redelivery_of":{"anyOf":[{"type":"string"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"}},"required":["id","endpoint_id","event","status","attempts","created_at"]},"DeliveryPublishedEvent":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"delivery":{"type":"object","properties":{"id":{"type":"string"},"post_id":{"type":"string"},"channel_id":{"type":"string"},"platform":{"$ref":"#/components/schemas/Platform"},"status":{"type":"string","enum":["published"]},"platform_post_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"permalink":{"anyOf":[{"type":"string"},{"type":"null"}]},"published_at":{"type":"string","format":"date-time"}},"required":["id","post_id","channel_id","platform","status"]}},"required":["delivery"]}}}]},"DeliveryDraftedEvent":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"delivery":{"type":"object","properties":{"id":{"type":"string"},"post_id":{"type":"string"},"channel_id":{"type":"string"},"platform":{"$ref":"#/components/schemas/Platform"},"status":{"type":"string","enum":["delivered_to_drafts"]},"platform_post_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"permalink":{"anyOf":[{"type":"string"},{"type":"null"}]},"published_at":{"type":"string","format":"date-time"}},"required":["id","post_id","channel_id","platform","status"]}},"required":["delivery"]}}}]},"DeliveryFailedEvent":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"delivery":{"type":"object","properties":{"id":{"type":"string"},"post_id":{"type":"string"},"channel_id":{"type":"string"},"platform":{"$ref":"#/components/schemas/Platform"},"status":{"type":"string","enum":["failed"]},"error":{"type":"object","properties":{"code":{"anyOf":[{"type":"string"},{"type":"null"}]},"message":{"anyOf":[{"type":"string"},{"type":"null"}]},"retryable":{"anyOf":[{"type":"boolean"},{"type":"null"}]}}}},"required":["id","post_id","channel_id","platform","status"]}},"required":["delivery"]}}}]},"ChannelNeedsReconnectEvent":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"channel":{"type":"object","properties":{"id":{"type":"string"},"platform":{"$ref":"#/components/schemas/Platform"},"handle":{"type":"string"},"status":{"type":"string","enum":["needs_reconnect"]}}}}}}}]},"PostCancelledEvent":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"post":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["cancelled"]}}}}}}}]}},"responses":{"Unauthorized":{"description":"Missing, unknown, expired or revoked credential, or an action that needs a signed-in person.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No such resource in this Workspace, or outside this credential's Channel scope. The two are indistinguishable on purpose.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Daily request allowance spent (1000 per Credential or connected assistant). Retry-After says when it returns.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"rate_limit":{"$ref":"#/components/schemas/RateLimit"}}}]}}},"headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Whole seconds."},"RateLimit-Limit":{"schema":{"type":"integer"}},"RateLimit-Remaining":{"schema":{"type":"integer"}},"RateLimit-Reset":{"schema":{"type":"integer"}}}}}},"security":[{"bearerAuth":[]}]}