Optional ISO 8601 time to publish at instead of now. At least a minute out, at most a year.
`idempotencyKey`
string
1–255 chars. Also accepted as the `Idempotency-Key` header; the body field wins.
firstComment is per target, not per post, so the same publish can carry different comment
wording for LinkedIn and Instagram. See First comments.So are content and mediaItems: set either on a target and it overrides the post's own for
that target only, which is how one announcement carries a square crop for Instagram and a wide
one everywhere else without becoming three separate posts. Leave them off and the target
follows the post. A few more per-target fields exist for specific platforms — postType on
Instagram, spaceSlug on Supa Hub — see the Posts reference.
What is validated up front
Before any network call, Chirio checks in this order:
Body shape — schema violations answer 400 invalid_request.
Accounts exist and belong to your project — otherwise 404 account_not_found.
Platform matches the account — targeting a threads account as instagram answers
400 platform_mismatch.
Per-platform content rules — length, media counts, video/image mixing. A violation on
any target rejects the whole request with 400 invalid_post, before anything publishes.
That ordering is deliberate: everything you can fix is checked before a single byte reaches a
platform, so a rejected request never leaves a half-published post behind. A scheduled post is
validated the same way at submission, and has its quota checked again when it actually goes
out.
Publishing later
Add publishAt and the response is a 202 instead of a 201: the post is stored, every
target reads pending, and a dispatcher sends it when its time comes. Nothing else about the
request changes. POST /api/v1/posts/:id/cancel calls one off while it is still waiting, and
a scheduled post is immutable — moving one is cancel plus create. Full detail in
Scheduling a post.
Reading the response
201 Created — 202 Accepted when scheduled, or 200 with replayed: true for an
idempotent retry:
Target fields
Field
Meaning
`status`
`published` or `failed` — `pending` while a scheduled post waits, `processing` mid-dispatch
`platformPostId`
The platform's own id
`url`
Public URL of the post, when the platform returns one
`error`
`code: message` on failure, `null` otherwise
`publishedAt`
When the platform accepted it
`commentStatus`
`none`, `published`, `failed` or `unsupported` — independent of `status`
`commentId` / `commentError`
Outcome of the first comment
Mixed results are normal
Do not treat 201 as success
A 201 means the request was accepted and every target was attempted. Targets fail independently — one platform's rate limit, expired connection or media error does not stop the others. Always iterate post.targets and act per target.
Retrying only the failed targets is also the cheap thing to do: usage is recorded per target
that actually published, so a retry of the third target bills the third target only.
Timing
An immediate publish is synchronous and waits for every platform. Image posts settle in
seconds. Video is different — the platform processes the file server-side and Chirio waits for
it, up to roughly four minutes per item, so a video-heavy carousel can take several minutes.
Two consequences worth designing for:
Send an Idempotency-Key so a client-side timeout is safe to retry. See
Idempotency.
Poll GET /api/v1/posts/:id when a replayed response shows targets that have not
settled yet.
Scheduling sidesteps both: a 202 returns immediately, and the dispatcher does the waiting.
Listing history
Newest first. limit is capped at 100 and defaults to 25.
const { post } = await res.json();
const failed = post.targets.filter((t) => t.status === "failed");
if (failed.length > 0) {
// Retry only these accountIds, with a NEW idempotency key.
console.warn(failed.map((t) => `${t.platform}: ${t.error}`));
}