ChirpieDocs

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/json

The 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_invalid rather than ignored. Sending a key is a request for deduplication, so answering 201 while 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

EndpointWhat it protects
POST /api/v1/postsPublishing or scheduling a post, on one account or a fan-out
POST /api/v1/threadsPublishing or scheduling a thread
POST /api/v1/mediaAn upload, so a retry does not leave a second copy behind
POST /api/v1/posts/:id/first-commentRetrying a first comment
POST /api/v1/posts/:id/comments/:commentId/replyReplying 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: true

That 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-9

chirpie 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.

On this page