Idempotency
Retry a post, thread, upload or comment safely with an Idempotency-Key, so a timeout or a network blip does not publish the same thing twice. Keys are remembered for 24 hours.
Why you want this
A network failure is not the same as a failed request. A connection that drops after Chirpie accepted your post but before the response reached you leaves you with no answer and no way to tell whether the post went out. Retrying might publish it twice. Not retrying might mean it never went out at all.
An Idempotency-Key closes that gap. Send one on the original request, and any retry of the
identical request gets the original answer back instead of publishing again.
Agents hit this more than people do, because an agent retries automatically and cannot look at the timeline to check.
The header
POST /api/v1/posts
Authorization: Bearer chirpie_sk_YOUR_KEY
Idempotency-Key: 8f1c0e0c-6d51-4d73-9f4e-6b2a0a2d3f11
Content-Type: application/jsonThe key is yours to choose. A UUID per logical operation is the usual thing. It is scoped to your account, so it cannot collide with anyone else's, and it is bound to a hash of the method, the path and the body of the request that first used it.
- Maximum 255 characters. A longer value, or a blank one, is refused with
400 idempotency_key_invalidrather than ignored. Sending a key is a request for deduplication, so answering201while quietly providing none would leave you believing your retries were safe when they were not. - A key lasts 24 hours. After that it expires and is free to use again, though reusing one is not a habit worth forming.
Where it works
| Endpoint | What it protects |
|---|---|
POST /api/v1/posts | Publishing or scheduling a post, on one account or a fan-out |
POST /api/v1/threads | Publishing or scheduling a thread |
POST /api/v1/media | An upload, so a retry does not leave a second copy behind |
POST /api/v1/posts/:id/first-comment | Retrying a first comment |
POST /api/v1/posts/:id/comments/:commentId/reply | Replying to a comment |
Every other endpoint ignores the header. GET and DELETE do not need it: reading is free of
side effects, and deleting the same post twice is already harmless.
The three outcomes
Once a key has been used, the next request carrying it lands in exactly one of three places.
Same key, same request: a replay
You get the original response back, with its original status code, and one extra header:
HTTP/1.1 201 Created
Idempotent-Replay: trueThat header is the only way to tell "this published just now" from "this published on your last attempt". Nothing is sent to the platform, no quota is spent, and no second post exists.
Same key, a different request: 422
{
"error": {
"code": "idempotency_key_reused",
"message": "Idempotency-Key 'post-2026-11-01' was already used for a different request. Use a new key for a new request, or send the original request again to get its result back."
}
}The key is bound to the request it was first used with, so changing the text, the account or the endpoint under the same key is refused rather than replaying an answer that does not match what you asked for. Generate a new key per distinct request.
Same key, first request still running: 409
{
"error": {
"code": "idempotency_in_progress",
"message": "Idempotency-Key 'post-2026-11-01' is still being processed. Retry in a moment to get the result of the first request."
}
}Chirpie answers immediately rather than holding your connection open. Publishing a long thread across several accounts can take a while, and queuing you behind it would turn one slow request into two. Retry the same request in a moment and you will get the replay.
What is stored, and what is not
Anything under 500 is stored and replayed, errors included. A 400 for text over a
platform's limit replays as that same 400. That is deliberate: the request was answered, and
answering it differently on a retry would be a lie about what happened.
This starts once the key is claimed, which is after your key is authenticated and counted
against the burst limit. A 401, a 403 insufficient_scope or a 429 rate_limited is
therefore never stored against a key: the request never got as far as claiming one, and a
later retry is answered on its own merits.
A 5xx releases the key. Those are the failures a retry is meant to fix, so the retry is
a real retry and runs the work again.
A crash releases nothing, because nothing is left running to release it. The claim sits
in_progress and a retry is answered 409 idempotency_in_progress until it goes stale, ten
minutes after it was made, at which point the next retry takes it over and runs the work.
With one exception: 502 thread_rollback_incomplete is stored and replayed. It means a
thread failed part-way and the parts already published could not be deleted, so they are
still on the platform (error.thread_rollback.still_live names them). Releasing the key
there would make the retry republish everything that is already live, which is the one thing
this page exists to prevent. Take the posts down first, then use a new key.
The upshot: retry on 5xx and on a network failure, fix the request on a 4xx. A 4xx will
not change its mind, and with a key attached it will keep telling you the same thing for 24
hours.
Using it from the SDK, CLI, MCP and n8n
SDK
createPost() and createThread() generate a fresh key on every call, which protects the
one HTTP request it rides on. Calling the method again is a new call and gets a new key, so to
make your own retry replay rather than publish again, pass the same key each time: store it
beside the job and send it with every attempt. That is also what survives a process restart.
// A key is generated automatically.
await chirpie.createPost({ account_id, text: "Hello" });
// Or supply your own, so the key survives a process restart.
await chirpie.createPost(
{ account_id, text: "Hello" },
{ idempotencyKey: "campaign-2026-11-01-launch" }
);
// Or send none at all.
await chirpie.createPost({ account_id, text: "Hello" }, { idempotencyKey: null });uploadMedia(input, options), retryFirstComment(id, options) and
replyToComment(postId, commentId, text, options) take the same options object, but generate
nothing: pass idempotencyKey yourself when you want one.
CLI
chirpie post "Hello" --account ACCOUNT_ID --idempotency-key campaign-2026-11-01
chirpie thread "One" "Two" --account ACCOUNT_ID --idempotency-key thread-42
chirpie posts first-comment POST_ID --idempotency-key retry-comment-7
chirpie comments reply POST_ID COMMENT_ID --text "Thanks" --idempotency-key reply-9chirpie post and chirpie thread generate a key per invocation when you do not pass one, the
same way the SDK does. To make re-running the same command replay rather than republish, name
the key yourself.
MCP
chirpie_post, chirpie_thread, chirpie_upload_media, chirpie_retry_first_comment and
chirpie_reply_to_comment each take an idempotency_key parameter.
n8n
Post > Create, Thread > Create and Media > Upload each carry an Idempotency Key option under Options. Set it from an expression that is stable across a retry of the same item, such as the workflow execution id plus the item index, rather than something that changes each run.
Picking a key
A key should identify the operation you mean to perform once, not the attempt.
- A UUID generated when you first decide to publish, reused by every retry of that decision.
- A deterministic name you can recompute:
campaign-2026-11-01-x-launch. This survives your process restarting, which a UUID held in memory does not. - Not a timestamp of the attempt, and not a random value per retry. Both defeat the point.
The dashboard's compose dialog sends one key per submit, which is why a double click there cannot produce two posts.
Post to several accounts at once
Send one request to many connected accounts, with per-account text and media, and read the whole fan-out back by its group id.
When a connection dies
How Chirpie tells you a social account stopped accepting posts, what happens to anything you had scheduled on it, and how to get back to publishing.