{"openapi":"3.0.3","info":{"title":"OmniPosts API","version":"v1","description":"One API serves the web app (session cookie `nsid`) and developers (API key, `Authorization: Bearer omniposts_…`; keys issued before the renames start `jamp_…` or `nsk_…` and keep working). Every call acts for exactly one organization — the session's or the key's. All timestamps are ISO-8601 UTC, all ids are UUIDs (v7, time-ordered).\n**Try it:** this file is served at `/openapi.yaml` and rendered at `/swagger-ui.html`. To use it from Insomnia, import that URL (or `docs/v1/openapi.yaml`) — `docs/v1/INSOMNIA.md` sets up the environments and the API key."},"servers":[{"url":"https://omniposts.dev","description":"Production"},{"url":"http://localhost:8080","description":"Local (`make run`)"}],"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"tags":[{"name":"auth","description":"Sign up, log in, sessions and API keys. Developers authenticate with `Authorization: Bearer omniposts_…`."},{"name":"workspaces","description":"Workspaces (organizations), their members and invites. A person may belong to several and switches the session between them; an API key acts for one."},{"name":"providers","description":"The platform catalog: what each platform supports, its limits, and why a channel may be locked."},{"name":"channels","description":"Connected social accounts and the connect flow (OAuth or credentials)."},{"name":"ideas","description":"Drafts, autosaved by the composer, promoted to a post when ready."},{"name":"groups","description":"Posts: one message fanned out to several channels, with a result per channel. Use `Idempotency-Key` on create."},{"name":"videos","description":"One video to one channel, with typed per-platform settings (`settings.type`). Text posts use groups."},{"name":"media","description":"Uploaded images and video referenced by posts."},{"name":"reports","description":"Delivery (always available) and platform metrics (per-provider, flag-gated)."},{"name":"settings","description":"Consent trail, data export and self-serve account deletion."},{"name":"queue","description":"The organization's weekly posting slots; create a post with `\"queue\": true` to take the next free one."},{"name":"plan","description":"Plan, limits and current usage."},{"name":"webhooks","description":"Outbound lifecycle webhooks, signed with a per-webhook secret."},{"name":"agents","description":"The MCP server for AI agents — the posting API as tools."},{"name":"callbacks","description":"Platform-to-OmniPosts callbacks (Meta). Authenticated by the platform's signature, not by OmniPosts credentials."},{"name":"public","description":"Pages, health and assets outside `/v1`. No authentication."}],"paths":{"/v1/auth/signup":{"post":{"tags":["auth"],"operationId":"signup","summary":"Sign up (creates a person, an organization and a session)","security":[],"parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"},"example":{"email":"ada@example.com","password":"correct horse battery","name":"Ada","organizationName":"Ada Co","acceptTerms":true}}}},"responses":{"201":{"description":"Account created and signed in.","headers":{"Set-Cookie":{"$ref":"#/components/headers/SessionCookie"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/auth/login":{"post":{"tags":["auth"],"operationId":"login","summary":"Log in (starts a session cookie)","security":[],"parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"},"example":{"email":"ada@example.com","password":"correct horse battery"}}}},"responses":{"200":{"description":"Signed in.","headers":{"Set-Cookie":{"$ref":"#/components/headers/SessionCookie"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/auth/logout":{"post":{"tags":["auth"],"operationId":"logout","summary":"Log out (ends the session)","security":[],"parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"204":{"description":"Session deleted and cookie cleared.","headers":{"Set-Cookie":{"$ref":"#/components/headers/SessionCookie"}}},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/me":{"get":{"tags":["auth"],"operationId":"me","summary":"Who am I — the caller's person and organization","responses":{"200":{"description":"The caller. `user` is null for an API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/api-keys":{"get":{"tags":["auth"],"operationId":"listApiKeys","summary":"List API keys","responses":{"200":{"description":"The organization's API keys (secrets never shown again).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyList"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["auth"],"operationId":"createApiKey","summary":"Create an API key (secret shown once)","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewApiKeyRequest"},"example":{"name":"ci"}}}},"responses":{"201":{"description":"Created. `key` is shown exactly once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewApiKey"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/api-keys/{id}":{"delete":{"tags":["auth"],"operationId":"revokeApiKey","summary":"Revoke an API key","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"204":{"description":"Revoked."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/workspaces":{"get":{"tags":["workspaces"],"operationId":"listWorkspaces","summary":"List the workspaces the caller belongs to","description":"A person gets every workspace they are a member of, oldest first; `current` marks the one their session acts for. An API key gets only its own workspace (`role` is null).","responses":{"200":{"description":"The caller's workspaces.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceList"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["workspaces"],"operationId":"createWorkspace","summary":"Create a workspace (the caller becomes its owner)","description":"The new workspace starts on the plan a sign-up gets, with the caller as its only owner. The session is not switched to it: call `POST /v1/workspaces/{id}/switch`. A person may own at most 10 workspaces (else 422 `workspace_limit_reached`). The name is trimmed and must be 1–80 characters (else 400).","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRequest"},"example":{"name":"Marketing"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/workspaces/{id}":{"get":{"tags":["workspaces"],"operationId":"getWorkspace","summary":"One workspace (members, or an API key of that workspace)","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["workspaces"],"operationId":"renameWorkspace","summary":"Rename a workspace (owner only)","description":"The name is trimmed and must be 1–80 characters (else 400).","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceRequest"},"example":{"name":"Marketing team"}}}},"responses":{"200":{"description":"Renamed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["workspaces"],"operationId":"deleteWorkspace","summary":"Delete a workspace and all its data (owner only)","description":"Needs `{\"confirm\": \"<the workspace's name>\"}` exactly (else 422 `confirmation_required`). Refused with 409 `last_workspace` when it is the caller's only workspace, and with 409 `members_remain` while anyone else is still a member (remove them first). On 202 the workspace's channels, posts, media, API keys and webhooks are deleted the way account deletion deletes them (immediately: credentials, sessions, keys; then everything else), and the caller's sessions in it move to their oldest other workspace.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteWorkspaceRequest"},"example":{"confirm":"Marketing"}}}},"responses":{"202":{"description":"Deletion accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletionAccepted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/workspaces/{id}/switch":{"post":{"tags":["workspaces"],"operationId":"switchWorkspace","summary":"Make this workspace the one the session acts for","description":"Rebinds the calling session (the `nsid` cookie) to the workspace; every later call made with that cookie acts for it. Other sessions of the same person are unaffected. 404 when the caller is not a member.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"200":{"description":"Switched; the caller as `GET /v1/me` now shows them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/workspaces/{id}/members":{"get":{"tags":["workspaces"],"operationId":"listMembers","summary":"List the members of a workspace","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Every member, in the order they joined.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/workspaces/{id}/members/{userId}":{"patch":{"tags":["workspaces"],"operationId":"changeMemberRole","summary":"Change a member's role (owner only)","description":"A workspace always keeps at least one owner (else 409 `last_owner`). Demoting an owner revokes the invites they created that are still pending.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/UserId"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberRoleRequest"},"example":{"role":"owner"}}}},"responses":{"200":{"description":"The member with their new role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Member"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["workspaces"],"operationId":"removeMember","summary":"Remove a member (owner), or leave (yourself)","description":"An owner may remove anyone; anyone may remove themselves (leave). Refused with 409 `last_owner` for the last owner, and with 409 `last_workspace` when someone tries to leave their only workspace. Access stops at once: the person's sessions in this workspace move to their oldest other workspace; the API keys they created in it are revoked, the channels they connected there are disconnected (tokens deleted; `channel.disconnected` webhooks) — so posts still scheduled to those channels fail with `failure.code` `disconnected` (class `reconnect`) until someone reconnects them — their open connects there are dropped (a connect finishing afterwards fails with `not_a_member`) and their pending invites revoked. A person removed from their only workspace gets a new empty one of their own, so they can still sign in.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/UserId"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"204":{"description":"Removed."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/workspaces/{id}/invites":{"get":{"tags":["workspaces"],"operationId":"listInvites","summary":"List pending invites (owner only)","description":"Invites not yet accepted, revoked or expired, newest first. The link is never shown again.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Pending invites.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["workspaces"],"operationId":"createInvite","summary":"Invite someone (owner only; the link is shown once)","description":"Returns a one-time `inviteUrl` (`<public base URL>/app/invite/<token>`, a page of the web app where the person signs in and accepts). OmniPosts sends no email: share the link yourself. With `email`, only the account with that address (case-insensitive) can accept it; sign-up does not verify addresses, so the link itself is the secret. `role` defaults to `member`. A member invite works for 7 days. An `owner` invite must name an `email` (else 400) and works for 48 hours. An invite stops working when its creator is no longer an owner of the workspace. At most 50 pending invites per workspace (else 422 `invite_limit_reached`). Answers carry `Cache-Control: no-store`.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteRequest"},"example":{"email":"grace@example.com","role":"member"}}}},"responses":{"201":{"description":"Created. `inviteUrl` is shown exactly once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewInvite"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/workspaces/{id}/invites/{inviteId}":{"delete":{"tags":["workspaces"],"operationId":"revokeInvite","summary":"Revoke a pending invite (owner only)","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/InviteId"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"204":{"description":"Revoked; the link no longer works."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/invites/{token}":{"get":{"tags":["workspaces"],"operationId":"previewInvite","summary":"What an invite link is for, before accepting it","description":"For the signed-in person holding the link. `status` says whether it can still be accepted (`revoked` also when its creator is no longer an owner). The invited address is never shown, and `invitedBy` is the inviter's display name only; `emailMatches` says whether the address is the caller's. 404 for an unknown token. Answers carry `Cache-Control: no-store`.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/InviteToken"}],"responses":{"200":{"description":"The invite.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitePreview"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/invites/{token}/accept":{"post":{"tags":["workspaces"],"operationId":"acceptInvite","summary":"Join the workspace an invite is for","description":"Single use. An invite with an email can be accepted only by the account with that address (else 403 `invite_email_mismatch`). Expired, used or revoked → 410 (`invite_expired`, `invite_used`, `invite_revoked` — also when its creator is no longer an owner of the workspace). Accepting when already a member returns the workspace and leaves the invite unused. The session is not switched to the workspace. Answers carry `Cache-Control: no-store`.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/InviteToken"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"200":{"description":"Joined (or already a member).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"410":{"$ref":"#/components/responses/Gone"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/providers":{"get":{"tags":["providers"],"operationId":"listProviders","summary":"List platforms and their capabilities","responses":{"200":{"description":"The capability registry (D-3). The composer renders from this.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProviderList"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/channels":{"get":{"tags":["channels"],"operationId":"listChannels","summary":"List connected channels","responses":{"200":{"description":"Live (connected) channels.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelList"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/channels/{id}":{"delete":{"tags":["channels"],"operationId":"disconnectChannel","summary":"Disconnect a channel","description":"Deletes tokens and cached platform data; our own publishing history stays.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"204":{"description":"Disconnected."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/connect-sessions":{"post":{"tags":["channels"],"operationId":"startConnect","summary":"Start connecting a channel","description":"With a session (a person): credential providers connect synchronously (`status: completed`, `channel` set); OAuth providers answer `status: pending` with an `authorizationUrl` to send the browser to.\nWith an API key (an agent): a *handoff*. The answer is `status: pending` with a one-time `handoffUrl` (shown only here). A signed-in member of the workspace opens it in a browser, is sent to the platform, and finishes there; the key polls `GET /v1/connect-sessions/{id}` until `completed` (or `failed` with `error` saying why, or `expired` after an hour). Credential providers cannot be handed off (422 `handoff_not_available`): a person types those in the app. Needs the `channels:manage` scope.\nA refused start answers 422; `error.code` is the failure's code and `details.failure` the failure: `unknown_provider`, `unknown_auth_method`, `channel_switched_off`, `not_configured`, `plan_limit_reached`, `handoff_not_available`, `user_required`, a platform's own refusal, `not_a_member` (the person was removed from, or left, the workspace while connecting) or `workspace_deleted` (the workspace was deleted while connecting). An OAuth session that fails later carries the same failures as one sentence in `ConnectSession.error`.","parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartConnectRequest"},"example":{"providerId":"exact","fields":{"token":"t1"}}}}},"responses":{"201":{"description":"Session started (and, for credential providers, completed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectSession"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Connect refused (unknown platform, platform switched off or not configured, bad credential). `error.code` is the failure code; `details.failure` the full Failure. For a locked platform (`channel_switched_off`, `not_configured`) the message only says it is coming soon (\"X is coming soon to OmniPosts.\"); why it is locked is `lockedReason` on `GET /v1/providers`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/connect-sessions/{id}":{"get":{"tags":["channels"],"operationId":"getConnectSession","summary":"Get a connect session's state","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The session; the wizard polls this after the OAuth redirect.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectSession"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/oauth/callback":{"get":{"tags":["channels"],"operationId":"oauthCallback","summary":"OAuth return from a platform (browser redirect)","description":"The platform redirects the browser here. It must arrive in the browser session (nsid cookie) of the user who started the connect, otherwise the session fails with `different_browser`. Always a redirect: to `/app/connect/session/{id}` when the state matched a pending session (whatever the outcome), `/app/channels?connect=expired` when it did not, `/app/channels?connect=failed` on an internal error.","security":[],"parameters":[{"name":"code","in":"query","required":false,"schema":{"type":"string"}},{"name":"state","in":"query","required":false,"schema":{"type":"string"}},{"name":"error","in":"query","required":false,"schema":{"type":"string"}},{"name":"iss","in":"query","required":false,"schema":{"type":"string"},"description":"RFC 9207 issuer of the authorization response. Connectors whose servers send it (Bluesky / AT Protocol) refuse a callback whose issuer is not the one the flow started with."}],"responses":{"302":{"description":"Back to the SPA.","headers":{"Location":{"$ref":"#/components/headers/Location"}}}}}},"/v1/ideas":{"post":{"tags":["ideas"],"operationId":"createIdea","summary":"Create a draft","parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdeaRequest"},"example":{"body":"first draft"}}}},"responses":{"201":{"description":"Draft created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Idea"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"tags":["ideas"],"operationId":"listIdeas","summary":"List drafts","description":"Newest first; discarded drafts are never listed.","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["draft","promoted"]}},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"A page of drafts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdeaPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/ideas/{id}":{"get":{"tags":["ideas"],"operationId":"getIdea","summary":"Get a draft","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The draft.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Idea"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["ideas"],"operationId":"updateIdea","summary":"Update a draft (autosave)","description":"Autosave. Rate-limited in the AUTOSAVE class. Omitted fields are unchanged. `409 stale_write` / `not_editable` carry `details.current` (the Idea as it now is).","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/IfMatch"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdeaRequest"},"example":{"body":"second draft"}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Idea"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["ideas"],"operationId":"discardIdea","summary":"Discard a draft","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"204":{"description":"Discarded (status → discarded). Discarding twice is a no-op."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/ideas/{id}/promote":{"post":{"tags":["ideas"],"operationId":"promoteIdea","summary":"Publish or schedule a draft","description":"Turns a draft into a post group with exactly the semantics of POST /v1/groups (207, idempotency). Per-channel tweaks saved in `targetDrafts` apply unless the request overrides them.","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/IdempotencyKey"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoteRequest"},"example":{"targets":[{"channelId":"0192a3b4-0000-7000-8000-000000000001"}]}}}},"responses":{"200":{"$ref":"#/components/responses/GroupReplayed"},"201":{"$ref":"#/components/responses/GroupCreated"},"207":{"$ref":"#/components/responses/GroupPartiallyCreated"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/groups":{"post":{"tags":["groups"],"operationId":"createGroup","summary":"Publish or schedule a post to channels","description":"Fan-out publish (D-5). 201 when every target was accepted, 207 when some were rejected at creation (`rejections` lists them), 200 + `Idempotent-Replayed: true` for a replay of the same Idempotency-Key and request, 422 `nothing_to_publish` when none was accepted. `queue: true` places the group in the next free slot of the posting schedule (`GET /v1/queue`) instead of `scheduledFor`; 422 `queue_empty` when the schedule has no times, `queue_full` when every slot in the next 12 weeks is taken. 422 `plan_limit_reached` (details.limit = posts_per_month) when the plan's monthly posts are used up.\n`posting_limit_reached` means the user has used up their posting limit: a cap on channel targets (a post to 3 channels uses 3), set per user by the operator, per UTC day or in total, and applying across all the user's workspaces. It answers **429** for a daily limit when the post would fit after the reset: `Retry-After` is the seconds until 00:00 UTC, `X-RateLimit-Remaining` is 0 and `details.resetsAt` says when. It answers **422** for a lifetime limit, or for a post larger than the limit itself (it can never fit); waiting does not help. `details` has `period`, `maxTargets`, `used`, `requested` and `remaining`. A post made with an API key counts against the key's creator. The plan's monthly post limit (`plan_limit_reached`) applies in addition.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupRequest"},"example":{"body":"hello","targets":[{"channelId":"0192a3b4-0000-7000-8000-000000000001"}]}}}},"responses":{"200":{"$ref":"#/components/responses/GroupReplayed"},"201":{"$ref":"#/components/responses/GroupCreated"},"207":{"$ref":"#/components/responses/GroupPartiallyCreated"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"tags":["groups"],"operationId":"listGroups","summary":"List posts","description":"Calendar. Groups whose time (scheduledFor, else createdAt) is in [from, to), newest first by that time (ties by newest id).","parameters":[{"name":"status","in":"query","required":false,"schema":{"$ref":"#/components/schemas/GroupStatus"}},{"$ref":"#/components/parameters/Channel"},{"$ref":"#/components/parameters/From"},{"$ref":"#/components/parameters/To"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"A page of groups.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/groups/validate":{"post":{"tags":["groups"],"operationId":"validateGroup","summary":"Check a post before sending it (dry run)","description":"The same body as POST /v1/groups, the same per-target checks (channel connected, platform switched on, account healthy, access not ending before the schedule, media present, text and settings within the platform's limits) and a read-only look at the plan's monthly posts and the user's posting limit — but nothing is written, no Idempotency-Key is consumed and no limit is counted. Always 200. `ok` means the real create would answer 201 or 207: at least one target in `accepted` and no entry in `problems`. `rejections` mirrors the create's; `problems` lists group-wide refusals (`plan_limit_reached`, `posting_limit_reached`). The answer can go stale: a limit may fill up before the create.","parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupRequest"},"example":{"body":"hello","targets":[{"channelId":"0192a3b4-0000-7000-8000-000000000001"}]}}}},"responses":{"200":{"description":"The verdict; nothing was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupValidation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/groups/{id}":{"get":{"tags":["groups"],"operationId":"getGroup","summary":"Get a post with per-channel results","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The group with its targets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Group"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["groups"],"operationId":"editGroup","summary":"Edit a scheduled post","description":"Edits body and/or time while no target has been claimed. 409 `stale_write` (If-Match mismatch; `details.current` is the Group), 409 `already_dispatched`, 422 `invalid_edit` (the new body fails a target's validation).","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/IfMatch"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupEditRequest"},"example":{"body":"edited"}}}},"responses":{"200":{"description":"Edited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Group"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/groups/{id}/cancel":{"post":{"tags":["groups"],"operationId":"cancelGroup","summary":"Cancel a scheduled post","description":"Cancel wins only before the worker claims a target (FC §5.2).","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"200":{"description":"Which targets were cancelled and which were not (and why).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/groups/{id}/retry":{"post":{"tags":["groups"],"operationId":"retryGroup","summary":"Retry the failed channels of a post","description":"Re-queues only failed targets whose `failure.retryable` is true; never an unconfirmed one. Targets that were rejected when the post was created count against the user's posting limit only now, when they are re-queued, so this can answer `posting_limit_reached` (429 or 422, as for `POST /v1/groups`) and then re-queues nothing.","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"200":{"description":"The group after re-queueing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Group"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/targets":{"get":{"tags":["groups"],"operationId":"listTargets","summary":"List per-channel deliveries","description":"Outcome inbox, one row per target, newest first by sent time (`publishedAt`, else the group's `scheduledFor`, else creation; ties by newest id). `from`/`to` filter on the target's last change.","parameters":[{"name":"status","in":"query","required":false,"schema":{"$ref":"#/components/schemas/TargetStatus"}},{"$ref":"#/components/parameters/Channel"},{"$ref":"#/components/parameters/From"},{"$ref":"#/components/parameters/To"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"A page of inbox rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/videos":{"post":{"tags":["videos"],"operationId":"createVideo","summary":"Publish or schedule a video to one channel","description":"Uploads a video to one channel. Upload the file first with `POST /v1/media` and pass its id. `settings.type` names the platform (`youtube` or `tiktok`) and decides which other settings keys are allowed; unknown keys are a 400. The channel must be on that platform (`tiktok` settings fit both TikTok and TikTok for Business channels), else 422 `settings_type_mismatch`.\nUnderneath this is a post with one target, so it shares everything with `POST /v1/groups`: `scheduledFor` (omit to publish now), `Idempotency-Key` (200 + `Idempotent-Replayed` on a replay, 409 `idempotency_conflict` on a different body), plan and posting limits, and 422 `nothing_to_publish` when the platform's rules refuse the video before it is sent (`details.rejections[0].failure` says why). The returned `id` also works on `/v1/groups/{id}` (cancel, retry, edit).","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoRequest"},"example":{"channelId":"0192a3b4-0000-7000-8000-000000000001","mediaId":"0192a3b4-0000-7000-8000-000000000002","description":"In this video I show how to post with the OmniPosts API.\n\n0:00 Intro","settings":{"type":"youtube","title":"How to post with the OmniPosts API","visibility":"private","madeForKids":false,"tags":["omniposts","api","tutorial"]}}}}},"responses":{"200":{"description":"Replay of an earlier request with the same Idempotency-Key.","headers":{"Idempotent-Replayed":{"$ref":"#/components/headers/IdempotentReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Video"}}}},"201":{"description":"Accepted; scheduled, or already on its way when `scheduledFor` is omitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Video"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/videos/{id}":{"get":{"tags":["videos"],"operationId":"getVideo","summary":"Get a video's delivery state","description":"Poll this after creating a video: `status` moves to `published` (with `externalUrl`) or `failed`/`unknown` (with `failure`). Any post with exactly one target can be read here; a post fanned out to several channels is a 404 (read it on `/v1/groups/{id}`).","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The video.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Video"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/media":{"get":{"tags":["media"],"operationId":"listMedia","summary":"List media","description":"The organization's media, newest first.","parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"A page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["media"],"operationId":"uploadMedia","summary":"Upload media (multipart or from a link)","description":"Send the file as `multipart/form-data`, or send `application/json` with a public `https` `url` and OmniPosts downloads it (at most 50 MB; private and internal addresses are refused, up to three redirects are followed, each checked the same way). Either way the type is sniffed from the bytes (JPEG, PNG, GIF, WebP, MP4/QuickTime), never taken from the client or the link. `checks` holds, per enabled provider, the objections that provider has to this file (empty list = fine). Rate-limited in the MEDIA class.","parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"altText":{"type":"string"}}}},"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2000,"description":"A public https link to the file."},"altText":{"type":"string","nullable":true,"maxLength":2000}}},"example":{"url":"https://cdn.example.com/bowls.jpg","altText":"Six bowls on a shelf"}}}},"responses":{"201":{"description":"Stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Media"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/media/{id}":{"delete":{"tags":["media"],"operationId":"deleteMedia","summary":"Delete media","description":"Removes the file. 404 for another organization's id. 409 `media_in_use` while a draft or a post that has not finished (anything but published or cancelled) still names it; a published post keeps working, the platform has its own copy. Rate-limited in the MEDIA class.","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"204":{"description":"Deleted."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["media"],"operationId":"updateMedia","summary":"Update media (alt text)","description":"Edit the image description (`altText`; null or blank clears it, at most 2000 characters, the longest any platform accepts — each platform's own shorter limit still applies when a post is checked). Returns the media object as POST /v1/media does, with `checks` re-run against the new description. Owner only (404 for another organization's id). 409 `already_sent` once any post using this media has been dispatched (publishing, awaiting the platform, unknown or published): what the platform received is settled, so upload the file again to use a new description. Rate-limited in the MEDIA class.","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["altText"],"properties":{"altText":{"type":"string","nullable":true,"maxLength":2000}}},"example":{"altText":"A stack of six hand-thrown bowls on a wooden shelf"}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Media"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/media/{id}/content":{"get":{"tags":["media"],"operationId":"mediaContent","summary":"Download media content","description":"The bytes, to the owning organization only. Served with `Content-Security-Policy: default-src 'none'; sandbox`.","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The file, with its sniffed type.","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/gif":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"video/mp4":{"schema":{"type":"string","format":"binary"}},"video/quicktime":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/reports/delivery":{"get":{"tags":["reports"],"operationId":"deliveryReport","summary":"Delivery report","description":"Panel A — computed from our tables only. Default window: the last 30 days; `from` must be before `to`; at most 400 days. Ratios and percentiles with nothing to measure are null, never 0.","parameters":[{"$ref":"#/components/parameters/From"},{"$ref":"#/components/parameters/To"},{"$ref":"#/components/parameters/Channel"}],"responses":{"200":{"description":"The report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryReport"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/metrics/summary":{"get":{"tags":["reports"],"operationId":"metricsSummary","summary":"Metrics summary across channels","description":"One row per published target (publishedAt in window), newest first. Same window rules as delivery. `metrics=extended` returns all twelve post metrics per row (the order of `ExtendedMetricName`) instead of the first-cut five; the default is unchanged.","parameters":[{"$ref":"#/components/parameters/From"},{"$ref":"#/components/parameters/To"},{"$ref":"#/components/parameters/Channel"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"},{"name":"metrics","in":"query","required":false,"schema":{"type":"string","enum":["default","extended"],"default":"default"}}],"responses":{"200":{"description":"A page of rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MetricsPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/metrics/post/{targetId}":{"get":{"tags":["reports"],"operationId":"metricsPost","summary":"Metrics for one delivery","description":"Cumulative series for one published target. `firstSixHours`: the hourly buckets of the first six hours; `lifetime`: every bucket (hourly for 48 h, then daily).","parameters":[{"name":"targetId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cumulative series for one target.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostSeries"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/metrics/post/{targetId}/extended":{"get":{"tags":["reports"],"operationId":"metricsPostExtended","summary":"Extended metrics for one delivery","description":"As `/v1/metrics/post/{targetId}`, over every post metric (reactions, comments, reposts, quotes, shares, saves, zaps, views, impressions, reach, link_clicks, profile_visits) in that order. A metric the platform cannot report is `not_supported` with its reason, never an empty series of zeros.","parameters":[{"name":"targetId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cumulative series for one target, every metric.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtendedPostSeries"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/reports/performance":{"get":{"tags":["reports"],"operationId":"performanceReport","summary":"Performance report","description":"Platform counters rolled up per channel for published targets in the window (same window rules as delivery). A total is the sum of each post's latest cumulative value over the posts that have one (`postsObserved`). The engagement rate is interactions ÷ impressions (else views), computed only over posts carrying that denominator; with no denominator it is null, never 0. `topPosts`: up to 5 posts by interactions.","parameters":[{"$ref":"#/components/parameters/From"},{"$ref":"#/components/parameters/To"},{"$ref":"#/components/parameters/Channel"}],"responses":{"200":{"description":"The report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PerformanceReport"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/reports/audience":{"get":{"tags":["reports"],"operationId":"audienceReport","summary":"Audience report","description":"Daily account-level samples (followers, following, posts, likes received) per channel — history the platforms themselves do not keep. `change` is last − first in the window, null with fewer than two points. `channel` that is not this organization's is 404.","parameters":[{"$ref":"#/components/parameters/From"},{"$ref":"#/components/parameters/To"},{"$ref":"#/components/parameters/Channel"}],"responses":{"200":{"description":"The report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudienceReport"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/settings/consents":{"get":{"tags":["settings"],"operationId":"listConsents","summary":"List my consents","x-requires-user":true,"responses":{"200":{"description":"The signed-in user's consent trail, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentList"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/queue":{"get":{"tags":["queue"],"operationId":"getQueue","summary":"Get the posting queue","description":"The posting schedule: weekly local times in one time zone, plus the next free slot instants (up to 5). Empty slots by default.","responses":{"200":{"description":"The schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Queue"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["queue"],"operationId":"replaceQueue","summary":"Replace the posting queue slots","description":"Replaces the whole schedule. Duplicate times collapse to one; at most 56 times.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueueRequest"},"example":{"timezone":"Asia/Kolkata","slots":[{"day":"mon","time":"09:30"},{"day":"thu","time":"18:00"}]}}}},"responses":{"200":{"description":"The schedule as stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Queue"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/plan":{"get":{"tags":["plan"],"operationId":"getPlan","summary":"Get plan, limits and usage","description":"The organization's plan, current usage and every plan's limits. `billing` is `early_access` while there is no billing: changing plan is free.","responses":{"200":{"description":"The plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanState"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["plan"],"operationId":"changePlan","summary":"Change plan (owner only)","x-requires-user":true,"description":"Switches plan. An owner only (403 `forbidden` for a member or an API key). A plan marked `beta` cannot be switched onto (422 `plan_in_beta`; people join the waitlist on the pricing page instead), but an organization already on one keeps it and may leave it. Lowering a plan never disconnects a channel or cancels a post; it refuses new ones above the limit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanChangeRequest"},"example":{"plan":"creator"}}}},"responses":{"200":{"description":"The plan after the change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanState"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/settings/export":{"get":{"tags":["settings"],"operationId":"exportAccount","summary":"Export all organization data (JSON)","description":"The whole organization as one JSON attachment. Its own budget: 3 per organization per hour (429 with Retry-After).","x-requires-user":true,"responses":{"200":{"description":"The export.","headers":{"Content-Disposition":{"required":true,"description":"attachment; filename=\"omniposts-export-<yyyy-mm-dd>.json\"","schema":{"type":"string","pattern":"^attachment; filename=\"omniposts-export-\\d{4}-\\d{2}-\\d{2}\\.json\"$"}},"Cache-Control":{"required":true,"schema":{"type":"string","enum":["no-store"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Export"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/settings/delete-account":{"post":{"tags":["settings"],"operationId":"deleteAccount","summary":"Delete your account (you, the person)","description":"Needs `{\"confirm\": \"DELETE\"}` exactly (else 422 `confirmation_required`). Deletes the signed-in person, not a workspace: they leave every workspace they share with others (their API keys, the channels they connected, pending connects and invites there are revoked or disconnected; posts still scheduled to those channels fail as `disconnected`), every workspace where they are the only member is deleted with all its data, and their account is erased (consents are withdrawn and kept as records). Nobody else's account or membership changes. Refused with 409 `sole_owner_with_members`, before anything changes, while they are the only owner of a workspace that has other members: make someone else an owner, or remove the others, first. On 202 the session cookie is cleared and every session stops working at once. The confirmation code's receipt completes when the deleted workspaces' data is gone. A rare 409 `try_again` means their workspaces changed mid-way and nothing was deleted.","x-requires-user":true,"parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAccountRequest"},"example":{"confirm":"DELETE"}}}},"responses":{"202":{"description":"Deletion accepted.","headers":{"Set-Cookie":{"$ref":"#/components/headers/SessionCookie"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletionAccepted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/webhooks":{"get":{"tags":["webhooks"],"operationId":"listWebhooks","summary":"List webhooks","responses":{"200":{"description":"Registered endpoints (secrets never shown again).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookList"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["webhooks"],"operationId":"createWebhook","summary":"Create a webhook (secret shown once)","description":"`https://` to a public host only (no private/loopback/link-local addresses, no internal names, ports 443/8443); `events` ⊆ the four event types. Refusals are 422 `invalid_webhook`.","parameters":[{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookRequest"},"example":{"url":"https://hooks.example.com/omniposts","events":["post.published"]}}}},"responses":{"201":{"description":"Registered. `secret` is shown exactly once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreated"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/webhooks/{id}":{"delete":{"tags":["webhooks"],"operationId":"deleteWebhook","summary":"Delete a webhook","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/ClientHeader"},{"$ref":"#/components/parameters/LegacyClientHeader"}],"responses":{"204":{"description":"Deleted, with its delivery log."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/webhooks/{id}/deliveries":{"get":{"tags":["webhooks"],"operationId":"listWebhookDeliveries","summary":"List a webhook's deliveries","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"The endpoint's delivery log, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/webhooks/meta/data-deletion":{"post":{"tags":["callbacks"],"operationId":"metaDataDeletion","summary":"Meta data-deletion callback","description":"Meta's data-deletion callback. Idempotent per user_id: a repeat re-runs the purge and returns the original confirmation code. Any signature failure is a bare 400; 503 when META_APP_SECRET is not configured.","security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/SignedRequestForm"}}}},"responses":{"200":{"description":"Purged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MetaDeletionReceipt"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"503":{"$ref":"#/components/responses/NotConfigured"}}}},"/webhooks/meta/deauthorize":{"post":{"tags":["callbacks"],"operationId":"metaDeauthorize","summary":"Meta deauthorize callback","security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/SignedRequestForm"}}}},"responses":{"200":{"description":"Channels of that Meta user disconnected.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false}}}},"400":{"$ref":"#/components/responses/BadRequest"},"503":{"$ref":"#/components/responses/NotConfigured"}}}},"/data-deletion/status":{"get":{"tags":["callbacks"],"operationId":"dataDeletionStatus","summary":"Data-deletion request status","description":"Public, server-rendered status of a deletion request (Meta's confirmation link, and self-serve account deletion).","security":[],"parameters":[{"name":"code","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Html"},"404":{"$ref":"#/components/responses/Html"}}}},"/oauth/jwks.json":{"get":{"tags":["channels"],"operationId":"oauthClientJwks","summary":"Public keys this server signs OAuth client assertions with","description":"A JWK Set (RFC 7517) of the public halves of the keys configured in `OMNIPOSTS_OAUTH_CLIENT_KEYS`, used by authorization servers to verify `private_key_jwt` client assertions (AT Protocol OAuth). Public members only. Empty `keys` when none are configured.","security":[],"responses":{"200":{"description":"The JWK Set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Jwks"}}}}}}},"/oauth/clients/{providerId}.json":{"get":{"tags":["channels"],"operationId":"oauthClientMetadata","summary":"OAuth client metadata document for one platform","description":"For platforms whose `client_id` is the URL of a client metadata document (AT Protocol OAuth), the document itself. 404 for platforms that register clients another way.","security":[],"parameters":[{"name":"providerId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The client metadata document.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/healthz":{"get":{"tags":["public"],"operationId":"health","summary":"Health check","security":[],"responses":{"200":{"description":"Up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"503":{"description":"The database is unreachable, or (where worker checks are on) no live worker. A broken connector never causes a 503: status stays 200 and the connectors block names it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthDown"}}}}}}},"/healthz/connectors":{"get":{"tags":["public"],"operationId":"connectorHealth","summary":"Connector health","description":"Per-provider connector health for operators (docs/v1/RUNBOOK-CONNECTORS.md). Public like /healthz: aggregates only — no channel, organization or post data, no secrets. Registry order; `state` is broken | degraded | ok | unknown (never probed and nothing else to say).","security":[],"responses":{"200":{"description":"Connector summary plus one entry per provider.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorHealth"}}}}}}},"/connect/{sessionId}":{"get":{"tags":["public"],"operationId":"connectReturn","summary":"Connect return page","description":"Legacy OAuth return path; forwards into the SPA's connect wizard.","security":[],"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"302":{"description":"To /app/connect/session/{sessionId}.","headers":{"Location":{"$ref":"#/components/headers/Location"}}}}}},"/media/{org}/{file}":{"get":{"tags":["public"],"operationId":"publicMedia","summary":"Public media file","description":"Public media for URL-ingest platforms (D-12): `<orgUuid>/<mediaUuid>.<ext>`, unguessable, never listed. 404 when media.public is off, the key is unknown or malformed.","security":[],"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"}},{"name":"file","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f-]{36}\\.[a-z0-9]{2,4}$"}}],"responses":{"200":{"description":"The file.","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/gif":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"video/mp4":{"schema":{"type":"string","format":"binary"}},"video/quicktime":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Not found (no body)."}}}},"/site/{file}":{"get":{"tags":["public"],"operationId":"siteAsset","summary":"Site asset","description":"Static files the public pages reference (the share card, icons, the self-hosted Caprasimo and Figtree web fonts, and field.js, the landing hero's word field). Cached for a day. 404 for any other name.","security":[],"parameters":[{"name":"file","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9-]+\\.[a-z0-9]{2,5}$"}}],"responses":{"200":{"description":"The image, font or script (png, woff2 or js; any other extension is a 404).","content":{"image/png":{"schema":{"type":"string","format":"binary"}},"font/woff2":{"schema":{"type":"string","format":"binary"}},"text/javascript":{"schema":{"type":"string"}}}},"404":{"description":"Not found (no body)."}}}},"/":{"get":{"tags":["public"],"operationId":"pageHome","summary":"Home page","description":"Public pages and the crawler files carry an `ETag` and are `Cache-Control: no-cache`, so a revalidation with `If-None-Match` answers 304 until the page changes; text answers are compressed.","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"},"304":{"description":"Not modified: the If-None-Match value is the current ETag."}}}},"/privacy":{"get":{"tags":["public"],"operationId":"pagePrivacy","summary":"Privacy page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/terms":{"get":{"tags":["public"],"operationId":"pageTerms","summary":"Terms page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/data-deletion":{"get":{"tags":["public"],"operationId":"pageDataDeletion","summary":"Data deletion page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/grievance":{"get":{"tags":["public"],"operationId":"pageGrievance","summary":"Grievance page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/security":{"get":{"tags":["public"],"operationId":"pageSecurity","summary":"Security page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/dpa":{"get":{"tags":["public"],"operationId":"pageDpa","summary":"DPA page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/subprocessors":{"get":{"tags":["public"],"operationId":"pageSubprocessors","summary":"Subprocessors page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/contact":{"get":{"tags":["public"],"operationId":"pageContact","summary":"Contact page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/refund":{"get":{"tags":["public"],"operationId":"pageRefund","summary":"Refund page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/waitlist":{"post":{"tags":["public"],"operationId":"joinWaitlist","summary":"Join the beta waitlist","description":"The pricing page's form. Takes an email address and which beta plan, nothing else (no password, no account). Always answers 303 back to `/pricing?waitlist=ok|invalid|busy#waitlist`, which shows a notice; a reload never re-submits. Signing up twice is a quiet no-op. Limited per client address (`busy`).","security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"email":{"type":"string","maxLength":254},"plan":{"type":"string","enum":["either","creator","pro"],"default":"either"},"website":{"type":"string","description":"Honeypot: left empty by people."}}}}}},"responses":{"303":{"description":"Redirect to the pricing page's waitlist section.","headers":{"Location":{"$ref":"#/components/headers/Location"}}}}}},"/pricing":{"get":{"tags":["public"],"operationId":"pagePricing","summary":"Pricing page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/about":{"get":{"tags":["public"],"operationId":"pageAbout","summary":"About page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/support":{"get":{"tags":["public"],"operationId":"pageSupport","summary":"Support page","description":"Where to find answers and which email subject to use.","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/channels":{"get":{"tags":["public"],"operationId":"pageChannels","summary":"Channels page","description":"Which channels work today and which are waiting, from the live feature flags.","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/bluesky":{"get":{"tags":["public"],"operationId":"pageBluesky","summary":"Bluesky page","description":"What OmniPosts posts to Bluesky and how to connect it. Generated from app/src/main/frontend/src/site. Served only while the Bluesky channel is available; otherwise 302 to /channels (temporary: it comes back).","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"},"302":{"description":"The channel is not available right now: Location /channels."}}}},"/mastodon":{"get":{"tags":["public"],"operationId":"pageMastodon","summary":"Mastodon page","description":"What OmniPosts posts to Mastodon and how to connect it. Generated from app/src/main/frontend/src/site. Served only while the Mastodon channel is available; otherwise 302 to /channels (temporary: it comes back).","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"},"302":{"description":"The channel is not available right now: Location /channels."}}}},"/telegram":{"get":{"tags":["public"],"operationId":"pageTelegram","summary":"Telegram page","description":"What OmniPosts posts to Telegram channels and how to connect one. Generated from app/src/main/frontend/src/site. Served only while the Telegram channel is available; otherwise 302 to /channels (temporary: it comes back).","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"},"302":{"description":"The channel is not available right now: Location /channels."}}}},"/developers":{"get":{"tags":["public"],"operationId":"pageDevelopers","summary":"Developers page","description":"The API landing page: a first request and links to the reference, idempotency, webhooks, MCP and this spec.","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/changelog":{"get":{"tags":["public"],"operationId":"pageChangelog","summary":"Changelog page","description":"Dated user-facing changes, newest first, each linking to the page that documents it.","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/status":{"get":{"tags":["public"],"operationId":"pageStatus","summary":"Status page","description":"The checks behind /healthz as a readable page, filled on each request and never cached. Marked noindex and left out of the sitemap.","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/product":{"get":{"tags":["public"],"operationId":"pageProduct","summary":"Product page","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/docs":{"get":{"tags":["public"],"operationId":"pageDocs","summary":"Documentation home","description":"Rendered from the Markdown in product-docs/.","security":[],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/docs/search":{"get":{"tags":["public"],"operationId":"pageDocsSearch","summary":"Search the documentation","description":"A plain GET form; no JavaScript. Marked noindex.","security":[],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":100}}],"responses":{"200":{"$ref":"#/components/responses/Html"}}}},"/docs/{page}":{"get":{"tags":["public"],"operationId":"pageDocsPage","summary":"A documentation page","description":"A page at the top of product-docs/, such as /docs/quickstart. Unknown pages answer 404 with a styled page.","security":[],"parameters":[{"name":"page","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Html"},"404":{"$ref":"#/components/responses/Html"}}}},"/docs/{section}/{page}":{"get":{"tags":["public"],"operationId":"pageDocsSectionPage","summary":"A documentation page inside a section","description":"Such as /docs/api/posts. Unknown pages answer 404 with a styled page. A page that was removed answers 301 to where its content went (/docs/channels/waiting-on-platforms → /docs/channels).","security":[],"parameters":[{"name":"section","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Html"},"301":{"description":"The page was removed; Location is where its content went.","headers":{"Location":{"schema":{"type":"string"}}}},"404":{"$ref":"#/components/responses/Html"}}}},"/connect/handoff/{token}":{"get":{"tags":["channels"],"operationId":"openConnectHandoff","summary":"Open a connect handoff link (browser)","description":"Where a person lands when they open the `handoffUrl` an API key got from `POST /v1/connect-sessions`. Not signed in: redirected to the app's login, which comes back here. Signed in as a member of the workspace: the session becomes theirs and the browser is sent to the platform; the OAuth return then finishes in this same browser. Signed in as someone else, or a link already used or expired: redirected to the app with the reason. Always a redirect; nothing to parse.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"302":{"description":"To the login page, the platform's consent page, or the app's channels page.","headers":{"Location":{"schema":{"type":"string"}}}}}}},"/mcp":{"post":{"tags":["agents"],"operationId":"mcp","summary":"MCP server (streamable HTTP, stateless)","description":"The Model Context Protocol endpoint for AI agents: JSON-RPC 2.0 over streamable HTTP, one request per call, no session. Authenticate exactly as the API (`Authorization: Bearer omniposts_…`); a key's scopes apply per tool. Tools: `list_providers`, `list_channels`, `validate_post`, `create_post`, `list_posts`, `get_post`, `cancel_post`, `retry_post`, `upload_media_from_url`, `get_post_metrics` — the same operations, shapes and error codes as this API. Send `Accept: application/json, text/event-stream`. Rate-limited in the WRITE class. See /docs/api/mcp.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A JSON-RPC 2.0 request (`initialize`, `tools/list`, `tools/call`, …)."},"example":{"jsonrpc":"2.0","id":1,"method":"tools/list"}}}},"responses":{"200":{"description":"A JSON-RPC 2.0 response (or, for a notification, an empty body).","content":{"application/json":{"schema":{"type":"object","description":"A JSON-RPC 2.0 response."}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/llms.txt":{"get":{"tags":["public"],"operationId":"llmsTxt","summary":"llms.txt","description":"The llmstxt.org index an agent reads first: who OmniPosts is in one sentence, the key product, docs and trust pages with one-line summaries, and where the contract and the MCP endpoint are.","security":[],"responses":{"200":{"description":"Plain text.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/llms-full.txt":{"get":{"tags":["public"],"operationId":"llmsFullTxt","summary":"llms-full.txt","description":"Every /docs page as Markdown, in the order of the docs menu, each under its title and absolute URL.","security":[],"responses":{"200":{"description":"Plain text (Markdown).","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/openapi.json":{"get":{"tags":["public"],"operationId":"openapiJson","summary":"This contract as JSON","description":"The same document as /openapi.yaml, converted to JSON for tools that read only JSON.","security":[],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/robots.txt":{"get":{"tags":["public"],"operationId":"robotsTxt","summary":"robots.txt","description":"Crawl rules. Public pages, /docs, /llms.txt, /llms-full.txt and the OpenAPI files are crawlable; the app (including its sign-in and sign-up shells, which are noindex), the API, health and media are not. Named AI crawlers get their own group with the same rules.","security":[],"responses":{"200":{"description":"robots.txt, with the sitemap's absolute URL.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/sitemap.xml":{"get":{"tags":["public"],"operationId":"sitemapXml","summary":"sitemap.xml","description":"Every indexable public page as absolute URLs built from the public base URL, each with a lastmod date (docs pages use their `updated` front matter). The status page and noindex docs pages are left out.","security":[],"responses":{"200":{"description":"A sitemaps.org urlset.","content":{"application/xml":{"schema":{"type":"string"}}}}}}},"/changelog.atom":{"get":{"tags":["public"],"operationId":"changelogAtom","summary":"Changelog feed","description":"The changelog page as an Atom feed, newest first, built from the page itself so the two agree. Announced by a `rel=\"alternate\"` link on /changelog and listed in /llms.txt.","security":[],"responses":{"200":{"description":"An Atom 1.0 feed.","content":{"application/atom+xml":{"schema":{"type":"string"}}}}}}},"/.well-known/security.txt":{"get":{"tags":["public"],"operationId":"securityTxt","summary":"security.txt","description":"Where to report a vulnerability (RFC 9116): the contact address, the security policy page and an expiry a year ahead.","security":[],"responses":{"200":{"description":"Plain text.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/.well-known/atproto-did":{"get":{"tags":["public"],"operationId":"atprotoDid","summary":"Bluesky handle proof","description":"The `did:plc:…` of the Bluesky account whose handle is this site's host, as plain text, when the operator has set `OMNIPOSTS_ATPROTO_DID`. 404 until then (a DNS `_atproto` TXT record does the same job).","security":[],"responses":{"404":{"description":"Not configured."}}}},"/favicon.ico":{"get":{"tags":["public"],"operationId":"faviconIco","summary":"favicon.ico","description":"Browsers ask for /favicon.ico by default; the icon is the web app's SVG.","security":[],"responses":{"301":{"description":"To /app/favicon.svg.","headers":{"Location":{"$ref":"#/components/headers/Location"}}}}}},"/apple-touch-icon.png":{"get":{"tags":["public"],"operationId":"appleTouchIcon","summary":"apple-touch-icon.png","description":"The 180×180 icon iOS asks for when a page is saved to the home screen. Cached for a day.","security":[],"responses":{"200":{"description":"The icon.","content":{"image/png":{"schema":{"type":"string","format":"binary"}}}}}}},"/site.webmanifest":{"get":{"tags":["public"],"operationId":"webManifest","summary":"Web app manifest","description":"Name, colours and install icons, linked from every public page. Cached for a day.","security":[],"responses":{"200":{"description":"A W3C web app manifest.","content":{"application/manifest+json":{"schema":{"type":"object","required":["name","short_name","start_url","icons"],"properties":{"name":{"type":"string"},"short_name":{"type":"string"},"start_url":{"type":"string"},"scope":{"type":"string"},"display":{"type":"string"},"background_color":{"type":"string"},"theme_color":{"type":"string"},"icons":{"type":"array","items":{"type":"object","required":["src","sizes","type"],"properties":{"src":{"type":"string"},"sizes":{"type":"string"},"type":{"type":"string"}}}}}}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"omniposts_…","description":"Developer API key from POST /v1/api-keys, `omniposts_…`. Keys issued before the rename to OmniPosts start `jamp_…` or `nsk_…` and keep working until revoked. No CSRF header needed."},"cookieAuth":{"type":"apiKey","in":"cookie","name":"nsid","description":"Web-app session (HttpOnly, SameSite=Lax; Secure outside dev-mode). Every non-GET request made with the cookie must also send `X-OmniPosts-Client: web` (legacy: `X-Neslivis-Client: web`), or it gets 403 `csrf`."}},"headers":{"SessionCookie":{"required":true,"description":"nsid=<token>; HttpOnly; SameSite=Lax; Path=/ (cleared with Max-Age=0).","schema":{"type":"string","pattern":"^nsid="}},"Location":{"required":true,"schema":{"type":"string"}},"RetryAfter":{"required":true,"description":"Seconds until the current window ends.","schema":{"type":"integer","minimum":1}},"RateLimitRemaining":{"description":"Requests left in the current window (sent whenever the filter counted the request).","schema":{"type":"integer","minimum":0}},"IdempotentReplayed":{"required":true,"schema":{"type":"string","enum":["true"]}}},"parameters":{"Id":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"UserId":{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"InviteId":{"name":"inviteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"InviteToken":{"name":"token","in":"path","required":true,"description":"The token at the end of an `inviteUrl`. Unknown or malformed → 404.","schema":{"type":"string"}},"ClientHeader":{"name":"X-OmniPosts-Client","in":"header","required":false,"description":"Required (value `web`) on every non-GET request authenticated by the session cookie, and on the public auth endpoints; otherwise 403 `csrf`. The legacy `X-Neslivis-Client` satisfies it too.","schema":{"type":"string","enum":["web"]}},"LegacyClientHeader":{"name":"X-Neslivis-Client","in":"header","required":false,"deprecated":true,"description":"Legacy name of `X-OmniPosts-Client`, from before the rename. Still accepted with the same meaning; send `X-OmniPosts-Client` in new clients.","schema":{"type":"string","enum":["web"]}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"1–255 characters (else 400). Same key + same request → 200 replay; same key + different request → 409 `idempotency_conflict`.","schema":{"type":"string","minLength":1,"maxLength":255}},"IfMatch":{"name":"If-Match","in":"header","required":false,"description":"\"<updatedAt>\" exactly as last received (quotes and W/ optional). Unparseable → 400.","schema":{"type":"string"}},"Cursor":{"name":"cursor","in":"query","required":false,"description":"The previous page's nextCursor.","schema":{"type":"string","format":"uuid"}},"Limit":{"name":"limit","in":"query","required":false,"description":"Default 25; values above 100 clamp to 100; values ≤ 0 mean the default.","schema":{"type":"integer"}},"Channel":{"name":"channel","in":"query","required":false,"schema":{"type":"string","format":"uuid"}},"From":{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},"To":{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}}},"responses":{"BadRequest":{"description":"`invalid_request` — malformed JSON, a missing or mistyped field or parameter (also `invalid_signed_request` on the Meta callbacks).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthenticated":{"description":"`unauthenticated` — no or bad session/key; `invalid_credentials` on login.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"`csrf` — cookie-authenticated write without `X-OmniPosts-Client: web` (or the legacy `X-Neslivis-Client: web`); `forbidden` — an API key on a person-only operation, or not an owner; `insufficient_scope` — an API key created with scopes that do not cover this operation (the message names the scope); `invite_email_mismatch` — the invite names another address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"`not_found` — also for another organization's ids; existence is never leaked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"`idempotency_conflict`, `stale_write` (details.current), `already_promoted`, `not_editable` (details.current), `already_dispatched`, `email_taken`, `already_sent`, `last_owner`, `last_workspace`, `members_remain`, `sole_owner_with_members`, `try_again` (account deletion raced with a membership change; nothing was deleted).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Gone":{"description":"`invite_expired`, `invite_used`, `invite_revoked` — the invite can no longer be accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unprocessable":{"description":"`nothing_to_publish` (details.rejections), `invalid_edit`, `terms_required`, `weak_password`, `password_too_long`, `media_rejected`, `invalid_webhook`, `confirmation_required`, `plan_limit_reached` (details.limit, details.plan), `plan_in_beta`, `queue_empty`, `queue_full`, `posting_limit_reached` (lifetime limit or a post larger than the limit; details.period, maxTargets, used, requested, remaining), `workspace_limit_reached`, `invite_limit_reached`, and a connect's failure codes (see POST /v1/connect-sessions, e.g. `not_a_member`, `workspace_deleted`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"`rate_limited` — per organization and per API replica (READ 600/min, WRITE 60/min, AUTOSAVE 30/min, MEDIA 10/min); shared across replicas: EXPORT 3/hour per organization, and /v1/auth 20/min per client IP and per account. Also `posting_limit_reached` — a daily per-user posting limit (details.resetsAt; Retry-After is the seconds until 00:00 UTC and X-RateLimit-Remaining is 0).","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotConfigured":{"description":"`not_configured` — META_APP_SECRET is not set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Html":{"description":"A server-rendered page.","content":{"text/html":{"schema":{"type":"string"}}}},"GroupCreated":{"description":"Every target accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupCreated"}}}},"GroupPartiallyCreated":{"description":"Some targets rejected at creation; `rejections` says which and why.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupCreated"}}}},"GroupReplayed":{"description":"Replay of an earlier request with the same Idempotency-Key.","headers":{"Idempotent-Replayed":{"$ref":"#/components/headers/IdempotentReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupCreated"}}}}},"schemas":{"Uuid":{"type":"string","format":"uuid"},"Instant":{"type":"string","format":"date-time"},"NullableInstant":{"type":"string","format":"date-time","nullable":true},"Null":{"description":"JSON null — the second branch of a nullable reference.","nullable":true,"enum":[null]},"Jwks":{"type":"object","additionalProperties":false,"required":["keys"],"properties":{"keys":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["kty","crv","x","y","kid","use","alg"],"properties":{"kty":{"type":"string","enum":["EC"]},"crv":{"type":"string","enum":["P-256"]},"x":{"type":"string"},"y":{"type":"string"},"kid":{"type":"string"},"use":{"type":"string","enum":["sig"]},"alg":{"type":"string","enum":["ES256"]}}}}}},"StringMap":{"type":"object","additionalProperties":{"type":"string"}},"Error":{"type":"object","additionalProperties":false,"required":["error"],"properties":{"error":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"message":{"type":"string","minLength":1},"details":{"$ref":"#/components/schemas/ErrorDetails"}}}}},"ErrorDetails":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"rejections":{"type":"array","items":{"$ref":"#/components/schemas/Rejection"}},"current":{"anyOf":[{"$ref":"#/components/schemas/Idea"},{"$ref":"#/components/schemas/Group"}]},"failure":{"$ref":"#/components/schemas/Failure"},"limit":{"type":"string","enum":["channels","posts_per_month"],"description":"plan_limit_reached: which limit."},"plan":{"$ref":"#/components/schemas/PlanId"},"period":{"type":"string","enum":["daily","lifetime"],"description":"posting_limit_reached: the kind of per-user limit that was hit."},"maxTargets":{"type":"integer","minimum":0,"description":"posting_limit_reached: the limit, in channel targets."},"used":{"type":"integer","minimum":0,"description":"posting_limit_reached: targets already used in the period."},"requested":{"type":"integer","minimum":0,"description":"posting_limit_reached: targets this request would use."},"remaining":{"type":"integer","minimum":0,"description":"posting_limit_reached: targets still available in the period."},"resetsAt":{"type":"string","format":"date-time","description":"posting_limit_reached: when a daily limit resets (00:00 UTC). Only on 429."},"settingsType":{"type":"string","description":"settings_type_mismatch: the `settings.type` sent."},"providerId":{"type":"string","description":"settings_type_mismatch: the platform of the channel."}}},"Failure":{"type":"object","additionalProperties":false,"required":["class","code","message","action","retryable"],"properties":{"class":{"type":"string","enum":["RECONNECT","RETRYABLE","UNKNOWN","USER_ERROR","PLATFORM_BLOCKED"]},"code":{"type":"string","minLength":1},"message":{"type":"string","minLength":1},"action":{"type":"string","enum":["RECONNECT","NONE_AUTO_RETRYING","VERIFY_MANUALLY","FIX_CONTENT","CONTACT_SUPPORT","RETRY"]},"retryable":{"type":"boolean"}}},"Rejection":{"type":"object","additionalProperties":false,"required":["channelId","failure"],"properties":{"channelId":{"type":"string","format":"uuid","nullable":true},"failure":{"$ref":"#/components/schemas/Failure"}}},"GroupValidation":{"type":"object","additionalProperties":false,"required":["ok","accepted","rejections","problems"],"properties":{"ok":{"type":"boolean","description":"Whether POST /v1/groups with this body would answer 201 or 207 right now."},"accepted":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["channelId","providerId"],"properties":{"channelId":{"$ref":"#/components/schemas/Uuid"},"providerId":{"type":"string"}}}},"rejections":{"type":"array","items":{"$ref":"#/components/schemas/Rejection"}},"problems":{"description":"Group-wide refusals; any entry makes `ok` false.","type":"array","items":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string","enum":["plan_limit_reached","posting_limit_reached"]},"message":{"type":"string"}}}}}},"SignupRequest":{"type":"object","additionalProperties":false,"required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"},"name":{"type":"string"},"organizationName":{"type":"string","description":"The first workspace's name, by the workspace-name rule: trimmed, 1–80 characters, no control or format characters (else 400). Blank or missing → \"<first word of name>'s workspace\", or \"My workspace\" without a name."},"acceptTerms":{"type":"boolean","description":"Must be true (else 422 terms_required)."}}},"LoginRequest":{"type":"object","additionalProperties":false,"required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"}}},"User":{"type":"object","additionalProperties":false,"required":["id","email","name"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"email":{"type":"string"},"name":{"type":"string","nullable":true}}},"Organization":{"type":"object","additionalProperties":false,"required":["id","name"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"name":{"type":"string"}}},"Me":{"type":"object","additionalProperties":false,"required":["user","organization","role"],"properties":{"user":{"anyOf":[{"$ref":"#/components/schemas/User"},{"$ref":"#/components/schemas/Null"}]},"organization":{"$ref":"#/components/schemas/Organization"},"role":{"description":"The person's role in `organization` (the current workspace); null for an API key.","anyOf":[{"$ref":"#/components/schemas/WorkspaceRole"},{"$ref":"#/components/schemas/Null"}]}}},"ApiKeyScope":{"type":"string","description":"What a key may touch, by resource. `account:read` (GET /v1/me, plan, queue, consents, API-key list, its workspace and members); `settings:write` (plan, queue, account deletion, API keys, workspaces and invites — all but the queue are person-only anyway); `channels:read` (providers, channels, connect-session state); `channels:manage` (start a connect, disconnect); `posts:read` (posts, targets, drafts, videos); `posts:write` (create, validate, edit, cancel, retry, promote); `media:read`; `media:write`; `metrics:read` (metrics and reports); `webhooks:manage`.","enum":["account:read","settings:write","channels:read","channels:manage","posts:read","posts:write","media:read","media:write","metrics:read","webhooks:manage"]},"NewApiKeyRequest":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","description":"Blank or missing → \"API key\"."},"scopes":{"description":"Missing → a key with every scope. An empty list is refused (400). A request outside a key's scopes is refused with 403 `insufficient_scope`.","type":"array","items":{"$ref":"#/components/schemas/ApiKeyScope"}},"expiresAt":{"description":"Missing → never. Must be in the future (else 400). An expired key gets 401.","allOf":[{"$ref":"#/components/schemas/Instant"}]}}},"ApiKey":{"type":"object","additionalProperties":false,"required":["id","name","prefix","createdAt","lastUsedAt","scopes","expiresAt","createdBy"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"name":{"type":"string"},"prefix":{"type":"string"},"createdAt":{"$ref":"#/components/schemas/Instant"},"lastUsedAt":{"$ref":"#/components/schemas/NullableInstant"},"scopes":{"description":"Every scope is listed for a key created without scopes.","type":"array","items":{"$ref":"#/components/schemas/ApiKeyScope"}},"expiresAt":{"$ref":"#/components/schemas/NullableInstant"},"createdBy":{"description":"Who made the key; null once their account is gone. Removing them from the workspace revokes the key.","anyOf":[{"$ref":"#/components/schemas/PersonRef"},{"$ref":"#/components/schemas/Null"}]}}},"PersonRef":{"description":"A person, by id and name.","type":"object","additionalProperties":false,"required":["id","name"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"name":{"type":"string","description":"Their display name, or their email when they have none."}}},"ApiKeyList":{"type":"object","additionalProperties":false,"required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}},"NewApiKey":{"type":"object","additionalProperties":false,"required":["id","name","prefix","key","scopes","expiresAt"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"name":{"type":"string"},"prefix":{"type":"string"},"key":{"type":"string","pattern":"^omniposts_"},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyScope"}},"expiresAt":{"$ref":"#/components/schemas/NullableInstant"}}},"WorkspaceRole":{"type":"string","enum":["owner","member"]},"Workspace":{"type":"object","additionalProperties":false,"required":["id","name","role","current","memberCount","createdAt"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"name":{"type":"string"},"role":{"description":"The caller's role; null for an API key.","anyOf":[{"$ref":"#/components/schemas/WorkspaceRole"},{"$ref":"#/components/schemas/Null"}]},"current":{"type":"boolean","description":"The workspace this session (or key) acts for."},"memberCount":{"type":"integer","minimum":0},"createdAt":{"$ref":"#/components/schemas/Instant"}}},"WorkspaceList":{"type":"object","additionalProperties":false,"required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Workspace"}}}},"WorkspaceRequest":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","description":"Trimmed; 1–80 characters; no control or format characters (else 400)."}}},"DeleteWorkspaceRequest":{"type":"object","additionalProperties":false,"required":["confirm"],"properties":{"confirm":{"type":"string","description":"Must be exactly the workspace's name."}}},"Member":{"type":"object","additionalProperties":false,"required":["userId","name","email","role","joinedAt"],"properties":{"userId":{"$ref":"#/components/schemas/Uuid"},"name":{"type":"string","nullable":true},"email":{"type":"string"},"role":{"$ref":"#/components/schemas/WorkspaceRole"},"joinedAt":{"$ref":"#/components/schemas/Instant"}}},"MemberList":{"type":"object","additionalProperties":false,"required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}},"MemberRoleRequest":{"type":"object","additionalProperties":false,"required":["role"],"properties":{"role":{"$ref":"#/components/schemas/WorkspaceRole"}}},"InviteRequest":{"type":"object","additionalProperties":false,"properties":{"email":{"type":"string","description":"Optional. Only the account with this address may accept."},"role":{"description":"Missing → member. `owner` needs `email`.","allOf":[{"$ref":"#/components/schemas/WorkspaceRole"}]}}},"Invite":{"type":"object","additionalProperties":false,"required":["id","email","role","invitedBy","createdAt","expiresAt"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"email":{"type":"string","nullable":true,"description":"Lower-cased; null when anyone with the link may accept."},"role":{"$ref":"#/components/schemas/WorkspaceRole"},"invitedBy":{"type":"string","nullable":true,"description":"The inviter's name (or email when they have no name); null if they are gone."},"createdAt":{"$ref":"#/components/schemas/Instant"},"expiresAt":{"$ref":"#/components/schemas/Instant"}}},"InviteList":{"type":"object","additionalProperties":false,"required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Invite"}}}},"NewInvite":{"type":"object","additionalProperties":false,"required":["id","email","role","invitedBy","createdAt","expiresAt","inviteUrl"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"email":{"type":"string","nullable":true},"role":{"$ref":"#/components/schemas/WorkspaceRole"},"invitedBy":{"type":"string","nullable":true},"createdAt":{"$ref":"#/components/schemas/Instant"},"expiresAt":{"$ref":"#/components/schemas/Instant"},"inviteUrl":{"type":"string","description":"Shown once. Only a hash of its token is stored."}}},"InvitePreview":{"type":"object","additionalProperties":false,"required":["workspace","invitedBy","role","expiresAt","status","emailRestricted","emailMatches","alreadyMember"],"properties":{"workspace":{"type":"object","additionalProperties":false,"required":["id","name"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"name":{"type":"string"}}},"invitedBy":{"type":"string","nullable":true,"description":"The inviter's display name; null when they have none. Never their email."},"role":{"$ref":"#/components/schemas/WorkspaceRole"},"expiresAt":{"$ref":"#/components/schemas/Instant"},"status":{"type":"string","enum":["pending","accepted","revoked","expired"],"description":"Only `pending` can be accepted."},"emailRestricted":{"type":"boolean","description":"The invite names an address."},"emailMatches":{"type":"boolean","description":"The caller may accept it: true when it names no address, or names theirs."},"alreadyMember":{"type":"boolean"}}},"Provider":{"type":"object","additionalProperties":false,"required":["id","displayName","tier","enabled","lockedReason","lockedKind","auth","text","media","uiFields","readback","analytics","accountAnalytics","apiVersion","checkedDate","stale"],"properties":{"id":{"type":"string"},"displayName":{"type":"string"},"tier":{"type":"string","enum":["FREE","GATED"]},"enabled":{"type":"boolean"},"lockedReason":{"type":"string","nullable":true,"description":"One sentence when locked, else null. For integrations and logs; show people \"Coming soon\"."},"lockedKind":{"type":"string","nullable":true,"enum":["platform_review","paid_access","not_configured","switched_off","upcoming"],"description":"Why the provider is locked; null when enabled. `platform_review` — waiting on the platform's app review or audit (TikTok, TikTok for Business, YouTube, LinkedIn Pages); `upcoming` — locked, and on the operator's \"Coming soon\" list (`OMNIPOSTS_UPCOMING`; by default Instagram, Facebook, Threads, TikTok and TikTok for Business, whose platform onboarding is still in progress); `paid_access` — the platform charges per post and paid posting is not switched on (X); `not_configured` — this server has no app credentials for the platform yet; `switched_off` — the publish flag is off for another reason (e.g. a kill switch). While locked, connecting is refused too. This and `lockedReason` are for integrations and logs: show people \"Coming soon\". Failure messages for a locked platform never carry the reason (\"X is coming soon to OmniPosts.\", \"Posting to X is paused for now, so this post wasn't sent.\")."},"auth":{"type":"object","additionalProperties":false,"description":"How to connect. The top-level `kind`, `fields`, `inputs` and `scopes` describe the default method; `methods` lists every way this platform can be connected, the default first (Bluesky: `oauth2`, then `app_password`). Send the chosen `method` when starting a connect session.","required":["kind","fields","inputs","scopes","methods"],"properties":{"kind":{"type":"string","enum":["APP_PASSWORD","BOT_TOKEN","NOSTR_KEY","OAUTH2"]},"fields":{"description":"What a credential-paste connect asks for (empty for OAuth).","type":"array","items":{"$ref":"#/components/schemas/CredentialField"}},"inputs":{"description":"Non-secret OAuth start inputs (e.g. Mastodon instance); empty otherwise.","type":"array","items":{"$ref":"#/components/schemas/CredentialField"}},"scopes":{"description":"Exact scope strings an OAuth connect asks for; empty otherwise.","type":"array","items":{"type":"string"}},"methods":{"type":"array","items":{"$ref":"#/components/schemas/AuthMethod"}}}},"text":{"type":"object","additionalProperties":false,"required":["maxLength","counter","required"],"properties":{"maxLength":{"type":"integer"},"counter":{"type":"string","enum":["GRAPHEMES","CODE_POINTS","WEIGHTED"]},"required":{"type":"boolean"}}},"media":{"type":"object","additionalProperties":false,"required":["maxImages","maxImageBytes","imageMimes","maxVideos","maxVideoBytes","videoMimes","maxAltTextLength","mediaRequired","ingest"],"properties":{"maxImages":{"type":"integer"},"maxImageBytes":{"type":"integer","format":"int64"},"imageMimes":{"type":"array","items":{"type":"string"}},"maxVideos":{"type":"integer"},"maxVideoBytes":{"type":"integer","format":"int64"},"videoMimes":{"type":"array","items":{"type":"string"}},"maxAltTextLength":{"type":"integer"},"mediaRequired":{"type":"boolean"},"ingest":{"type":"string","enum":["UPLOAD","URL"]}}},"uiFields":{"type":"array","items":{"$ref":"#/components/schemas/UiField"}},"readback":{"type":"string","enum":["EXACT","SEARCH","NONE"]},"thread":{"description":"Present when the platform supports threads; `maxParts` counts the post itself.","type":"object","nullable":true,"additionalProperties":false,"required":["maxParts"],"properties":{"maxParts":{"type":"integer"}}},"analytics":{"description":"Per declared metric. `supported` is false (with a reason sentence) while the provider's `metrics.<id>` flag is OFF — the only gate — or the platform cannot report it.","type":"object","additionalProperties":false,"properties":{"REACTIONS":{"$ref":"#/components/schemas/MetricSupport"},"REPOSTS":{"$ref":"#/components/schemas/MetricSupport"},"COMMENTS":{"$ref":"#/components/schemas/MetricSupport"},"QUOTES":{"$ref":"#/components/schemas/MetricSupport"},"ZAPS":{"$ref":"#/components/schemas/MetricSupport"},"VIEWS":{"$ref":"#/components/schemas/MetricSupport"},"REACH":{"$ref":"#/components/schemas/MetricSupport"},"IMPRESSIONS":{"$ref":"#/components/schemas/MetricSupport"},"SHARES":{"$ref":"#/components/schemas/MetricSupport"},"SAVES":{"$ref":"#/components/schemas/MetricSupport"},"LINK_CLICKS":{"$ref":"#/components/schemas/MetricSupport"},"PROFILE_VISITS":{"$ref":"#/components/schemas/MetricSupport"}}},"accountAnalytics":{"description":"Per declared account-level metric, sampled once per UTC day (GET /v1/reports/audience). Same gate and sentences as `analytics`; empty for providers that read none.","type":"object","additionalProperties":false,"properties":{"FOLLOWERS":{"$ref":"#/components/schemas/MetricSupport"},"FOLLOWING":{"$ref":"#/components/schemas/MetricSupport"},"POSTS":{"$ref":"#/components/schemas/MetricSupport"},"LIKES_RECEIVED":{"$ref":"#/components/schemas/MetricSupport"}}},"apiVersion":{"type":"string","nullable":true},"checkedDate":{"type":"string","format":"date","nullable":true},"stale":{"type":"boolean","description":"True when checkedDate is more than 90 days old."}}},"CredentialField":{"type":"object","additionalProperties":false,"required":["name","label","help","secret"],"properties":{"name":{"type":"string"},"label":{"type":"string"},"help":{"type":"string","nullable":true},"secret":{"type":"boolean"},"defaultValue":{"type":"string","nullable":true,"description":"What the form starts with (Mastodon `mastodon.social`). Never on a secret field."},"bareSuffix":{"type":"string","nullable":true,"description":"Added by the server to a value with no dot, colon or `@` (a leading `@` aside), so a bare name is enough (Bluesky `.bsky.social`). Forms show it after the box."},"prefix":{"type":"string","nullable":true,"description":"Shown before the box so people see the shape of the value (Bluesky handle `@`, Mastodon server `@you@`). Display only; never part of the value."},"placeholder":{"type":"string","nullable":true,"description":"Shown inside the empty box (Bluesky `yourname.bsky.social`)."}}},"UiField":{"type":"object","additionalProperties":false,"required":["name","label","type","options","defaultValue","required","help","lockedValue"],"properties":{"name":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["TEXT","TEXTAREA","SELECT","TOGGLE"]},"options":{"type":"array","items":{"type":"string"}},"defaultValue":{"type":"string","nullable":true},"required":{"type":"boolean"},"help":{"type":"string","nullable":true},"lockedValue":{"type":"string","nullable":true}}},"MetricSupport":{"type":"object","additionalProperties":false,"required":["supported","reason","reasonKind"],"properties":{"supported":{"type":"boolean"},"reason":{"type":"string","nullable":true},"reasonKind":{"$ref":"#/components/schemas/ReasonKind"}}},"ProviderList":{"type":"object","additionalProperties":false,"required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Provider"}}}},"Channel":{"type":"object","additionalProperties":false,"required":["id","providerId","displayName","handle","avatarUrl","health","connectedAt","connectedBy"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"providerId":{"type":"string"},"displayName":{"type":"string","nullable":true},"handle":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"health":{"type":"object","additionalProperties":false,"required":["state","reason","action"],"properties":{"state":{"type":"string","enum":["OK","DEGRADED","NEEDS_RECONNECT","BLOCKED"]},"reason":{"type":"string","nullable":true},"action":{"type":"string","nullable":true}}},"connectedAt":{"$ref":"#/components/schemas/Instant"},"connectedBy":{"description":"Who connected (or last reconnected) the channel; null when unknown (connected before this was recorded) or their account is gone. When that person leaves the workspace, is removed, or deletes their account, the channel is disconnected. A null one is never disconnected on anyone's behalf.","anyOf":[{"$ref":"#/components/schemas/PersonRef"},{"$ref":"#/components/schemas/Null"}]}}},"ChannelList":{"type":"object","additionalProperties":false,"required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Channel"}}}},"AuthMethod":{"type":"object","additionalProperties":false,"required":["method","kind","fields","inputs","scopes"],"properties":{"method":{"type":"string","enum":["oauth2","app_password","bot_token","nostr_key"],"description":"The value to send as `method` when starting a connect session."},"kind":{"type":"string","enum":["APP_PASSWORD","BOT_TOKEN","NOSTR_KEY","OAUTH2"]},"fields":{"description":"What a credential-paste connect asks for (empty for OAuth).","type":"array","items":{"$ref":"#/components/schemas/CredentialField"}},"inputs":{"description":"Non-secret OAuth start inputs (e.g. a Bluesky handle); empty otherwise.","type":"array","items":{"$ref":"#/components/schemas/CredentialField"}},"scopes":{"type":"array","items":{"type":"string"}}}},"StartConnectRequest":{"type":"object","additionalProperties":false,"required":["providerId"],"properties":{"providerId":{"type":"string"},"method":{"type":"string","enum":["oauth2","app_password","bot_token","nostr_key"],"description":"Which of the provider's `auth.methods` to use. Omitted: the default method, or the credential method when `fields` are sent. A method the provider does not offer is refused with 422 `unknown_auth_method`."},"fields":{"$ref":"#/components/schemas/StringMap"},"input":{"$ref":"#/components/schemas/StringMap"}}},"ConnectSession":{"type":"object","additionalProperties":false,"required":["id","providerId","status","channel","error"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"providerId":{"type":"string"},"status":{"type":"string","enum":["pending","completed","failed","expired"]},"authorizationUrl":{"type":"string","description":"Only on the 201 of an OAuth start by a person."},"handoffUrl":{"type":"string","description":"Only on the 201 of a start by an API key: the one-time link a signed-in person opens to continue. Never shown again."},"channel":{"anyOf":[{"$ref":"#/components/schemas/Channel"},{"$ref":"#/components/schemas/Null"}]},"error":{"type":"string","nullable":true,"description":"One plain sentence when failed: the message of the failure that ended the session, for example `different_browser` (\"Finish connecting in the same browser and account you started from…\"), `not_a_member` (\"You're no longer a member of this workspace, so the channel wasn't connected.\") or `workspace_deleted` (\"This workspace was deleted, so the channel wasn't connected.\"), a plan limit, or the platform's refusal."}}},"TargetDraft":{"type":"object","additionalProperties":false,"properties":{"body":{"type":"string","nullable":true},"mediaIds":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/Uuid"}},"settings":{"type":"object","nullable":true,"additionalProperties":{"type":"string"}}}},"IdeaRequest":{"type":"object","additionalProperties":false,"properties":{"body":{"type":"string"},"mediaIds":{"type":"array","items":{"$ref":"#/components/schemas/Uuid"}},"targetDrafts":{"description":"Per-channel overrides keyed by channel id.","type":"object","additionalProperties":{"$ref":"#/components/schemas/TargetDraft"}}}},"Idea":{"type":"object","additionalProperties":false,"required":["id","body","mediaIds","status","targetDrafts","createdAt","updatedAt"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"body":{"type":"string","nullable":true},"mediaIds":{"type":"array","items":{"$ref":"#/components/schemas/Uuid"}},"status":{"type":"string","enum":["draft","promoted","discarded"]},"targetDrafts":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TargetDraft"}},"createdAt":{"$ref":"#/components/schemas/Instant"},"updatedAt":{"$ref":"#/components/schemas/Instant"}}},"IdeaPage":{"type":"object","additionalProperties":false,"required":["items","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Idea"}},"nextCursor":{"type":"string","format":"uuid","nullable":true}}},"PromoteRequest":{"type":"object","additionalProperties":false,"properties":{"targets":{"type":"array","items":{"$ref":"#/components/schemas/TargetRequest"}},"scheduledFor":{"$ref":"#/components/schemas/Instant"},"timezone":{"type":"string"},"queue":{"type":"boolean","description":"Place the group in the next free posting slot. Excludes scheduledFor."},"thread":{"description":"Further parts of a thread (see GroupRequest.thread).","type":"array","items":{"$ref":"#/components/schemas/ThreadPart"}}}},"GroupStatus":{"type":"string","enum":["draft","scheduled","publishing","published","partial","failed","cancelled"]},"TargetStatus":{"type":"string","enum":["pending","preflight","queued","publishing","awaiting_platform","unknown","published","failed","cancelled"]},"ThreadPart":{"description":"One further part of a thread, a reply to the part before it. The post itself is the first part.","type":"object","additionalProperties":false,"required":["body"],"properties":{"body":{"type":"string"},"mediaIds":{"type":"array","items":{"$ref":"#/components/schemas/Uuid"}}}},"TargetRequest":{"type":"object","additionalProperties":false,"required":["channelId"],"properties":{"channelId":{"$ref":"#/components/schemas/Uuid"},"body":{"type":"string","description":"Overrides the group body for this channel."},"mediaIds":{"type":"array","items":{"$ref":"#/components/schemas/Uuid"}},"settings":{"$ref":"#/components/schemas/StringMap"},"thread":{"description":"This channel's own further parts; replaces the group's `thread`.","type":"array","items":{"$ref":"#/components/schemas/ThreadPart"}}}},"GroupRequest":{"type":"object","additionalProperties":false,"properties":{"body":{"type":"string"},"mediaIds":{"type":"array","items":{"$ref":"#/components/schemas/Uuid"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/TargetRequest"}},"scheduledFor":{"$ref":"#/components/schemas/Instant"},"timezone":{"type":"string"},"queue":{"type":"boolean","description":"Place the group in the next free posting slot. Excludes scheduledFor (400 if both)."},"thread":{"description":"Further parts of a thread, in order, each posted as a reply to the one before. Only for platforms whose provider lists `thread.maxParts` (Bluesky, Mastodon); elsewhere the target is rejected with `thread_not_supported`. Each part is checked like a post.","type":"array","items":{"$ref":"#/components/schemas/ThreadPart"}}}},"VideoRequest":{"type":"object","additionalProperties":false,"required":["channelId","mediaId","settings"],"properties":{"channelId":{"$ref":"#/components/schemas/Uuid"},"mediaId":{"allOf":[{"$ref":"#/components/schemas/Uuid"}],"description":"A video uploaded with `POST /v1/media`."},"description":{"type":"string","description":"The video description. YouTube: up to 5,000 bytes (UTF-8), no < or >. On TikTok it is the caption: up to 2,200 characters, emoji count as two."},"scheduledFor":{"allOf":[{"$ref":"#/components/schemas/Instant"}],"description":"When to publish; omit to publish now."},"timezone":{"type":"string"},"settings":{"oneOf":[{"$ref":"#/components/schemas/YouTubeVideoSettings"},{"$ref":"#/components/schemas/TikTokVideoSettings"},{"$ref":"#/components/schemas/TikTokDraftSettings"}]}}},"YouTubeVideoSettings":{"type":"object","additionalProperties":false,"required":["type","title","madeForKids"],"description":"YouTube Data API v3 `videos.insert` (https://developers.google.com/youtube/v3/docs/videos/insert, checked 2026-10-19). Each key maps to one `snippet`/`status` property or the `notifySubscribers` parameter.","properties":{"type":{"type":"string","enum":["youtube"]},"title":{"type":"string","description":"Up to 100 characters; no < or >."},"visibility":{"type":"string","enum":["private","unlisted","public"],"description":"Default `private`. Until Google's audit of OmniPosts is done (`youtube.audited`), OmniPosts refuses anything but `private` (`private_only`); this is OmniPosts's choice, not YouTube's."},"madeForKids":{"type":"boolean","description":"`status.selfDeclaredMadeForKids`: whether the video is made for children (US COPPA). Required and never defaulted, because it is the uploader's declaration."},"tags":{"type":"array","items":{"type":"string","pattern":"^[^,]*$"},"description":"`snippet.tags`. No commas or < > inside a tag. At most 500 characters in total, counted as YouTube does: each tag's characters, plus 2 for a tag containing a space (YouTube quotes it), plus one per comma between tags."},"categoryId":{"type":"string","pattern":"^[0-9]{1,4}$","description":"Omit to let YouTube decide."},"format":{"type":"string","enum":["video","short"],"description":"Default `video`. `short` is checked before upload (vertical or square, at most 3 minutes); YouTube decides from the file itself."},"notifySubscribers":{"type":"boolean","description":"Default true."},"containsSyntheticMedia":{"type":"boolean","description":"Realistic altered or AI-made content disclosure. Default false."}}},"TikTokVideoSettings":{"type":"object","additionalProperties":false,"required":["type","privacyLevel"],"example":{"type":"tiktok","privacyLevel":"SELF_ONLY","disableDuet":true,"aiGenerated":false},"description":"For a `tiktok` or `tiktok_business` channel. The request's `description` is the caption (TikTok has no separate video title). Before posting, OmniPosts reads the creator's current TikTok settings: `privacyLevel` must be one this account offers, a video longer than the account's limit is refused, and interactions the creator switched off in TikTok stay off whatever is sent here. Those refusals show as the video's `failure`.","properties":{"type":{"type":"string","enum":["tiktok"]},"mode":{"type":"string","enum":["direct"],"description":"Post directly (the default). See `TikTokDraftSettings` for drafts."},"privacyLevel":{"type":"string","enum":["PUBLIC_TO_EVERYONE","MUTUAL_FOLLOW_FRIENDS","FOLLOWER_OF_CREATOR","SELF_ONLY"],"description":"Who can see the video. Required, with no default: TikTok requires the person to choose it each time. Until TikTok audits OmniPosts, TikTok allows only `SELF_ONLY` (anything else is a 422 `nothing_to_publish`). TikTok for Business channels publish videos to everyone, so they take only `PUBLIC_TO_EVERYONE`."},"disableComment":{"type":"boolean","description":"Turn off comments. Default false."},"disableDuet":{"type":"boolean","description":"Turn off Duet. Default false."},"disableStitch":{"type":"boolean","description":"Turn off Stitch. Default false."},"commercialContent":{"type":"boolean","description":"TikTok's commercial content disclosure switch. When true, `brandedContent`, `yourBrand` or both must be true (400 otherwise: \"You need to indicate if your content promotes yourself, a third party, or both.\"). Default false."},"brandedContent":{"type":"boolean","description":"Paid partnership: the video promotes another brand in exchange for something of value. TikTok labels it \"Paid partnership\", and it cannot be `SELF_ONLY`. Default false."},"yourBrand":{"type":"boolean","description":"Promotes your own business; TikTok labels it \"Promotional content\". Default false."},"aiGenerated":{"type":"boolean","description":"Label the video as AI-generated. Default false."},"coverTimestampMs":{"type":"integer","minimum":0,"maximum":2147483647,"description":"TikTok channels only: the frame, in milliseconds from the start, to use as the cover. Omit for the first frame. A moment after the end of the video fails the video (`cover_after_end`) instead of TikTok silently using the first frame."}}},"TikTokDraftSettings":{"type":"object","additionalProperties":false,"required":["type","mode"],"example":{"type":"tiktok","mode":"draft"},"description":"TikTok only (not TikTok for Business): upload the video to the creator's TikTok inbox instead of posting it. TikTok notifies the creator, who adds the caption and settings and posts it in TikTok. A draft carries only the video, so it takes no other settings and the request's `description` must be empty (400 otherwise). The video is `published` once it is in the inbox, with no `externalUrl`. TikTok allows at most 5 pending drafts from apps per account in 24 hours (failure `pending_draft_cap`). The channel needs the `video.upload` permission; channels connected before it was requested must reconnect.","properties":{"type":{"type":"string","enum":["tiktok"]},"mode":{"type":"string","enum":["draft"]}}},"Video":{"type":"object","additionalProperties":false,"required":["id","targetId","channelId","providerId","status","description","mediaId","scheduledFor","timezone","failure","externalId","externalUrl","publishedAt","attempt","createdAt","updatedAt"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"targetId":{"$ref":"#/components/schemas/Uuid"},"channelId":{"$ref":"#/components/schemas/Uuid"},"providerId":{"type":"string"},"status":{"$ref":"#/components/schemas/TargetStatus"},"description":{"type":"string","nullable":true},"mediaId":{"type":"string","format":"uuid","nullable":true},"scheduledFor":{"$ref":"#/components/schemas/NullableInstant"},"timezone":{"type":"string","nullable":true},"failure":{"anyOf":[{"$ref":"#/components/schemas/Failure"},{"$ref":"#/components/schemas/Null"}]},"externalId":{"type":"string","nullable":true},"externalUrl":{"type":"string","nullable":true},"publishedAt":{"$ref":"#/components/schemas/NullableInstant"},"attempt":{"type":"integer","minimum":0},"createdAt":{"$ref":"#/components/schemas/Instant"},"updatedAt":{"$ref":"#/components/schemas/Instant"}}},"GroupEditRequest":{"type":"object","additionalProperties":false,"properties":{"body":{"type":"string"},"scheduledFor":{"$ref":"#/components/schemas/Instant"}}},"Target":{"type":"object","additionalProperties":false,"required":["id","groupId","channelId","providerId","status","failure","externalId","externalUrl","publishedAt","attempt","body"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"groupId":{"$ref":"#/components/schemas/Uuid"},"channelId":{"$ref":"#/components/schemas/Uuid"},"providerId":{"type":"string"},"status":{"$ref":"#/components/schemas/TargetStatus"},"failure":{"anyOf":[{"$ref":"#/components/schemas/Failure"},{"$ref":"#/components/schemas/Null"}]},"externalId":{"type":"string","nullable":true},"externalUrl":{"type":"string","nullable":true},"publishedAt":{"$ref":"#/components/schemas/NullableInstant"},"attempt":{"type":"integer","minimum":0},"body":{"type":"string","nullable":true,"description":"This target's body override; null when it posts the group body."},"thread":{"description":"This channel's own further parts; null when it posts the group's.","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/ThreadPart"}},"threadState":{"description":"How far a thread got; null for a post without a thread or one not yet sent.","type":"object","nullable":true,"additionalProperties":false,"required":["parts","posted","failure"],"properties":{"parts":{"type":"integer","description":"Further parts asked for."},"posted":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["externalId","externalUrl"],"properties":{"externalId":{"type":"string","nullable":true},"externalUrl":{"type":"string","nullable":true}}}},"failure":{"description":"Set when the thread stopped early; the first part and `posted` are live.","anyOf":[{"$ref":"#/components/schemas/Failure"},{"$ref":"#/components/schemas/Null"}]}}}}},"InboxTarget":{"description":"A Target plus its group's scheduledFor. Here `body` is the text this target posts (its override, else the group body).","type":"object","additionalProperties":false,"required":["id","groupId","channelId","providerId","status","failure","externalId","externalUrl","publishedAt","attempt","body","scheduledFor"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"groupId":{"$ref":"#/components/schemas/Uuid"},"channelId":{"$ref":"#/components/schemas/Uuid"},"providerId":{"type":"string"},"status":{"$ref":"#/components/schemas/TargetStatus"},"failure":{"anyOf":[{"$ref":"#/components/schemas/Failure"},{"$ref":"#/components/schemas/Null"}]},"externalId":{"type":"string","nullable":true},"externalUrl":{"type":"string","nullable":true},"publishedAt":{"$ref":"#/components/schemas/NullableInstant"},"attempt":{"type":"integer","minimum":0},"body":{"type":"string","nullable":true},"thread":{"description":"This channel's own further parts; null when it posts the group's.","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/ThreadPart"}},"threadState":{"description":"How far a thread got; null for a post without a thread or one not yet sent.","type":"object","nullable":true,"additionalProperties":false,"required":["parts","posted","failure"],"properties":{"parts":{"type":"integer","description":"Further parts asked for."},"posted":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["externalId","externalUrl"],"properties":{"externalId":{"type":"string","nullable":true},"externalUrl":{"type":"string","nullable":true}}}},"failure":{"description":"Set when the thread stopped early; the first part and `posted` are live.","anyOf":[{"$ref":"#/components/schemas/Failure"},{"$ref":"#/components/schemas/Null"}]}}},"scheduledFor":{"$ref":"#/components/schemas/NullableInstant"}}},"Group":{"type":"object","additionalProperties":false,"required":["id","status","body","mediaIds","scheduledFor","timezone","queued","createdAt","updatedAt","targets"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"status":{"$ref":"#/components/schemas/GroupStatus"},"body":{"type":"string","nullable":true},"mediaIds":{"type":"array","items":{"$ref":"#/components/schemas/Uuid"}},"scheduledFor":{"$ref":"#/components/schemas/NullableInstant"},"timezone":{"type":"string","nullable":true},"queued":{"type":"boolean","description":"Placed by the posting queue. A time set by hand (PATCH) clears it."},"createdAt":{"$ref":"#/components/schemas/Instant"},"updatedAt":{"$ref":"#/components/schemas/Instant"},"targets":{"type":"array","items":{"$ref":"#/components/schemas/Target"}},"thread":{"description":"The group's further parts; null when the post is not a thread.","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/ThreadPart"}}}},"GroupCreated":{"description":"A Group plus the targets rejected at creation (empty on 201).","type":"object","additionalProperties":false,"required":["id","status","body","mediaIds","scheduledFor","timezone","queued","createdAt","updatedAt","targets","rejections"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"status":{"$ref":"#/components/schemas/GroupStatus"},"body":{"type":"string","nullable":true},"mediaIds":{"type":"array","items":{"$ref":"#/components/schemas/Uuid"}},"scheduledFor":{"$ref":"#/components/schemas/NullableInstant"},"timezone":{"type":"string","nullable":true},"queued":{"type":"boolean","description":"Placed by the posting queue. A time set by hand (PATCH) clears it."},"createdAt":{"$ref":"#/components/schemas/Instant"},"updatedAt":{"$ref":"#/components/schemas/Instant"},"targets":{"type":"array","items":{"$ref":"#/components/schemas/Target"}},"thread":{"description":"The group's further parts; null when the post is not a thread.","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/ThreadPart"}},"rejections":{"type":"array","items":{"$ref":"#/components/schemas/Rejection"}}}},"GroupPage":{"type":"object","additionalProperties":false,"required":["items","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Group"}},"nextCursor":{"type":"string","format":"uuid","nullable":true}}},"InboxPage":{"type":"object","additionalProperties":false,"required":["items","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/InboxTarget"}},"nextCursor":{"type":"string","format":"uuid","nullable":true}}},"CancelResult":{"type":"object","additionalProperties":false,"required":["cancelled","refused"],"properties":{"cancelled":{"description":"Channel ids of the targets cancelled.","type":"array","items":{"$ref":"#/components/schemas/Uuid"}},"refused":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["channelId","status","reason"],"properties":{"channelId":{"$ref":"#/components/schemas/Uuid"},"status":{"$ref":"#/components/schemas/TargetStatus"},"reason":{"type":"string","enum":["already_dispatched","already_finished"]}}}}}},"Media":{"type":"object","additionalProperties":false,"required":["id","mime","bytes","width","height","altText","checks"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"mime":{"type":"string","enum":["image/jpeg","image/png","image/gif","image/webp","video/mp4","video/quicktime"]},"bytes":{"type":"integer","format":"int64"},"width":{"type":"integer","nullable":true},"height":{"type":"integer","nullable":true},"altText":{"type":"string","nullable":true},"checks":{"description":"Per enabled provider id, that provider's objections to this file.","type":"object","additionalProperties":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}}},"MediaPage":{"type":"object","additionalProperties":false,"required":["items","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Media"}},"nextCursor":{"type":"string","format":"uuid","nullable":true}}},"DeliveryReport":{"type":"object","additionalProperties":false,"required":["window","totals","byChannel","mediaPreflight"],"properties":{"window":{"type":"object","additionalProperties":false,"required":["from","to"],"properties":{"from":{"$ref":"#/components/schemas/Instant"},"to":{"$ref":"#/components/schemas/Instant"}}},"totals":{"type":"object","additionalProperties":false,"required":["groups","targets","published","failed","cancelled","scheduled"],"properties":{"groups":{"type":"integer"},"targets":{"type":"integer"},"published":{"type":"integer"},"failed":{"type":"integer"},"cancelled":{"type":"integer"},"scheduled":{"type":"integer"}}},"byChannel":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["channel","provider","published","failed","successRate","failuresByClass","scheduleAdherence","cadence"],"properties":{"channel":{"$ref":"#/components/schemas/Uuid"},"provider":{"type":"string"},"published":{"type":"integer"},"failed":{"type":"integer"},"successRate":{"type":"number","nullable":true,"minimum":0,"maximum":1},"failuresByClass":{"type":"object","additionalProperties":false,"required":["RECONNECT","RETRYABLE","UNKNOWN","USER_ERROR","PLATFORM_BLOCKED"],"properties":{"RECONNECT":{"type":"integer"},"RETRYABLE":{"type":"integer"},"UNKNOWN":{"type":"integer"},"USER_ERROR":{"type":"integer"},"PLATFORM_BLOCKED":{"type":"integer"}}},"scheduleAdherence":{"type":"object","additionalProperties":false,"required":["p50Seconds","p95Seconds","lateOver5min","missed"],"properties":{"p50Seconds":{"type":"integer","nullable":true},"p95Seconds":{"type":"integer","nullable":true},"lateOver5min":{"type":"integer"},"missed":{"type":"integer"}}},"cadence":{"type":"object","additionalProperties":false,"required":["postsPerWeek"],"properties":{"postsPerWeek":{"type":"number"}}}}}},"mediaPreflight":{"type":"object","additionalProperties":false,"required":["attempted","rejected","rate"],"properties":{"attempted":{"type":"integer"},"rejected":{"type":"integer"},"rate":{"type":"number","nullable":true}}}}},"MetricName":{"type":"string","enum":["reactions","reposts","comments","quotes","zaps"]},"Availability":{"type":"string","enum":["observed","not_supported","not_yet_collected"]},"ReasonKind":{"type":"string","nullable":true,"enum":["platform_hides","not_collected","unverified","collection_stopped","window_closed","private_post","reconnect","paid_plan","business_only"],"description":"Machine-readable kind of `reason`, from a closed set; null when the number is available (or still being collected). `platform_hides` — the platform does not share it with apps; `not_collected` — OmniPosts does not collect it yet; `unverified` — not yet confirmed reliable (or the provider's metrics flag is off); `collection_stopped` — collection ended for this post; `window_closed` — the platform only shares it for a limited time and that time has passed (last value shown); `private_post` — not reported for private posts; `reconnect` — reconnect the account and allow statistics; `paid_plan` — only on the platform's paid plan; `business_only` — only for business accounts."},"MetricCell":{"type":"object","additionalProperties":false,"required":["metric","value","availability","reason","reasonKind","observedAt"],"properties":{"metric":{"$ref":"#/components/schemas/ExtendedMetricName"},"value":{"type":"integer","format":"int64","nullable":true},"availability":{"$ref":"#/components/schemas/Availability"},"reason":{"type":"string","nullable":true},"reasonKind":{"$ref":"#/components/schemas/ReasonKind"},"observedAt":{"$ref":"#/components/schemas/NullableInstant"}}},"MetricsRow":{"type":"object","additionalProperties":false,"required":["targetId","channelId","providerId","publishedAt","externalUrl","groupId","text","scope","metrics"],"properties":{"targetId":{"$ref":"#/components/schemas/Uuid"},"channelId":{"$ref":"#/components/schemas/Uuid"},"providerId":{"type":"string"},"publishedAt":{"$ref":"#/components/schemas/Instant"},"externalUrl":{"type":"string","nullable":true},"groupId":{"type":"string","format":"uuid","description":"The post group (one composed post) this target belongs to; rows sharing it are one post on several channels."},"text":{"type":"string","nullable":true,"maxLength":140,"description":"What the post said: the target's own text, else the group body; whitespace collapsed, cut to 140 characters (ellipsis included). Null when the post had no text."},"scope":{"type":"string","enum":["appview_global","home_instance","relay_set","channel_admin_push","first_party"]},"metrics":{"description":"By default exactly reactions, reposts, comments, quotes, zaps — in that order. With `metrics=extended`, all twelve post metrics in `ExtendedMetricName` order.","type":"array","minItems":5,"maxItems":12,"items":{"$ref":"#/components/schemas/MetricCell"}}}},"MetricsPage":{"type":"object","additionalProperties":false,"required":["items","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MetricsRow"}},"nextCursor":{"type":"string","format":"uuid","nullable":true}}},"Series":{"type":"object","additionalProperties":false,"required":["metric","points","availability","reason","reasonKind"],"properties":{"metric":{"$ref":"#/components/schemas/MetricName"},"points":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["at","value"],"properties":{"at":{"$ref":"#/components/schemas/Instant"},"value":{"type":"integer","format":"int64"}}}},"availability":{"$ref":"#/components/schemas/Availability"},"reason":{"type":"string","nullable":true},"reasonKind":{"$ref":"#/components/schemas/ReasonKind"}}},"PostSeries":{"type":"object","additionalProperties":false,"required":["firstSixHours","lifetime"],"properties":{"firstSixHours":{"type":"array","items":{"$ref":"#/components/schemas/Series"}},"lifetime":{"type":"array","items":{"$ref":"#/components/schemas/Series"}}}},"ExtendedMetricName":{"type":"string","enum":["reactions","comments","reposts","quotes","shares","saves","zaps","views","impressions","reach","link_clicks","profile_visits"]},"AccountMetricName":{"type":"string","enum":["followers","following","posts","likes_received"]},"Window":{"type":"object","additionalProperties":false,"required":["from","to"],"properties":{"from":{"$ref":"#/components/schemas/Instant"},"to":{"$ref":"#/components/schemas/Instant"}}},"SeriesPoint":{"type":"object","additionalProperties":false,"required":["at","value"],"properties":{"at":{"$ref":"#/components/schemas/Instant"},"value":{"type":"integer","format":"int64"}}},"ExtendedSeries":{"type":"object","additionalProperties":false,"required":["metric","points","availability","reason","reasonKind"],"properties":{"metric":{"$ref":"#/components/schemas/ExtendedMetricName"},"points":{"type":"array","items":{"$ref":"#/components/schemas/SeriesPoint"}},"availability":{"$ref":"#/components/schemas/Availability"},"reason":{"type":"string","nullable":true},"reasonKind":{"$ref":"#/components/schemas/ReasonKind"}}},"ExtendedPostSeries":{"type":"object","additionalProperties":false,"required":["firstSixHours","lifetime"],"properties":{"firstSixHours":{"type":"array","minItems":12,"maxItems":12,"items":{"$ref":"#/components/schemas/ExtendedSeries"}},"lifetime":{"type":"array","minItems":12,"maxItems":12,"items":{"$ref":"#/components/schemas/ExtendedSeries"}}}},"MetricTotal":{"type":"object","additionalProperties":false,"required":["metric","total","postsObserved","availability","reason","reasonKind"],"properties":{"metric":{"$ref":"#/components/schemas/ExtendedMetricName"},"total":{"type":"integer","format":"int64","nullable":true,"description":"Null unless observed; never 0 for not_supported."},"postsObserved":{"type":"integer"},"availability":{"$ref":"#/components/schemas/Availability"},"reason":{"type":"string","nullable":true},"reasonKind":{"$ref":"#/components/schemas/ReasonKind"}}},"Engagement":{"type":"object","additionalProperties":false,"required":["interactions","denominator","denominatorTotal","rate","posts"],"properties":{"interactions":{"type":"integer","format":"int64","nullable":true,"description":"Reactions + comments + reposts + quotes + shares + saves, over every post that has any."},"denominator":{"type":"string","enum":["impressions","views"],"nullable":true},"denominatorTotal":{"type":"integer","format":"int64","nullable":true},"rate":{"type":"number","nullable":true,"description":"Over the posts carrying the denominator only; null when there is none."},"posts":{"type":"integer","description":"Posts the rate is computed over."}}},"ChannelPerformance":{"type":"object","additionalProperties":false,"required":["channel","provider","posts","metrics","engagement"],"properties":{"channel":{"$ref":"#/components/schemas/Uuid"},"provider":{"type":"string"},"posts":{"type":"integer"},"metrics":{"type":"array","minItems":12,"maxItems":12,"items":{"$ref":"#/components/schemas/MetricTotal"}},"engagement":{"$ref":"#/components/schemas/Engagement"}}},"TopPost":{"type":"object","additionalProperties":false,"required":["targetId","channelId","providerId","publishedAt","externalUrl","groupId","text","interactions","denominator","denominatorValue"],"properties":{"targetId":{"$ref":"#/components/schemas/Uuid"},"channelId":{"$ref":"#/components/schemas/Uuid"},"providerId":{"type":"string"},"publishedAt":{"$ref":"#/components/schemas/Instant"},"externalUrl":{"type":"string","nullable":true},"groupId":{"type":"string","format":"uuid","description":"The post group (one composed post) this target belongs to; rows sharing it are one post on several channels."},"text":{"type":"string","nullable":true,"maxLength":140,"description":"What the post said: the target's own text, else the group body; whitespace collapsed, cut to 140 characters (ellipsis included). Null when the post had no text."},"interactions":{"type":"integer","format":"int64"},"denominator":{"type":"string","enum":["impressions","views"],"nullable":true},"denominatorValue":{"type":"integer","format":"int64","nullable":true}}},"PerformanceReport":{"type":"object","additionalProperties":false,"required":["window","channels","topPosts"],"properties":{"window":{"$ref":"#/components/schemas/Window"},"channels":{"type":"array","items":{"$ref":"#/components/schemas/ChannelPerformance"}},"topPosts":{"type":"array","maxItems":5,"items":{"$ref":"#/components/schemas/TopPost"}}}},"AudienceSeries":{"type":"object","additionalProperties":false,"required":["metric","points","first","last","change","availability","reason","reasonKind"],"properties":{"metric":{"$ref":"#/components/schemas/AccountMetricName"},"points":{"type":"array","items":{"$ref":"#/components/schemas/SeriesPoint"}},"first":{"type":"integer","format":"int64","nullable":true},"last":{"type":"integer","format":"int64","nullable":true},"change":{"type":"integer","format":"int64","nullable":true},"availability":{"$ref":"#/components/schemas/Availability"},"reason":{"type":"string","nullable":true},"reasonKind":{"$ref":"#/components/schemas/ReasonKind"}}},"ChannelAudience":{"type":"object","additionalProperties":false,"required":["channel","provider","metrics"],"properties":{"channel":{"$ref":"#/components/schemas/Uuid"},"provider":{"type":"string"},"metrics":{"type":"array","minItems":4,"maxItems":4,"items":{"$ref":"#/components/schemas/AudienceSeries"}}}},"AudienceReport":{"type":"object","additionalProperties":false,"required":["window","channels"],"properties":{"window":{"$ref":"#/components/schemas/Window"},"channels":{"type":"array","items":{"$ref":"#/components/schemas/ChannelAudience"}}}},"Consent":{"type":"object","additionalProperties":false,"required":["purpose","noticeVersion","grantedAt","withdrawnAt"],"properties":{"purpose":{"type":"string"},"noticeVersion":{"type":"string"},"grantedAt":{"$ref":"#/components/schemas/Instant"},"withdrawnAt":{"$ref":"#/components/schemas/NullableInstant"}}},"ConsentList":{"type":"object","additionalProperties":false,"required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Consent"}}}},"Export":{"description":"Section rows are Postgres row_to_json renderings (snake_case columns); their shape is the database's, not part of this contract.","type":"object","additionalProperties":false,"required":["exportedAt","format","organization","users","channels","consents","ideas","groups","postingSlots","media","metrics","webhooks","auditEvents"],"properties":{"exportedAt":{"$ref":"#/components/schemas/Instant"},"format":{"type":"string","enum":["neslivis-export/1"]},"organization":{"type":"object"},"users":{"type":"array","items":{"type":"object"}},"channels":{"type":"array","items":{"type":"object"}},"consents":{"type":"array","items":{"type":"object"}},"ideas":{"type":"array","items":{"type":"object"}},"groups":{"type":"array","items":{"type":"object"}},"postingSlots":{"type":"array","items":{"type":"object"}},"media":{"type":"array","items":{"type":"object"}},"metrics":{"type":"array","items":{"type":"object"}},"webhooks":{"type":"array","items":{"type":"object"}},"auditEvents":{"type":"array","items":{"type":"object"}}}},"QueueDay":{"type":"string","enum":["mon","tue","wed","thu","fri","sat","sun"]},"QueueSlot":{"type":"object","additionalProperties":false,"required":["day","time"],"properties":{"day":{"$ref":"#/components/schemas/QueueDay"},"time":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Local time, HH:MM."}}},"QueueRequest":{"type":"object","additionalProperties":false,"required":["timezone","slots"],"properties":{"timezone":{"type":"string","description":"IANA time zone, e.g. Europe/London."},"slots":{"type":"array","items":{"$ref":"#/components/schemas/QueueSlot"}}}},"Queue":{"type":"object","additionalProperties":false,"required":["timezone","slots","upcoming"],"properties":{"timezone":{"type":"string"},"slots":{"type":"array","items":{"$ref":"#/components/schemas/QueueSlot"}},"upcoming":{"type":"array","items":{"$ref":"#/components/schemas/Instant"},"description":"The next free slot instants, earliest first."}}},"PlanId":{"type":"string","enum":["free","creator","pro"]},"Plan":{"type":"object","additionalProperties":false,"required":["id","name","summary","channels","postsPerMonth","beta"],"properties":{"id":{"$ref":"#/components/schemas/PlanId"},"beta":{"type":"boolean","description":"Not open yet: cannot be switched onto; the pricing page offers a waitlist."},"name":{"type":"string"},"summary":{"type":"string"},"channels":{"type":"integer","description":"Live connected channels at once."},"postsPerMonth":{"type":"integer","nullable":true,"description":"Posts created per UTC calendar month; null means no limit."}}},"PlanState":{"type":"object","additionalProperties":false,"required":["billing","plan","usage","plans"],"properties":{"billing":{"type":"string","enum":["early_access"]},"plan":{"$ref":"#/components/schemas/Plan"},"usage":{"type":"object","additionalProperties":false,"required":["channels","postsThisMonth"],"properties":{"channels":{"type":"integer"},"postsThisMonth":{"type":"integer"}}},"plans":{"type":"array","items":{"$ref":"#/components/schemas/Plan"}}}},"PlanChangeRequest":{"type":"object","additionalProperties":false,"required":["plan"],"properties":{"plan":{"type":"string"}}},"DeleteAccountRequest":{"type":"object","additionalProperties":false,"properties":{"confirm":{"type":"string","description":"Must be exactly DELETE."}}},"DeletionAccepted":{"type":"object","additionalProperties":false,"required":["confirmationCode"],"properties":{"confirmationCode":{"type":"string","minLength":1}}},"WebhookEvent":{"type":"string","enum":["post.published","post.failed","token.expired","channel.disconnected"]},"WebhookRequest":{"type":"object","additionalProperties":false,"required":["url","events"],"properties":{"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}}}},"Webhook":{"type":"object","additionalProperties":false,"required":["id","url","events","active","consecutiveFailures","createdAt","disabledAt"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"url":{"type":"string"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"}},"active":{"type":"boolean"},"consecutiveFailures":{"type":"integer","minimum":0},"createdAt":{"$ref":"#/components/schemas/Instant"},"disabledAt":{"$ref":"#/components/schemas/NullableInstant"}}},"WebhookCreated":{"type":"object","additionalProperties":false,"required":["id","url","events","active","consecutiveFailures","createdAt","disabledAt","secret"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"url":{"type":"string"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"}},"active":{"type":"boolean"},"consecutiveFailures":{"type":"integer","minimum":0},"createdAt":{"$ref":"#/components/schemas/Instant"},"disabledAt":{"$ref":"#/components/schemas/NullableInstant"},"secret":{"type":"string","pattern":"^whsec_"}}},"WebhookList":{"type":"object","additionalProperties":false,"required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}},"WebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","status","responseStatus","attempt","createdAt"],"properties":{"id":{"$ref":"#/components/schemas/Uuid"},"event":{"$ref":"#/components/schemas/WebhookEvent"},"status":{"type":"string","enum":["pending","delivered","failed"]},"responseStatus":{"type":"integer","nullable":true},"attempt":{"type":"integer","minimum":0},"createdAt":{"$ref":"#/components/schemas/Instant"}}},"DeliveryPage":{"type":"object","additionalProperties":false,"required":["items","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"nextCursor":{"type":"string","format":"uuid","nullable":true}}},"SignedRequestForm":{"type":"object","properties":{"signed_request":{"type":"string","description":"base64url(sig).base64url(payload), HMAC-SHA256 with the Meta app secret."}}},"MetaDeletionReceipt":{"type":"object","additionalProperties":false,"required":["url","confirmation_code"],"properties":{"url":{"type":"string","description":"<public-base-url>/data-deletion/status?code=<code>"},"confirmation_code":{"type":"string"}}},"Health":{"type":"object","additionalProperties":false,"required":["status","db","workers","connectors"],"properties":{"status":{"type":"string","enum":["ok"]},"db":{"type":"string","enum":["ok"]},"workers":{"type":"object","additionalProperties":false,"required":["live","stale"],"properties":{"live":{"type":"integer","minimum":0},"stale":{"type":"integer","minimum":0}}},"connectors":{"description":"Compact connector roll-up; {\"status\": \"unavailable\"} when the summary query itself fails (the service is up, connector state is unknown).","oneOf":[{"$ref":"#/components/schemas/ConnectorSummary"},{"$ref":"#/components/schemas/ConnectorsUnavailable"}]}}},"HealthDown":{"type":"object","additionalProperties":false,"required":["status","db"],"properties":{"status":{"type":"string","enum":["down","no_worker"]},"db":{"type":"string","enum":["unreachable","ok"]},"workers":{"type":"object","additionalProperties":false,"required":["live","stale"],"properties":{"live":{"type":"integer","minimum":0},"stale":{"type":"integer","minimum":0}}},"connectors":{"oneOf":[{"$ref":"#/components/schemas/ConnectorSummary"},{"$ref":"#/components/schemas/ConnectorsUnavailable"}]}}},"ConnectorSummary":{"type":"object","additionalProperties":false,"required":["status","broken","degraded","stale","deprecations"],"properties":{"status":{"type":"string","enum":["ok","degraded","broken"]},"broken":{"type":"array","items":{"type":"string"}},"degraded":{"type":"array","items":{"type":"string"}},"stale":{"type":"array","items":{"type":"string"}},"deprecations":{"type":"array","items":{"type":"string"}}}},"ConnectorsUnavailable":{"type":"object","additionalProperties":false,"required":["status"],"properties":{"status":{"type":"string","enum":["unavailable"]}}},"ConnectorHealth":{"type":"object","additionalProperties":false,"required":["summary","connectors"],"properties":{"summary":{"$ref":"#/components/schemas/ConnectorSummary"},"connectors":{"type":"array","items":{"$ref":"#/components/schemas/ConnectorEntry"}}}},"ConnectorEntry":{"type":"object","additionalProperties":false,"required":["id","tier","apiVersion","probeSupported","descriptorCheckedDate","stale","probe","anomaly","openAlerts","deprecations","state"],"properties":{"id":{"type":"string"},"tier":{"type":"string","enum":["free","gated"]},"apiVersion":{"type":"string"},"probeSupported":{"type":"boolean"},"descriptorCheckedDate":{"type":"string","format":"date"},"stale":{"type":"boolean"},"probe":{"type":"object","additionalProperties":false,"required":["lastProbeAt","lastVerdict","lastOkAt","consecutiveFailedRuns","lastRun"],"properties":{"lastProbeAt":{"type":"string","format":"date-time","nullable":true},"lastVerdict":{"type":"string","enum":["ok","degraded","failed","inconclusive"],"nullable":true},"lastOkAt":{"type":"string","format":"date-time","nullable":true},"consecutiveFailedRuns":{"type":"integer","minimum":0},"lastRun":{"type":"object"}}},"anomaly":{"type":"object","additionalProperties":false,"required":["state","checkedAt"],"properties":{"state":{"type":"string","enum":["ok","anomalous"]},"checkedAt":{"type":"string","description":"ISO instant, or \"\" when never checked"}}},"openAlerts":{"type":"array","items":{"type":"string"}},"deprecations":{"type":"array","items":{"$ref":"#/components/schemas/DeprecationNotice"}},"state":{"type":"string","enum":["ok","degraded","broken","unknown"]}}},"DeprecationNotice":{"description":"Either a platform deprecation header ({host, header, value, seenAt}) or a probe observation ({source: probe, notice}).","type":"object","additionalProperties":false,"properties":{"host":{"type":"string"},"header":{"type":"string","nullable":true},"value":{"type":"string","nullable":true},"seenAt":{"type":"string","format":"date-time"},"source":{"type":"string","enum":["probe"]},"notice":{"type":"string"}}}}}}