{"openapi":"3.1.0","info":{"title":"JuzPost REST API","version":"1.0.0","summary":"Token-only API behind the `juzpost` command line client.","description":"The machine-facing surface of JuzPost: connect-once device login, draft creation,\nmedia upload to R2, and scheduling posts across connected social accounts.\n\n**Base URL.** `https://www.juzpost.com/api/cli/v1`. Note the `cli` segment:\n`/api/v1` is a different, older surface used by the dashboard and is not this API.\n\n**Authentication.** Every operation requires `Authorization: Bearer <token>`. There are no\nunauthenticated operations.\n\n## Get a token\n\n1. Open **[Settings, General](https://www.juzpost.com/dashboard/general)** in the JuzPost\n   dashboard.\n2. Find the **API keys** card and choose **Generate API key**.\n3. Copy the key. It begins with `jp_live_` and is shown **once**, at creation. Store it in\n   your secret manager before leaving the page.\n4. Check it works:\n\n   ```bash\n   curl https://www.juzpost.com/api/cli/v1/me \\\n     --header \"Authorization: Bearer jp_live_YOUR_KEY\"\n   ```\n\n   A `200` comes back with your account, plan and capabilities. A `401` means the key is\n   wrong, revoked, or was pasted with a trailing newline.\n\nRevoke a key from the same card. Nothing in this API can delete the credential it is\ncalling with, which is deliberate: an automated client should not be able to lock itself\nout, and revocation should be a human action.\n\n**One token, one account.** A token belongs to exactly one JuzPost account (a *workspace*).\nEvery operation acts on that one account, and no operation anywhere accepts an account id.\n\n**Entitlements.** Every operation other than `GET /me` requires the\ncaller's plan to include the `api` capability, and `POST /schedule`,\n`POST /posts/{postId}/schedule` and `POST /storage/presign` additionally require `schedule`\n(or `publish`, for immediate posting). A plan that lacks the capability produces `402`,\nnever `403`. Call `GET /me` first to check before attempting a write that is doomed.\n\n**Errors.** Every non-2xx response uses the same envelope: a single `error` object carrying\na stable machine-readable `code`, a human `message`, and optional `details`. Never branch on\nthe message text; branch on `error.code` or the HTTP status.\n\n**Unknown ids.** An id that is not yours is reported as `404`, never `403`, so no operation\ncan be used to discover whether someone else's data exists.\n","contact":{"name":"JuzPost Support","url":"https://www.juzpost.com/support"},"license":{"name":"Proprietary","url":"https://www.juzpost.com/terms"}},"servers":[{"url":"https://www.juzpost.com/api/cli/v1","description":"The only supported base URL."}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Identity","description":"Who the calling token belongs to, and what its plan allows."},{"name":"Accounts","description":"Connected social accounts and the named groups that bundle them."},{"name":"Posts","description":"Create, read, update, and delete draft posts."},{"name":"Scheduling","description":"Put drafts on a publishing timer, one post at a time or in bulk."},{"name":"Storage","description":"Presigned direct-to-R2 media upload and download URLs."}],"paths":{"/me":{"get":{"operationId":"getMe","summary":"Show who the token belongs to and what its plan allows","description":"Returns the calling user, the credential, the workspace and its scheduling defaults,\nthe plan, billing state, the entitlements that gate every other operation, quota usage,\nthe limits in force and a `blocked` list of plain-English reasons a write would fail.\nThis is the only authenticated operation that is not itself gated on the `api`\ncapability, so call it first to discover whether a schedule or upload would be\nrejected with `402`. The same shape backs the MCP `get_context` tool.\n","tags":["Identity"],"responses":{"200":{"description":"Identity, credential, workspace, plan, billing, entitlements, quotas, limits and blockers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/workspace":{"get":{"operationId":"getWorkspace","summary":"Get the workspace timezone and preset posting times","description":"Returns the workspace the token belongs to. The `timezone` and `defaultTimes` are the\ninputs the bulk scheduler uses when you do not override them, so read this before\ncalling `POST /schedule` if you want to predict the slots it will pick.\n","tags":["Identity"],"responses":{"200":{"description":"The account this token belongs to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"description":"The account behind this token no longer exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/accounts":{"get":{"operationId":"listAccounts","summary":"List your connected social accounts","description":"Returns connected social accounts, newest first by default. Use the returned `id` values\nas `socialAccountIds` when scheduling. Access and refresh tokens are never included in\nthe response. Results are cursor-paginated: pass `pagination.nextCursor` back as\n`cursor` to get the next page, and stop when `pagination.hasMore` is false.\n","tags":["Accounts"],"parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Order"},{"$ref":"#/components/parameters/AccountSort"},{"$ref":"#/components/parameters/PlatformFilter"},{"$ref":"#/components/parameters/GroupIdFilter"}],"responses":{"200":{"description":"One page of connected accounts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/groups":{"get":{"operationId":"listGroups","summary":"List the named account groups you can schedule to","description":"Returns channel groups: saved bundles of social accounts, used so you can schedule to\n\"all-tiktok\" instead of listing account ids. The group `name` is what\n`POST /schedule` accepts in its `group` field. Cursor-paginated.\n","tags":["Accounts"],"parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Order"},{"$ref":"#/components/parameters/GroupSort"},{"$ref":"#/components/parameters/NameFilter"}],"responses":{"200":{"description":"One page of channel groups.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/groups/{groupId}":{"get":{"operationId":"getGroup","summary":"Get one account group and its members","description":"Returns a single channel group with the full list of social account ids in it. A group\nbelonging to someone else is reported as `404`, never `403`.\n","tags":["Accounts"],"parameters":[{"$ref":"#/components/parameters/GroupIdPath"}],"responses":{"200":{"description":"The group and its member account ids.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Group"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/posts":{"get":{"operationId":"listPosts","summary":"List your posts","description":"Returns posts newest first by default, with the post-level fields only. Per-account\ndelivery state lives on `GET /posts/{postId}`. Note that `from` and `to` filter on\ncreation time, not on the scheduled publish time. Sorting by `scheduledFor` silently\nexcludes posts that have no scheduled time, since unscheduled drafts have nothing to\nsort on. Cursor-paginated.\n","tags":["Posts"],"parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Order"},{"$ref":"#/components/parameters/PostSort"},{"$ref":"#/components/parameters/StatusFilter"},{"$ref":"#/components/parameters/AccountIdFilter"},{"$ref":"#/components/parameters/FromFilter"},{"$ref":"#/components/parameters/ToFilter"}],"responses":{"200":{"description":"One page of posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"}}},"post":{"operationId":"createPost","summary":"Create a draft post","description":"Creates a post in `draft` status. Nothing is published or scheduled by this call. Use\n`POST /posts/{postId}/schedule` or `POST /schedule` afterwards. Upload media first via\n`POST /storage/presign` and pass the returned `resolvedKey` values in `mediaUrls`;\nreferencing a key here also claims it so the orphan-upload sweeper will not delete it.\nOnly the returned `id` comes back; read the post with `GET /posts/{postId}`.\n","tags":["Posts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostRequest"}}}},"responses":{"201":{"description":"Draft created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedPost"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"429":{"description":"This workspace has already created 30 posts through the API or an AI connector in\nthe last hour. `error.details` is `{ \"limit\": 30, \"windowSeconds\": 3600 }`. Wait for\nthe window to roll over, or schedule the drafts you already have. Posts created in\nthe dashboard do not count.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/posts/{postId}":{"get":{"operationId":"getPost","summary":"Get one post with its per-account delivery state","description":"Returns the full post including free-form `metadata` and a `targets` array holding one\nentry per social account the post fans out to, each with its own status, fire time and\nerror. Use this to check what actually happened after scheduling, since the post-level\n`status` is only a coarse summary. A post that is not yours is reported as `404`.\n","tags":["Posts"],"parameters":[{"$ref":"#/components/parameters/PostIdPath"}],"responses":{"200":{"description":"The post and its per-account targets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updatePost","summary":"Edit a draft post","description":"Updates a post that is still in `draft` status; once scheduled or published it can no\nlonger be edited and this returns `409`. Every field is optional and omitted fields are\nleft untouched. Send `null` for `mediaUrls` or `coverR2Key` to clear them. `title`,\n`description` and `hashtags` are merged into the post's metadata rather than replacing\nit wholesale. Changing `mediaUrls` clears the Bluesky alt text on accounts that publish\nthe post's files, since each entry describes a file by its position; labels stay.\n","tags":["Posts"],"parameters":[{"$ref":"#/components/parameters/PostIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePostRequest"}}}},"responses":{"200":{"description":"Draft updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedPost"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"operationId":"deletePost","summary":"Delete a draft post","description":"Permanently deletes a post that is still in `draft` status, along with its per-account\ntarget rows. A scheduled or published post cannot be deleted and returns `409`. Media\nthe post referenced is released back to orphan state and will be removed by the upload\nsweeper unless another post claims it.\n","tags":["Posts"],"parameters":[{"$ref":"#/components/parameters/PostIdPath"}],"responses":{"200":{"description":"Draft deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletedPost"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/posts/{postId}/schedule":{"post":{"operationId":"schedulePost","summary":"Schedule or immediately publish one draft","description":"Fans one draft out to between one and ten social accounts. Supply `publishAt` to set a\nfuture fire time, or omit it entirely to publish right now. A future time must be at\nleast five minutes and at most roughly six months out. The response reports one result\nper account; check each entry rather than only the top-level `success`, because a\npartial failure still returns `200`. Immediate publishing requires the `publish`\ncapability while a future time requires `schedule`; either way a plan that lacks it\nreturns `402`.\n","tags":["Scheduling"],"parameters":[{"$ref":"#/components/parameters/PostIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchedulePostRequest"}}}},"responses":{"200":{"description":"Timers created. Inspect `results` per account: a mix of scheduled and failed\ntargets is still reported here rather than as an error status.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResult"}}}},"400":{"description":"Invalid body, a fire time outside the allowed window, an override keyed by an\naccount not in `socialAccountIds`, a non-text post with no media, a caption\nlonger than the target platform allows, or `blueskySettings` for an account that is\nnot on Bluesky, with an alt text count that differs from its files, or with labels on\na text post. Also `wordpressSettings` for an account that is not on WordPress, a\ncategory name the site does not have (the error lists the site's categories), or\n`seo` for an account whose site does not let it write Yoast SEO fields.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"description":"The post does not exist, or at least one requested social account\ndoes not. Which one is deliberately not disclosed.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The post left `draft` status before the timer could be claimed. It is already\nscheduled or uploading. Re-read the post before retrying.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/schedule":{"post":{"operationId":"smartSchedule","summary":"Spread many drafts across your preset posting times","description":"The bulk scheduler. Resolves a group (or an explicit account list) to targets, then\nassigns drafts to your preset wall-clock times, `minPerDay` posts per day,\nwalking forward from `startDate` and skipping any slot less than five minutes away.\nEvery post goes to every resolved account. Set `dryRun` to true to see the plan without\ncreating any timers. Always do this first. At most 50 posts are scheduled per call;\nthe rest come back in `skipped` with a reason, so call again to continue. Targets are\ncapped at 10 accounts.\n","tags":["Scheduling"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartScheduleRequest"}}}},"responses":{"200":{"description":"The plan, plus what was scheduled and what was skipped. Individual entries in\n`scheduled` may carry `status: failed` with an `error`, so check them.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartScheduleResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"description":"The named group does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The request is well-formed but cannot be planned: no accounts resolved, more than\n10 accounts in the group, no posting times available, or `minPerDay` larger than the\nnumber of time slots. The message says which.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/storage/presign":{"post":{"operationId":"presignStorage","summary":"Get a short-lived URL to upload media straight to storage","description":"Issues a presigned URL so file bytes go directly to object storage instead of through\nthis API. For an upload, PUT the raw bytes to `signedUrl` with the same `Content-Type`\nand exactly the `contentLength` you declared. Both are bound into the signature and a\nmismatch is rejected by storage. Then pass the returned `resolvedKey` as a `mediaUrls`\nentry or `coverR2Key` when creating a post. Keys are forced under your own storage\nprefix, so you cannot presign into another tenant's space. Uploads are capped at 500 MB\nfor video, 15 MB for images and 10 MB otherwise, rate-limited to 20 presigns per minute\nper account, and counted against your storage quota.\n","tags":["Storage"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresignRequest"}}}},"responses":{"200":{"description":"A presigned URL valid for `expiresIn` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresignResponse"}}}},"400":{"description":"Invalid body, neither `key` nor `relativeKey` supplied, a key containing characters\noutside `A-Za-z0-9._-/`, or a missing `contentLength` on an upload.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Either the plan lacks the `api`/`schedule` capability, or your storage\nquota is already full. In the quota case `error.details` carries `usedBytes` and\n`quotaBytes`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"The declared `contentLength` exceeds the size cap for that content type.\n`error.details.code` is `file_too_large`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key created in the dashboard under Settings, General, API keys, sent as\n`Authorization: Bearer jp_live_...`. Required on every operation without exception.\nMissing, malformed, revoked or unknown tokens all produce `401` with code\n`unauthorized`.\n"}},"parameters":{"Limit":{"name":"limit","in":"query","required":false,"description":"How many items to return in this page. Values outside 1-100 are clamped rather than\nrejected. Defaults to 20.\n","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},"Cursor":{"name":"cursor","in":"query","required":false,"description":"Opaque continuation token. Omit it for the first page, then pass back the previous\nresponse's `pagination.nextCursor` verbatim. Never parse, construct or modify it, and\ndo not reuse a cursor after changing `sort`, `order` or any filter, which returns `400`\nwith `error.details.code` of `invalid_cursor`.\n","schema":{"type":"string"}},"Order":{"name":"order","in":"query","required":false,"description":"Sort direction. Anything other than the literal `asc` is treated as `desc`.\n","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},"AccountSort":{"name":"sort","in":"query","required":false,"description":"Which field to sort connected accounts by. An unlisted value returns `400`.\n","schema":{"type":"string","enum":["createdAt","username","platform"],"default":"createdAt"}},"GroupSort":{"name":"sort","in":"query","required":false,"description":"Which field to sort groups by. An unlisted value returns `400`.\n","schema":{"type":"string","enum":["createdAt","name"],"default":"createdAt"}},"PostSort":{"name":"sort","in":"query","required":false,"description":"Which field to sort posts by. Choosing `scheduledFor` restricts the result to posts that\nactually have a scheduled time; unscheduled drafts are omitted. An unlisted value\nreturns `400`.\n","schema":{"type":"string","enum":["createdAt","scheduledFor"],"default":"createdAt"}},"PlatformFilter":{"name":"platform","in":"query","required":false,"description":"Return only accounts on this platform, matched exactly, for example `tiktok`,\n`youtube`, `instagram`, `x`, `bluesky`, `pinterest`.\n","schema":{"type":"string"}},"GroupIdFilter":{"name":"groupId","in":"query","required":false,"description":"Return only accounts that are members of this channel group. An unknown or empty group\nyields an empty page rather than an error.\n","schema":{"type":"string","format":"uuid"}},"NameFilter":{"name":"name","in":"query","required":false,"description":"Return only groups whose name contains this substring.","schema":{"type":"string"}},"StatusFilter":{"name":"status","in":"query","required":false,"description":"Return only posts in this status, matched exactly, typically `draft`, `scheduled`,\n`posted` or `failed`.\n","schema":{"type":"string"}},"AccountIdFilter":{"name":"accountId","in":"query","required":false,"description":"Return only posts that target this social account. An account with no posts yields an\nempty page rather than an error.\n","schema":{"type":"string","format":"uuid"}},"FromFilter":{"name":"from","in":"query","required":false,"description":"Return only posts created at or after this instant. This filters on creation time, not\non the scheduled publish time. Any string parseable as a date is accepted; send ISO 8601.\n","schema":{"type":"string","format":"date-time"}},"ToFilter":{"name":"to","in":"query","required":false,"description":"Return only posts created at or before this instant. Filters on creation time, not on\nthe scheduled publish time.\n","schema":{"type":"string","format":"date-time"}},"PostIdPath":{"name":"postId","in":"path","required":true,"description":"Id of the post, as returned by `POST /posts` or `GET /posts`.","schema":{"type":"string","format":"uuid"}},"GroupIdPath":{"name":"groupId","in":"path","required":true,"description":"Id of the channel group, as returned by `GET /groups`.","schema":{"type":"string","format":"uuid"}}},"responses":{"BadRequest":{"description":"The request body or query string failed validation. `error.message` names the first\nproblem found.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"No bearer token was sent, or the token is unknown, revoked or malformed. Run the device\nlogin again to obtain a new token.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PaymentRequired":{"description":"The plan behind this token does not include the capability this operation needs.\n`error.details.requiredPlan` names the tier that does. This is never a token problem, so\ndo not retry without changing the plan.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The token is valid but the request reaches outside what it is allowed to touch, such as\na storage key outside the caller's own storage prefix.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No such resource. A resource that belongs to someone else is\nalso reported here, so this never confirms whether another tenant's id exists.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"The resource is not in a state that allows this operation, typically a post that has\nalready left `draft` status. Re-read the resource before retrying.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Too many requests in a short window. Back off and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"ErrorCode":{"type":"string","description":"Stable machine-readable error identifier. Branch on this rather than on the message.\nEach code maps to exactly one HTTP status.\n","enum":["bad_request","unauthorized","payment_required","forbidden","not_found","conflict","unprocessable","pending","rate_limited","internal"]},"Error":{"type":"object","description":"The envelope returned by every non-2xx response in this API. There is never a top-level\nfield other than `error`.\n","required":["error"],"properties":{"error":{"type":"object","description":"The single error describing why the request did not succeed.","required":["code","message"],"properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string","description":"Human-readable explanation, safe to show to a user. Wording is not stable across\nreleases, so never parse or match on it.\n"},"details":{"description":"Optional structured context whose shape depends on the code, for example\n`{ \"requiredPlan\": \"pro\" }` on a `payment_required`, `{ \"code\":\n\"invalid_cursor\" }` on a bad cursor, or `{ \"usedBytes\": …, \"quotaBytes\": … }`\nwhen a storage quota is full. Treat it as opaque unless a specific operation\ndocuments its contents.\n"}}}}},"PaginationMeta":{"type":"object","description":"Page state returned by every list operation. Keyset-based, so it stays correct while\nrows are being inserted, and it deliberately carries no total count.\n","required":["limit","nextCursor","hasMore"],"properties":{"limit":{"type":"integer","description":"The page size actually applied, after clamping to 1-100."},"nextCursor":{"type":["string","null"],"description":"Pass this back as the `cursor` query parameter to fetch the next page. It is `null`\nwhen there is no next page.\n"},"hasMore":{"type":"boolean","description":"True when at least one more page exists. Stop paginating when this is false rather\nthan when the data array is short.\n"}}},"PageEnvelope":{"type":"object","description":"Shared shape of every paginated response: a `data` array plus `pagination` state.\n","required":["data","pagination"],"properties":{"data":{"type":"array","description":"The rows in this page, ordered by the requested sort and direction.","items":{}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}},"Entitlement":{"type":"object","description":"One capability and, when it is locked, the lowest plan that allows it.","required":["allowed","requiredPlan"],"properties":{"allowed":{"type":"boolean","description":"Whether the plan behind this token permits the action right now."},"requiredPlan":{"type":["string","null"],"enum":["none","solo","growth","pro",null],"description":"The lowest plan that allows the action, or null when `allowed` is true."}}},"Entitlements":{"type":"object","description":"The capability flags that gate this API. Check these before attempting a write rather\nthan discovering a `402` mid-workflow.\n","required":["api","schedule","publish","connectAccount","createWorkspace"],"properties":{"api":{"allOf":[{"$ref":"#/components/schemas/Entitlement"}],"description":"Programmatic access at all. When not allowed, every operation except `GET /me`\nreturns `402`. Requires the Pro plan.\n"},"schedule":{"allOf":[{"$ref":"#/components/schemas/Entitlement"}],"description":"Scheduling for a future time, and by extension media uploads. When not allowed,\n`POST /schedule`, future-dated single-post scheduling and `POST /storage/presign`\nreturn `402`.\n"},"publish":{"allOf":[{"$ref":"#/components/schemas/Entitlement"}],"description":"Publishing immediately. When not allowed, scheduling with no `publishAt` returns `402`."},"connectAccount":{"allOf":[{"$ref":"#/components/schemas/Entitlement"}],"description":"Whether another social account can be connected. Reflects the account slots granted to this customer, not only the plan."},"createWorkspace":{"allOf":[{"$ref":"#/components/schemas/Entitlement"}],"description":"Whether another workspace can be created."}}},"Billing":{"type":"object","description":"Subscription state for the workspace.","required":["status","periodEnd","cancelAtPeriodEnd","graceUntil","dunning"],"properties":{"status":{"type":"string","description":"The billing provider's subscription status, such as `active`, `trialing` or `past_due`."},"periodEnd":{"type":"string","format":"date-time","description":"End of the current billing period."},"cancelAtPeriodEnd":{"type":"boolean","description":"Whether the subscription ends at `periodEnd`."},"graceUntil":{"type":["integer","null"],"description":"Unix milliseconds until which already-scheduled posts keep publishing after a failed payment. Null when not in dunning."},"dunning":{"type":"boolean","description":"True while payment has failed and the grace window is open. New schedules are refused; scheduled posts still fire until `graceUntil`."}}},"Me":{"type":"object","description":"Everything an agent needs to know before acting, in one call.","required":["user","credential","workspace","plan","billing","entitlements","quotas","limits","blocked"],"properties":{"user":{"type":"object","description":"The user the credential was minted for.","required":["id","email"],"properties":{"id":{"type":"string","format":"uuid","description":"The user's id."},"email":{"type":["string","null"],"description":"The user's email address, or null if the account has none on record."}}},"credential":{"type":"object","description":"The credential used for this request. Never includes the secret.","required":["type","scopes","expiresAt"],"properties":{"type":{"type":"string","enum":["api_key","oauth"],"description":"`api_key` for a dashboard-issued key, `oauth` for an AI connector grant."},"scopes":{"type":"array","description":"Scopes granted. API keys hold all four.","items":{"type":"string","enum":["read","create","edit","admin"]}},"expiresAt":{"type":["string","null"],"format":"date-time","description":"When an OAuth access token expires. Null for API keys."}}},"workspace":{"allOf":[{"$ref":"#/components/schemas/Workspace"},{"type":"object","required":["slug","role"],"properties":{"slug":{"type":"string","description":"URL slug of the workspace."},"role":{"type":"string","description":"The calling user's role in the workspace, such as `owner`."}}}],"description":"The workspace this credential is scoped to. Everything you can read or write lives inside it."},"plan":{"type":"object","description":"The plan in force. An unknown or lapsed subscription resolves to `none`.","required":["id","name","trialActive"],"properties":{"id":{"type":"string","enum":["none","solo","growth","pro"],"description":"Plan id. `none` means no active paid plan, in which case most operations return `402`."},"name":{"type":"string","description":"Display name of the plan."},"trialActive":{"type":"boolean","description":"Whether the subscription is in its trial period."}}},"billing":{"oneOf":[{"$ref":"#/components/schemas/Billing"},{"type":"null"}],"description":"Subscription state, or null when the workspace has never subscribed."},"entitlements":{"$ref":"#/components/schemas/Entitlements"},"quotas":{"type":"object","description":"Usage against the caps. `subscription` is one pool shared by every workspace the\nplan owner has; `workspace` is this workspace alone.\n","required":["subscription","workspace"],"properties":{"subscription":{"type":"object","required":["ownedBy","accounts","queuedPosts"],"properties":{"ownedBy":{"type":"string","enum":["you","workspace owner"],"description":"Whose subscription the plan and these counts belong to."},"accounts":{"type":"object","required":["used","max"],"properties":{"used":{"type":"integer","description":"Social accounts connected across every workspace the owner has."},"max":{"type":"integer","description":"Account slots granted to the subscription."}}},"queuedPosts":{"type":"object","required":["used","max"],"properties":{"used":{"type":"integer","description":"Posts scheduled and not yet published, across every workspace the owner has."},"max":{"type":"integer","description":"Maximum scheduled posts the plan allows at once."}}}}},"workspace":{"type":"object","required":["accounts","storage"],"properties":{"accounts":{"type":"integer","description":"Social accounts connected in this workspace."},"storage":{"type":"object","required":["usedBytes","quotaBytes"],"properties":{"usedBytes":{"type":"integer","description":"Bytes of media stored in this workspace."},"quotaBytes":{"type":"integer","description":"Storage quota in bytes for this workspace."}}}}}}},"limits":{"type":"object","description":"The fixed limits scheduling and uploads are validated against.","required":["scheduleAheadDays","minScheduleMinutes","maxFilesPerPost","blockedPlatforms","presignPerMinute"],"properties":{"scheduleAheadDays":{"type":"integer","description":"How far into the future a post may be scheduled."},"minScheduleMinutes":{"type":"integer","description":"Minimum lead time for a scheduled post."},"maxFilesPerPost":{"type":"integer","description":"Maximum media files on one post."},"blockedPlatforms":{"type":"array","description":"Platforms the plan cannot post to.","items":{"type":"string"}},"presignPerMinute":{"type":"integer","description":"`POST /storage/presign` calls allowed per minute."}}},"blocked":{"type":"array","description":"Plain-English reasons a write would fail right now, such as a missing plan, a\npast-due subscription or a full queue. Empty when nothing blocks you. Show these to\nthe user instead of attempting the doomed write.\n","items":{"type":"string"}}}},"Workspace":{"type":"object","description":"The account this token belongs to: the boundary around your accounts, posts and media,\nplus the scheduling defaults the bulk scheduler reads.\n","required":["id","name","timezone","defaultTimes"],"properties":{"id":{"type":"string","format":"uuid","description":"The account id. You never send it anywhere; quote it if you contact support."},"name":{"type":"string","description":"Display name of the account."},"timezone":{"type":"string","description":"IANA timezone name, such as `America/New_York`. Preset posting times are wall-clock\ntimes in this zone, converted to UTC at scheduling time, so daylight saving is\nhandled for you.\n","examples":["America/New_York"]},"defaultTimes":{"type":"array","description":"Preset posting times as 24-hour `HH:MM` wall-clock strings in your timezone.\n`POST /schedule` uses these unless you override them, and fails with `422` if this\nlist is empty and you pass none.\n","items":{"type":"string","description":"A posting time as `HH:MM` in your timezone.","pattern":"^\\d{2}:\\d{2}$"}}}},"Account":{"type":"object","description":"A connected social account. Access and refresh tokens are never serialized here.\n","required":["id","username","platform","avatarUrl","isActive","createdAt"],"properties":{"id":{"type":"string","format":"uuid","description":"The account id. Pass these values in `socialAccountIds` when scheduling.\n"},"username":{"type":"string","description":"The handle or channel name on the platform."},"platform":{"type":"string","description":"Which network this account is on, for example `tiktok`, `youtube`, `instagram`,\n`x`, `bluesky` or `pinterest`.\n"},"avatarUrl":{"type":["string","null"],"description":"Profile picture URL, or null if the platform gave none."},"isActive":{"type":"boolean","description":"Whether the connection is still usable. An inactive account normally needs\nreconnecting in the dashboard before it can publish.\n"},"createdAt":{"type":"string","format":"date-time","description":"When the account was connected."}}},"AccountPage":{"allOf":[{"$ref":"#/components/schemas/PageEnvelope"},{"type":"object","description":"A page of connected social accounts.","properties":{"data":{"type":"array","description":"The accounts in this page.","items":{"$ref":"#/components/schemas/Account"}}}}]},"Group":{"type":"object","description":"A channel group: a named bundle of social accounts you can schedule to by name.\n","required":["id","name","accountIds","accountCount","createdAt"],"properties":{"id":{"type":"string","format":"uuid","description":"The group id."},"name":{"type":"string","description":"The group's name. This is the value `POST /schedule` accepts in its `group` field,\nmatched exactly.\n"},"accountIds":{"type":"array","description":"Ids of the social accounts in this group, in no guaranteed order.","items":{"type":"string","format":"uuid","description":"A social account id belonging to this group."}},"accountCount":{"type":"integer","description":"How many accounts are in the group. The bulk scheduler rejects groups of more than\n10 with a `422`.\n"},"createdAt":{"type":"string","format":"date-time","description":"When the group was created."}}},"GroupPage":{"allOf":[{"$ref":"#/components/schemas/PageEnvelope"},{"type":"object","description":"A page of channel groups.","properties":{"data":{"type":"array","description":"The groups in this page.","items":{"$ref":"#/components/schemas/Group"}}}}]},"PostType":{"type":"string","description":"What kind of content the post carries. Anything other than `text` and `blog` must have at\nleast one media key before it can be scheduled. `blog` is a WordPress blog post: `title`\nis required and `description` is the body, HTML sent as written (plain text works, blank\nlines become paragraphs). The first image is the featured image and any others are added\nunder the body. Images only.\n","enum":["text","image","video","story","blog"]},"PostListItem":{"type":"object","description":"A post as it appears in a list. Per-account delivery state is omitted here. Fetch\n`GET /posts/{postId}` for that.\n","required":["id","status","type","title","content","mediaUrls","scheduledFor","coverR2Key","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid","description":"The post id."},"status":{"type":"string","description":"Coarse lifecycle summary such as `draft`, `scheduled`, `posted` or `failed`. It is a\nrollup hint, not authoritative per account. Read `targets` on the detail endpoint\nto know what each account actually did.\n"},"type":{"$ref":"#/components/schemas/PostType"},"title":{"type":["string","null"],"description":"The title stored in the post's metadata, or null if none was set."},"content":{"type":"string","description":"The post body or caption. Empty string when the post has none."},"mediaUrls":{"type":"array","description":"Storage keys of the post's media, as returned by `POST /storage/presign`. Empty when\nthe post has no media.\n","items":{"type":"string","description":"A storage key under your own prefix."}},"scheduledFor":{"type":["string","null"],"format":"date-time","description":"The post-level scheduled fire time, or null if the post is not scheduled. Individual\naccounts may have their own times that differ from this.\n"},"coverR2Key":{"type":["string","null"],"description":"Storage key of the cover image, or null if none was set."},"createdAt":{"type":"string","format":"date-time","description":"When the post was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the post was last modified."}}},"PostPage":{"allOf":[{"$ref":"#/components/schemas/PageEnvelope"},{"type":"object","description":"A page of posts.","properties":{"data":{"type":"array","description":"The posts in this page.","items":{"$ref":"#/components/schemas/PostListItem"}}}}]},"PostTarget":{"type":"object","description":"One account's copy of a post: its own status, fire time and failure reason. This is the\nauthoritative record of what happened for that account.\n","required":["socialAccountId","platform","username","status","publishAt","publishedAt","error","link","blueskySettings","wordpressSettings"],"properties":{"socialAccountId":{"type":["string","null"],"format":"uuid","description":"The social account this target publishes to."},"platform":{"type":["string","null"],"description":"The account's platform id, e.g. `instagram`; null if the account was removed."},"username":{"type":["string","null"],"description":"The account's handle on that platform; null if unknown or the account was removed."},"status":{"type":["string","null"],"description":"Delivery state for this account, typically `pending`, `scheduled`, `uploading`,\n`published` or `failed`.\n"},"publishAt":{"type":["string","null"],"format":"date-time","description":"When this account's copy is due to fire, or null if not scheduled."},"publishedAt":{"type":["string","null"],"format":"date-time","description":"When this account's copy actually went live, or null if it has not."},"error":{"type":["string","null"],"description":"Why this account's copy failed, or null if it has not failed."},"link":{"type":["string","null"],"format":"uri","description":"Where this account's copy lives on the platform, or null when it has not been\npublished or the platform gives no link. For a WordPress draft this is the wp-admin\nedit page, which opens only for a user signed in to that site.\n"},"blueskySettings":{"description":"Alt text and content warnings for a Bluesky account's copy, or null when none were\nset or the account is not on Bluesky.\n","oneOf":[{"$ref":"#/components/schemas/BlueskySettings"},{"type":"null"}]},"wordpressSettings":{"description":"Status, categories, tags, slug, excerpt, featured image alt text and SEO fields for a\nWordPress account's copy, or null when none were set or the account is not on\nWordPress.\n","oneOf":[{"$ref":"#/components/schemas/WordPressSettings"},{"type":"null"}]}}},"Post":{"type":"object","description":"A post with its metadata and per-account delivery state.","required":["id","status","type","content","mediaUrls","coverR2Key","scheduledFor","metadata","socialAccountIds","targets","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid","description":"The post id."},"status":{"type":"string","description":"Coarse lifecycle summary. Prefer `targets` when you need to know the real state per\naccount.\n"},"type":{"$ref":"#/components/schemas/PostType"},"content":{"type":"string","description":"The post body or caption."},"mediaUrls":{"type":"array","description":"Storage keys of the post's media. Empty when there is none.","items":{"type":"string","description":"A storage key under your own prefix."}},"coverR2Key":{"type":["string","null"],"description":"Storage key of the cover image, or null if none was set."},"scheduledFor":{"type":["string","null"],"format":"date-time","description":"The post-level scheduled fire time, or null if unscheduled."},"metadata":{"type":"object","description":"The post's public metadata: `title`, `description` and `hashtags`, plus `source` and\n`client` when the post was created through the API or a connector. Other internal\nkeys are never returned.\n","properties":{"title":{"type":"string","description":"The post title, if one was set."},"description":{"type":"string","description":"The long-form description, if one was set."},"hashtags":{"type":"array","description":"Hashtags without the leading `#`, if any were set.","items":{"type":"string","description":"A single hashtag, without the `#`."}},"source":{"type":"string","description":"`connector` when the post was created through the API or an MCP connector."},"client":{"type":"string","description":"The API key or connector client that created the post."}}},"socialAccountIds":{"type":"array","description":"Convenience list of the accounts this post fans out to, the same ids that appear in\n`targets`.\n","items":{"type":"string","format":"uuid","description":"A social account id this post targets."}},"targets":{"type":"array","description":"One entry per account the post fans out to. Empty until the post is scheduled.\n","items":{"$ref":"#/components/schemas/PostTarget"}},"createdAt":{"type":"string","format":"date-time","description":"When the post was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the post was last modified."}}},"CreatePostRequest":{"type":"object","description":"Fields for a new draft. Every field is optional; an empty body creates an empty draft of\ntype `video`. Scheduling is a separate call.\n","properties":{"content":{"type":"string","maxLength":2200,"description":"The post body or caption, up to 2200 characters. Defaults to an empty string.\nPer-platform limits are enforced later, at schedule time.\n"},"mediaUrls":{"type":"array","description":"Storage keys of already-uploaded media, using the `resolvedKey` values returned by\n`POST /storage/presign`. Listing a key here claims it so it will not be swept away\nas an orphan. These are storage keys, not public URLs.\n","items":{"type":"string","description":"A storage key returned by the presign operation."}},"title":{"type":"string","maxLength":200,"description":"Title stored in the post's metadata. Note that scheduling re-validates titles against\na 100-character limit, so keep it under that if the post will be scheduled.\n"},"description":{"type":"string","description":"Long-form description stored in the post's metadata, used by platforms that have a\ndescription field separate from the caption.\n"},"hashtags":{"type":"array","description":"Hashtags stored in the post's metadata. Do not include the leading `#`.","items":{"type":"string","description":"A single hashtag, without the `#`."}},"type":{"$ref":"#/components/schemas/PostType"},"coverR2Key":{"type":"string","description":"Storage key of a cover image already uploaded via `POST /storage/presign`.\nReferencing it here also claims it against the orphan sweeper.\n"}}},"UpdatePostRequest":{"type":"object","description":"Fields to change on a draft. Omit a field to leave it alone. Only `mediaUrls` and\n`coverR2Key` accept `null`, which clears them.\n","properties":{"content":{"type":"string","maxLength":2200,"description":"Replacement post body or caption, up to 2200 characters."},"mediaUrls":{"type":["array","null"],"description":"Replacement list of storage keys, or `null` to remove all media. Replaces the\nexisting list entirely rather than appending to it.\n","items":{"type":"string","description":"A storage key returned by the presign operation."}},"title":{"type":"string","maxLength":200,"description":"Replacement title, merged into the post's metadata."},"description":{"type":"string","description":"Replacement description, merged into the post's metadata."},"hashtags":{"type":"array","description":"Replacement hashtag list, merged into the post's metadata. Replaces the previous\nlist entirely. Do not include the leading `#`.\n","items":{"type":"string","description":"A single hashtag, without the `#`."}},"type":{"$ref":"#/components/schemas/PostType"},"coverR2Key":{"type":["string","null"],"description":"Replacement cover image storage key, or `null` to clear the cover."}}},"CreatedPost":{"type":"object","description":"The id of the post that was created or updated. Fetch `GET /posts/{postId}` if you need\nthe full record.\n","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"The post id."}}},"DeletedPost":{"type":"object","description":"Confirmation that the draft was deleted, with what it was.","required":["deleted","post"],"properties":{"deleted":{"type":"boolean","description":"Always true. The post and its target rows are gone."},"post":{"type":"object","description":"The deleted draft, so a caller can name it without keeping a copy.","required":["title","content","type","mediaCount"],"properties":{"title":{"type":["string","null"],"description":"The draft's title, if one was set."},"content":{"type":["string","null"],"description":"The caption."},"type":{"type":"string","description":"The post type: text, image, video, story or blog."},"mediaCount":{"type":"integer","description":"How many media files were attached."}}}}},"PlatformOverride":{"type":"object","description":"Per-account content overrides. Anything omitted falls back to the post's own values, and\nan empty string is treated as \"no override\" rather than as blank content.\n","properties":{"title":{"type":"string","maxLength":10000,"description":"Title to use for this account instead of the post's title."},"description":{"type":"string","maxLength":10000,"description":"Description to use for this account instead of the post's description."},"hashtags":{"type":"array","maxItems":30,"description":"Hashtags to use for this account instead of the post's hashtags.","items":{"type":"string","maxLength":100,"description":"A single hashtag, without the `#`."}}}},"TikTokSettings":{"type":"object","description":"Per-account TikTok delivery options. Ignored for non-TikTok accounts.","required":["deliveryMode","privacyLevel"],"properties":{"deliveryMode":{"type":"string","description":"`direct_post` publishes straight to the account's feed; `inbox` drops the video into\nthe TikTok app's inbox for the user to finish and post manually.\n","enum":["inbox","direct_post"]},"privacyLevel":{"type":["string","null"],"maxLength":64,"description":"TikTok privacy setting for a direct post, such as `PUBLIC_TO_EVERYONE`. Send null\nwhen it does not apply.\n"},"commercialContent":{"type":"boolean","description":"Whether the video is disclosed as commercial content."},"brandOrganic":{"type":"boolean","description":"Whether the disclosure marks the video as promoting your own brand."},"brandContent":{"type":"boolean","description":"Whether the disclosure marks the video as a paid partnership."}}},"PinterestSettings":{"type":"object","description":"Per-account Pinterest options. Ignored for non-Pinterest accounts.","required":["boardId"],"properties":{"boardId":{"type":"string","pattern":"^\\d+$","maxLength":32,"description":"Numeric id of the board to pin to. Digits only. Anything else is rejected outright.\n"}}},"SchedulePostRequest":{"type":"object","description":"Which accounts to send one draft to, and when. Every key used in an overrides map must\nalso appear in `socialAccountIds`, or the whole request is rejected with `400`.\n","required":["socialAccountIds"],"properties":{"socialAccountIds":{"type":"array","minItems":1,"maxItems":10,"description":"The accounts to publish to, between one and ten. Duplicates are silently collapsed.\nIf any id is not yours the entire request fails with `404` and does not\npartially schedule.\n","items":{"type":"string","format":"uuid","description":"A social account id from `GET /accounts`."}},"publishAt":{"type":"string","format":"date-time","description":"When to publish, as an ISO 8601 UTC instant. Must be at least 5 minutes and at most\nroughly 6 months in the future. Omit this field entirely to publish immediately, but\nthat path needs the `publish` capability instead of `schedule`.\n"},"title":{"type":"string","maxLength":100,"description":"Title to use for this run, overriding the stored one and persisted back onto the\npost. Note this limit is stricter than the 200 characters `POST /posts` accepts.\n"},"description":{"type":"string","maxLength":10000,"description":"Description to use for this run, overriding the stored one and persisted back onto\nthe post. Also re-checked against each target platform's own caption limit.\n"},"hashtags":{"type":"array","maxItems":30,"description":"Hashtags to use for this run, overriding the stored ones. Do not include the `#`.\n","items":{"type":"string","maxLength":100,"description":"A single hashtag, without the `#`."}},"platformOverrides":{"type":"object","description":"Per-account content overrides, keyed by social account id. Every key must be one of\nthe ids in `socialAccountIds`.\n","additionalProperties":{"$ref":"#/components/schemas/PlatformOverride"}},"publishAtOverrides":{"type":"object","description":"Per-account fire times, keyed by social account id, each an ISO 8601 UTC instant.\nAccounts not listed use the top-level `publishAt`. Every key must be one of the ids\nin `socialAccountIds`.\n","additionalProperties":{"type":"string","format":"date-time","description":"The ISO 8601 UTC fire time for that one account."}},"tiktokSettings":{"type":"object","description":"Per-account TikTok settings, keyed by social account id. Every key must be one of the\nids in `socialAccountIds`.\n","additionalProperties":{"$ref":"#/components/schemas/TikTokSettings"}},"pinterestSettings":{"type":"object","description":"Per-account Pinterest settings, keyed by social account id. Every key must be one of\nthe ids in `socialAccountIds`.\n","additionalProperties":{"$ref":"#/components/schemas/PinterestSettings"}},"blueskySettings":{"type":"object","description":"Per-account Bluesky alt text and content warnings, keyed by social account id. Every\nkey must be one of the ids in `socialAccountIds` and must name a Bluesky account.\n","additionalProperties":{"$ref":"#/components/schemas/BlueskySettings"}},"wordpressSettings":{"type":"object","description":"Per-account WordPress post settings for a `blog` post, keyed by social account id.\nEvery key must be one of the ids in `socialAccountIds` and must name a WordPress\naccount. Nothing reaches WordPress before the post fires.\n","additionalProperties":{"$ref":"#/components/schemas/WordPressSettings"}}}},"WordPressSettings":{"type":"object","description":"How one WordPress account's copy of a blog post is created. Every field is optional.\nStored on that account's copy and returned on `GET /posts/{postId}`.\n","properties":{"status":{"type":"string","enum":["publish","draft"],"description":"`publish` (the default when omitted) or `draft`, which creates a WordPress draft for\nthe site's author to review.\n"},"categories":{"type":"array","description":"Category names. Each must already exist on the site; an unknown name fails with\n`400` listing the site's categories.\n","items":{"type":"string","description":"One category name."}},"tags":{"type":"array","description":"Tag names. A tag the site does not have yet is created.","items":{"type":"string","description":"One tag name."}},"slug":{"type":"string","maxLength":200,"description":"The URL slug. Made from the title when omitted."},"excerpt":{"type":"string","description":"Short summary shown in feeds and link previews."},"featuredImageAlt":{"type":"string","description":"Alt text for the featured image, which is the post's first image."},"seo":{"type":"object","description":"Yoast SEO fields. Only for an account whose site lets it write them; otherwise the\nrequest fails with `400`.\n","properties":{"title":{"type":"string","description":"SEO title shown in search results instead of the post title."},"description":{"type":"string","description":"Meta description. Yoast prints none when this is omitted."},"focusKeyphrase":{"type":"string","description":"Yoast focus keyphrase."}}}}},"BlueskySettings":{"type":"object","description":"Alt text for each file one Bluesky account publishes, and the content warning labels\non the post. Stored on that account's copy and returned on `GET /posts/{postId}`.\n","required":["altText","labels"],"properties":{"altText":{"type":"array","maxItems":4,"description":"One entry per file, in the order that account publishes them. Use an empty string\nfor a file with no description. The count must equal the number of files, or the\nrequest fails with `400`.\n","items":{"type":"string","description":"The description of one file, up to 2,000 characters counted as Bluesky counts them (graphemes)."}},"labels":{"type":"array","maxItems":2,"description":"Content warnings. At most one of `sexual` (Suggestive), `nudity` or `porn` (Adult),\nplus `graphic-media`. Only on posts with images or video. Bluesky leaves posts\nlabeled `sexual`, `porn` or `graphic-media` out of feeds for people who have not\nturned on adult content. Send `[]` for none.\n","items":{"type":"string","enum":["sexual","nudity","porn","graphic-media"],"description":"One Bluesky self-label value."}}}},"TargetResult":{"type":"object","description":"The outcome of scheduling one account's copy of a post.","required":["socialAccountId","platform","username","status"],"properties":{"socialAccountId":{"type":"string","format":"uuid","description":"The account this result is about."},"platform":{"type":["string","null"],"description":"The account's platform id, e.g. `instagram`; null if the account was removed."},"username":{"type":["string","null"],"description":"The account's handle on that platform; null if unknown or the account was removed."},"status":{"type":"string","description":"The account's state after the call. `scheduled` means a timer was created, `failed`\nwhen it could not be, or an existing state such as `published` when the target was\nalready handled and was deliberately left alone.\n"},"error":{"type":"string","description":"Present only when this account failed; explains why."}}},"ScheduleResult":{"type":"object","description":"Per-account results for one post. Note this shape is passed through from the underlying\nscheduler and, unlike the errors in this API, is not wrapped in an envelope.\n","required":["success","results"],"properties":{"success":{"type":"boolean","description":"True only when no account was left failed or pending. Partial failure still returns\nHTTP 200 with this set to false, so always inspect `results`.\n"},"results":{"type":"array","description":"One entry per requested account, in the order they were processed.","items":{"$ref":"#/components/schemas/TargetResult"}}}},"SmartScheduleRequest":{"type":"object","description":"Inputs for the bulk scheduler. You must supply either `group` or a non-empty\n`socialAccountIds`; if both are given, `socialAccountIds` wins.\n","required":["minPerDay"],"properties":{"minPerDay":{"type":"integer","minimum":1,"maximum":20,"description":"How many posts to place per day. Must not exceed the number of available time slots,\nor the call fails with `422`. Add more times or lower this number.\n"},"group":{"type":"string","description":"Name of the channel group whose accounts should receive every post, matched exactly\nas returned by `GET /groups`. Unknown names return `404`.\n"},"socialAccountIds":{"type":"array","description":"Explicit accounts to receive every post, used instead of `group`. Duplicates are\ncollapsed. More than 10 distinct accounts is rejected with `422`.\n","items":{"type":"string","format":"uuid","description":"A social account id from `GET /accounts`."}},"postIds":{"type":"array","description":"The specific drafts to schedule, in the order you want them placed. Omit this to use\nevery draft you have, oldest first, up to 200. Any id that is not one of your\ndrafts is reported in `skipped` rather than failing the call.\n","items":{"type":"string","format":"uuid","description":"A draft post id."}},"startDate":{"type":"string","format":"date","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"First calendar day to place posts on, as `YYYY-MM-DD` in your timezone.\nDefaults to tomorrow. Slots less than five minutes away are skipped, so a start date\nin the past simply yields fewer usable slots.\n"},"times":{"type":"array","description":"Wall-clock posting times as `HH:MM` in your timezone, overriding the\nyour defaults for this call only. Times are sorted ascending and the earliest\n`minPerDay` of them are used each day.\n","items":{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"A posting time as `HH:MM` in your timezone."}},"dryRun":{"type":"boolean","description":"When true, compute and return the plan without creating any timers. Run this first\nto confirm the dates look right, then repeat without it.\n"}}},"PlanSummary":{"type":"object","description":"How the scheduler laid the posts out.","required":["timezone","perDay","days","slotsUsed"],"properties":{"timezone":{"type":"string","description":"The timezone the wall-clock times were interpreted in."},"perDay":{"type":"integer","description":"Posts placed per day. This is `minPerDay` clamped to the number of available slots.\n"},"days":{"type":"integer","description":"How many distinct calendar days the plan spans."},"slotsUsed":{"type":"array","description":"The specific `HH:MM` times used each day, in ascending order.","items":{"type":"string","description":"A posting time as `HH:MM` in your timezone."}}}},"ScheduledEntry":{"type":"object","description":"One draft's placement in the plan, and what came of it.","required":["postId","publishAt","accountIds"],"properties":{"postId":{"type":"string","format":"uuid","description":"The draft that was placed."},"publishAt":{"type":"string","format":"date-time","description":"The ISO 8601 UTC instant this post will fire at."},"accountIds":{"type":"array","description":"Every resolved account this post goes to. The bulk scheduler always cross-posts, so\nthis is the same list for every entry.\n","items":{"type":"string","format":"uuid","description":"A social account id receiving this post."}},"status":{"type":"string","description":"`scheduled` when timers were created, `failed` when the underlying scheduler rejected\nthis post. Absent on a dry run, since nothing was attempted.\n","enum":["scheduled","failed"]},"error":{"type":"string","description":"Present only when `status` is `failed`; explains why that post was rejected."}}},"SkippedEntry":{"type":"object","description":"A draft that was deliberately not scheduled, and why.","required":["postId","reason"],"properties":{"postId":{"type":"string","format":"uuid","description":"The post that was skipped."},"reason":{"type":"string","description":"Why it was skipped, typically that it is not a draft or not yours, or\nthat it fell past the 50-posts-per-call limit, in which case simply call again.\n"}}},"SmartScheduleResponse":{"type":"object","description":"The computed plan plus its outcome.","required":["scheduled","skipped","plan"],"properties":{"scheduled":{"type":"array","description":"Every post that was placed, with its fire time. On a dry run these are proposals and\nnothing has been persisted.\n","items":{"$ref":"#/components/schemas/ScheduledEntry"}},"skipped":{"type":"array","description":"Posts that were not placed. Always check this. An empty array means everything you\nasked for was scheduled.\n","items":{"$ref":"#/components/schemas/SkippedEntry"}},"plan":{"$ref":"#/components/schemas/PlanSummary"},"dryRun":{"type":"boolean","description":"Present and true only when the request set `dryRun`. Its absence means timers were\nactually created.\n"}}},"PresignRequest":{"type":"object","description":"What you want to upload or download. Supply exactly one of `relativeKey` (preferred) or\n`key`; both resolve to a path under your own storage prefix.\n","required":["contentType"],"properties":{"relativeKey":{"type":"string","minLength":1,"description":"Path for the object relative to your storage prefix, such as\n`media/clip_03.mp4`. Only letters, digits, `.`, `_`, `-` and `/` are allowed, and\n`..` segments are rejected. The server prepends your storage prefix and returns\nthe result as `resolvedKey`.\n"},"key":{"type":"string","minLength":1,"description":"A full storage key, used instead of `relativeKey`, normally only for\n`operation: download` of a key you already hold. It must already start with your own\nstorage prefix or the request is refused with `403`.\n"},"contentType":{"type":"string","minLength":1,"description":"MIME type of the object, such as `video/mp4` or `image/jpeg`. It is bound into the\nsignature, so the PUT must send exactly this same `Content-Type`. It also selects\nthe size cap: 500 MB for `video/*`, 15 MB for `image/*`, 10 MB for anything else.\n"},"contentLength":{"type":"integer","minimum":1,"description":"Exact byte length of the object you are about to upload. Required for uploads and\nignored for downloads. It is bound into the signature, so a PUT of a different size\nis rejected by storage, and it is counted against your storage quota as soon as\nthe URL is issued.\n"},"operation":{"type":"string","description":"`upload` presigns a PUT for new bytes; `download` presigns a GET for an object that\nalready exists. Downloads skip the quota and rate-limit checks.\n","enum":["upload","download"],"default":"upload"},"expiresInSeconds":{"type":"integer","minimum":60,"maximum":86400,"default":300,"description":"How long the returned URL stays valid, between 60 seconds and one day. Keep it short:\nwhoever holds the URL can use it until it expires. Allow generous client-side\ntimeouts for large uploads within this window.\n"}}},"PresignResponse":{"type":"object","description":"A signed URL and the key it points at.","required":["signedUrl","resolvedKey","expiresIn"],"properties":{"signedUrl":{"type":"string","format":"uri","description":"Send the raw bytes here with a plain PUT (or GET, for a download) and no\n`Authorization` header. The signature in the URL is the credential. Never log or\nshare this URL.\n"},"resolvedKey":{"type":"string","description":"The storage key the URL writes to. This is the value to pass as a\n`mediaUrls` entry or `coverR2Key` when creating or updating a post.\n"},"expiresIn":{"type":"integer","description":"Seconds the URL remains valid, counting from when it was issued."}}}}}}