JuzPost Scheduling API
Put drafts on a publishing timer, one post at a time or in bulk.
Put drafts on a publishing timer, one post at a time or in bulk.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/juzpost:juzpost-scheduling-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: JuzPost REST Scheduling 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,
media upload to R2, and scheduling posts across connected social accounts.
**Base URL.** `https://www.juzpost.com/api/cli/v1`. Note the `cli` segment:
`/api/v1` is a different, older surface used by the dashboard and is not this API.
**Authentication.** Every operation requires `Authorization: Bearer `. There are no
unauthenticated operations.
## Get a token
1. Open **Settings, General** in the JuzPost
dashboard.
2. Find the **API keys** card and choose **Generate API key**.
3. Copy the key. It begins with `jp_live_` and is shown **once**, at creation. Store it in
your secret manager before leaving the page.
4. Check it works:
```bash
curl https://www.juzpost.com/api/cli/v1/me \
--header "Authorization: Bearer jp_live_YOUR_KEY"
```
A `200` comes back with your account, plan and capabilities. A `401` means the key is
wrong, revoked, or was pasted with a trailing newline.
Revoke a key from the same card. Nothing in this API can delete the credential it is
calling with, which is deliberate: an automated client should not be able to lock itself
out, and revocation should be a human action.
**One token, one account.** A token belongs to exactly one JuzPost account (a *workspace*).
Every operation acts on that one account, and no operation anywhere accepts an account id.
**Entitlements.** Every operation other than `GET /me` requires the
caller''s plan to include the `api` capability, and `POST /schedule`,
`POST /posts/{postId}/schedule` and `POST /storage/presign` additionally require `schedule`
(or `publish`, for immediate posting). A plan that lacks the capability produces `402`,
never `403`. Call `GET /me` first to check before attempting a write that is doomed.
**Errors.** Every non-2xx response uses the same envelope: a single `error` object carrying
a stable machine-readable `code`, a human `message`, and optional `details`. Never branch on
the message text; branch on `error.code` or the HTTP status.
**Unknown ids.** An id that is not yours is reported as `404`, never `403`, so no operation
can be used to discover whether someone else''s data exists.'
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: Scheduling
description: Put drafts on a publishing timer, one post at a time or in bulk.
paths:
/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
future fire time, or omit it entirely to publish right now. A future time must be at
least five minutes and at most roughly six months out. The response reports one result
per account; check each entry rather than only the top-level `success`, because a
partial failure still returns `200`. Immediate publishing requires the `publish`
capability while a future time requires `schedule`; either way a plan that lacks it
returns `402`.'
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
targets is still reported here rather than as an error status.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ScheduleResult'
'400':
description: 'Invalid body, a fire time outside the allowed window, an override keyed by an
account not in `socialAccountIds`, a non-text post with no media, a caption
longer than the target platform allows, or `blueskySettings` for an account that is
not on Bluesky, with an alt text count that differs from its files, or with labels on
a text post. Also `wordpressSettings` for an account that is not on WordPress, a
category name the site does not have (the error lists the site''s categories), or
`seo` for an account whose site does not let it write Yoast SEO fields.
'
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
does not. Which one is deliberately not disclosed.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: 'The post left `draft` status before the timer could be claimed. It is already
scheduled or uploading. Re-read the post before retrying.
'
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
assigns drafts to your preset wall-clock times, `minPerDay` posts per day,
walking forward from `startDate` and skipping any slot less than five minutes away.
Every post goes to every resolved account. Set `dryRun` to true to see the plan without
creating any timers. Always do this first. At most 50 posts are scheduled per call;
the rest come back in `skipped` with a reason, so call again to continue. Targets are
capped at 10 accounts.'
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
`scheduled` may carry `status: failed` with an `error`, so check them.
'
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
10 accounts in the group, no posting times available, or `minPerDay` larger than the
number of time slots. The message says which.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
SmartScheduleRequest:
type: object
description: 'Inputs for the bulk scheduler. You must supply either `group` or a non-empty
`socialAccountIds`; if both are given, `socialAccountIds` wins.
'
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,
or the call fails with `422`. Add more times or lower this number.
'
group:
type: string
description: 'Name of the channel group whose accounts should receive every post, matched exactly
as returned by `GET /groups`. Unknown names return `404`.
'
socialAccountIds:
type: array
description: 'Explicit accounts to receive every post, used instead of `group`. Duplicates are
collapsed. More than 10 distinct accounts is rejected with `422`.
'
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
every draft you have, oldest first, up to 200. Any id that is not one of your
drafts is reported in `skipped` rather than failing the call.
'
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.
Defaults to tomorrow. Slots less than five minutes away are skipped, so a start date
in the past simply yields fewer usable slots.
'
times:
type: array
description: 'Wall-clock posting times as `HH:MM` in your timezone, overriding the
your defaults for this call only. Times are sorted ascending and the earliest
`minPerDay` of them are used each day.
'
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
to confirm the dates look right, then repeat without it.
'
SchedulePostRequest:
type: object
description: 'Which accounts to send one draft to, and when. Every key used in an overrides map must
also appear in `socialAccountIds`, or the whole request is rejected with `400`.
'
required:
- socialAccountIds
properties:
socialAccountIds:
type: array
minItems: 1
maxItems: 10
description: 'The accounts to publish to, between one and ten. Duplicates are silently collapsed.
If any id is not yours the entire request fails with `404` and does not
partially schedule.
'
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
roughly 6 months in the future. Omit this field entirely to publish immediately, but
that path needs the `publish` capability instead of `schedule`.
'
title:
type: string
maxLength: 100
description: 'Title to use for this run, overriding the stored one and persisted back onto the
post. Note this limit is stricter than the 200 characters `POST /posts` accepts.
'
description:
type: string
maxLength: 10000
description: 'Description to use for this run, overriding the stored one and persisted back onto
the post. Also re-checked against each target platform''s own caption limit.
'
hashtags:
type: array
maxItems: 30
description: 'Hashtags to use for this run, overriding the stored ones. Do not include the `#`.
'
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
the ids in `socialAccountIds`.
'
additionalProperties:
$ref: '#/components/schemas/PlatformOverride'
publishAtOverrides:
type: object
description: 'Per-account fire times, keyed by social account id, each an ISO 8601 UTC instant.
Accounts not listed use the top-level `publishAt`. Every key must be one of the ids
in `socialAccountIds`.
'
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
ids in `socialAccountIds`.
'
additionalProperties:
$ref: '#/components/schemas/TikTokSettings'
pinterestSettings:
type: object
description: 'Per-account Pinterest settings, keyed by social account id. Every key must be one of
the ids in `socialAccountIds`.
'
additionalProperties:
$ref: '#/components/schemas/PinterestSettings'
blueskySettings:
type: object
description: 'Per-account Bluesky alt text and content warnings, keyed by social account id. Every
key must be one of the ids in `socialAccountIds` and must name a Bluesky account.
'
additionalProperties:
$ref: '#/components/schemas/BlueskySettings'
wordpressSettings:
type: object
description: 'Per-account WordPress post settings for a `blog` post, keyed by social account id.
Every key must be one of the ids in `socialAccountIds` and must name a WordPress
account. Nothing reaches WordPress before the post fires.
'
additionalProperties:
$ref: '#/components/schemas/WordPressSettings'
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`
when it could not be, or an existing state such as `published` when the target was
already handled and was deliberately left alone.
'
error:
type: string
description: Present only when this account failed; explains why.
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
this is the same list for every entry.
'
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
this post. Absent on a dry run, since nothing was attempted.
'
enum:
- scheduled
- failed
error:
type: string
description: Present only when `status` is `failed`; explains why that post was rejected.
ScheduleResult:
type: object
description: 'Per-account results for one post. Note this shape is passed through from the underlying
scheduler and, unlike the errors in this API, is not wrapped in an envelope.
'
required:
- success
- results
properties:
success:
type: boolean
description: 'True only when no account was left failed or pending. Partial failure still returns
HTTP 200 with this set to false, so always inspect `results`.
'
results:
type: array
description: One entry per requested account, in the order they were processed.
items:
$ref: '#/components/schemas/TargetResult'
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.
'
WordPressSettings:
type: object
description: 'How one WordPress account''s copy of a blog post is created. Every field is optional.
Stored on that account''s copy and returned on `GET /posts/{postId}`.
'
properties:
status:
type: string
enum:
- publish
- draft
description: '`publish` (the default when omitted) or `draft`, which creates a WordPress draft for
the site''s author to review.
'
categories:
type: array
description: 'Category names. Each must already exist on the site; an unknown name fails with
`400` listing the site''s categories.
'
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
request fails with `400`.
'
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.
Error:
type: object
description: 'The envelope returned by every non-2xx response in this API. There is never a top-level
field other than `error`.
'
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
releases, so never parse or match on it.
'
details:
description: 'Optional structured context whose shape depends on the code, for example
`{ "requiredPlan": "pro" }` on a `payment_required`, `{ "code":
"invalid_cursor" }` on a bad cursor, or `{ "usedBytes": …, "quotaBytes": … }`
when a storage quota is full. Treat it as opaque unless a specific operation
documents its contents.
'
TikTokSettings:
type: object
description: Per-account TikTok options. Ignored for non-TikTok accounts.
required:
- privacyLevel
properties:
deliveryMode:
type: string
description: 'Optional. `direct_post` publishes straight to the account''s feed and is the only
accepted value; any other value is rejected with a 400. Defaults to `direct_post`.
'
enum:
- direct_post
default: direct_post
privacyLevel:
type:
- string
- 'null'
maxLength: 64
description: 'Who can see the post, such as `PUBLIC_TO_EVERYONE` or `SELF_ONLY`. A null is
rejected when the post is scheduled; leave the account out of `tiktokSettings`
to publish to `PUBLIC_TO_EVERYONE`.
'
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.
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.
'
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.
BlueskySettings:
type: object
description: 'Alt text for each file one Bluesky account publishes, and the content warning labels
on the post. Stored on that account''s copy and returned on `GET /posts/{postId}`.
'
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
for a file with no description. The count must equal the number of files, or the
request fails with `400`.
'
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),
plus `graphic-media`. Only on posts with images or video. Bluesky leaves posts
labeled `sexual`, `porn` or `graphic-media` out of feeds for people who have not
turned on adult content. Send `[]` for none.
'
items:
type: string
enum:
- sexual
- nudity
- porn
- graphic-media
description: One Bluesky self-label value.
PlatformOverride:
type: object
description: 'Per-account content overrides. Anything omitted falls back to the post''s own values, and
an empty string is treated as "no override" rather than as blank content.
'
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 `#`.
ErrorCode:
type: string
description: 'Stable machine-readable error identifier. Branch on this rather than on the message.
Each code maps to exactly one HTTP status.
'
enum:
- bad_request
- unauthorized
- payment_required
- forbidden
- not_found
- conflict
- unprocessable
- pending
- rate_limited
- internal
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
that it fell past the 50-posts-per-call limit, in which case simply call again.
'
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
nothing has been persisted.
'
items:
$ref: '#/components/schemas/ScheduledEntry'
skipped:
type: array
description: 'Posts that were not placed. Always check this. An empty array means everything you
asked for was scheduled.
'
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
actually created.
'
responses:
PaymentRequired:
description: 'The plan behind this token does not include the capability this operation needs.
`error.details.requiredPlan` names the tier that does. This is never a token problem, so
do not retry without changing the plan.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: 'The request body or query string failed validation. `error.message` names the first
problem found.
'
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
login again to obtain a new token.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
parameters:
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
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: 'An API key created in the dashboard under Settings, General, API keys, sent as
`Authorization: Bearer jp_live_...`. Required on every operation without exception.
Missing, malformed, revoked or unknown tokens all produce `401` with code
`unauthorized`.
'