Publishing is synchronous and can run for minutes on video. A network failure or client
timeout leaves you unable to tell whether the post went out. An idempotency key removes the
question.
Keys are 1–255 characters and scoped per project, so two tenants can use the same string
without colliding.
What a retry does
Attempt
Status
Body
First
`201` — or `202` if you sent `publishAt`
`replayed: false`
Retry with the same key
`200`
`replayed: true`, the original post
Retrying never publishes twice. The post row carrying the key is inserted before any
platform call, and a unique index on (project, key) means a concurrent duplicate loses the
insert race and is answered with the original post instead.
A replayed response is a snapshot, not a promise
Replayed targets may not be final
A replay returns the stored per-target state at that moment. If the original request is still running — video processing, a slow carousel — some targets will still be pending. Poll GET /api/v1/posts/:id until they settle.
let { post, replayed } = await publish();
while (replayed && post.targets.some((t) => t.status !== "published" && t.status !== "failed")) {
await new Promise((r) => setTimeout(r, 5_000));
({ post } = await fetch(`${CHIRIO}/api/v1/posts/${post.id}`, { headers }).then((r) => r.json()));
}
A scheduled post replays the same way, and its targets stay pending until its time comes
— so poll on a schedule of your own rather than in a tight loop.
Choosing a key
Derive it from the logical post, not from the attempt:
Good: release-2-4-0, campaign-42-slot-3, a UUID stored with your own draft record.
Bad: a fresh crypto.randomUUID() per attempt — that is no key at all.
Use a new key when retrying failed targets
Reusing the key returns the original post untouched. To republish targets that failed, send a fresh request with only those accountIds and a NEW idempotency key — usage is recorded per target that actually published, so retrying the third of three targets bills one write.
Rescheduling works the same way. A scheduled post is immutable, so moving one is
followed by a fresh create — with a key, since the old one
would just hand back the post you cancelled.
POST /posts/:id/cancel
new
What is not idempotent — and what does not need a key
Only POST /api/v1/posts takes an idempotency key. POST /api/v1/accounts/connect mints a
new single-use OAuth state each call by design — an authorize URL that could be replayed would
be a security problem, not a convenience.
The repost endpoints need no key because they carry one implicitly. A repost of a published
target is billed once per target; a repost of any other post URL is billed once per account
and post. Calling either twice reaches the platform twice — where a repeat retweet is a no-op
— and bills once. See Metering.