Skip to content

// Engineering - October 18, 2026

Idempotency keys for AI agents.

An idempotency key is a unique value, usually a UUID, that a client sends with a write request so the server can recognize a retry and return the first result instead of doing the work twice. AI agents need one because they retry after timeouts and dropped connections, and an unkeyed retry creates duplicate cards, comments or payments.

What is idempotency, and what does an idempotency key add?

An operation is idempotent when doing it twice has the same effect as doing it once. HTTP's core specification, RFC 9110 section 9.2.2, puts it this way: a method is idempotent "if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request." By that definition PUT, DELETE and the safe methods such as GET are idempotent. POST is not on that list, and PATCH (defined separately in RFC 5789) is not guaranteed to be idempotent either. Those are the methods that create cards, post comments and move work.

The same section says a client "SHOULD NOT automatically retry a request with a non-idempotent method" unless it has some way to know the retry is safe. An idempotency key is that way. The client creates a unique value for one intended change and sends it with the request. The server records the key with the result. If the same key comes back, the server returns the stored result instead of running the write again.

There is no finished standard for it yet. The IETF HTTP APIs working group drafted an Idempotency-Key header, but as of October 2026 the latest version (draft-07, October 2025) has expired without becoming an RFC. In practice each API defines its own rules, so read them before you rely on a key. For the basics of keys in automation scripts, see our guide to API integrations. This post goes deeper on agents.

Why do AI agents need idempotency keys more than scripts do?

Agents retry on their own, and often cannot tell whether the last write landed. A script retries when you tell it to. An agent retries whenever its plan says the last step failed. Three situations come up all the time:

  • A tool call times out. The request reached the server and the card was created, but the response never came back. From the agent's side, nothing happened.
  • The agent second-guesses itself. A model that sees an ambiguous or empty result may decide to "try again" with the same arguments.
  • A session restarts. A new session reads the task, sees a step without a recorded result and runs it again.

Each of these turns into a duplicate unless the write is safe to repeat. Amazon's Builders' Library article Timeouts, retries and backoff with jitter makes the same point for services: APIs with side effects are not safe to retry unless they provide idempotency.

The Model Context Protocol has a related hint. Its 2026-07-28 schema lets a server mark a tool with idempotentHint, meaning "calling the tool repeatedly with the same arguments will have no additional effect on its environment." It defaults to false, and the tools spec says clients must treat annotations as untrusted unless they come from trusted servers. A hint tells a client whether a retry is probably harmless. It does not make the retry harmless. That still takes a key or a tool that is idempotent by design.

How do idempotency key rules differ between APIs?

APIs differ on four points: what the key looks like, how long it is kept, what happens when a key is reused with a different body, and what happens when a retry arrives while the first request is still running. The table compares the IETF draft, Stripe (the best-known public example) and Flux, each from its own primary source, as of October 2026.

Question IETF draft-07 Stripe API v1 Flux REST API
Header Idempotency-Key Idempotency-Key X-Idempotency-Key
Key format A UUID or similar random identifier is recommended Up to 255 characters; V4 UUIDs suggested Must be a UUID
How long it is kept Each API should publish its own expiry policy Can be removed once at least 24 hours old Stored on the change's activity log entry; no fixed expiry window
Same key, different body Should be rejected with 422 Parameters are compared, and a mismatch is an error Not compared: the earlier result comes back and the new change is skipped
Retry while the first is still running Should get 409 Conflict Not saved as a result, so it can be retried Not detected: both copies can run and make the change

Sources: draft-ietf-httpapi-idempotency-key-header-07, Stripe idempotent requests, and for Flux, the server code behind its REST API. The Flux column is the one we can speak to first-hand, and its last two rows are the ones to design around: one key per intended change, and never two copies of a request in flight at once.

How do you send idempotency keys to the Flux REST API?

Send a UUID in the X-Idempotency-Key header on the write request. Flux, task management for AI agent teams, checks the header before it does the work. When it finds the key on an earlier change, it answers 200 with "deduplicated": true and the stored result (create, duplicate and update routes return the stored card, column or board; move, reorder and delete routes return a plain success flag). Otherwise it makes the change and saves the key on the change's activity log entry. Authenticate with a Flux API key as a Bearer token. The key needs the Write scope.

KEY=$(uuidgen)   # one key per intended change, created once

curl -sS https://flux.umin.ai/api/cards \
  -H "Authorization: Bearer $FLUX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: $KEY" \
  -d '{"boardId":"BOARD_UUID","columnId":"COLUMN_UUID","title":"Fix login redirect loop","priority":"high"}'

# Timed out? Send the same request with the same $KEY.
# If the first attempt landed, Flux answers 200 with
#   {"card": {...}, "deduplicated": true}
# and does not create a second card.

For agent tool code, wrap every write in one helper. The key is created once, before the first attempt, and every retry reuses it. The helper retries only on timeouts, network errors, 429 rate limits and 5xx responses, waits a random, growing time between attempts (jitter keeps many clients from retrying in lockstep), and gives up after four tries:

import { randomUUID } from 'node:crypto'

const API = 'https://flux.umin.ai/api'
const MAX_ATTEMPTS = 4

// Retry one Flux write without duplicates: one key, reused on every attempt.
export async function fluxWrite(path: string, method: 'POST' | 'PATCH', body: unknown) {
  const key = randomUUID() // created once, before the first attempt
  let lastError: unknown

  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
    try {
      const res = await fetch(API + path, {
        method,
        headers: {
          Authorization: 'Bearer ' + process.env.FLUX_API_KEY,
          'Content-Type': 'application/json',
          'X-Idempotency-Key': key,
        },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(15_000),
      })
      if (res.ok) return res.json() // a replay carries deduplicated: true
      if (res.status < 500 && res.status !== 429) {
        // 4xx other than 429, including a 409 version conflict: retrying the same request won't help
        throw Object.assign(new Error('Flux ' + res.status + ': ' + (await res.text())), { final: true })
      }
      lastError = new Error('Flux ' + res.status)
    } catch (err) {
      if ((err as { final?: boolean }).final) throw err
      lastError = err // timeout or network error: the write may or may not have landed
    }
    if (attempt < MAX_ATTEMPTS) {
      // exponential backoff with random jitter: wait up to 0.5 s, 1 s, then 2 s
      await new Promise((r) => setTimeout(r, Math.random() * 500 * 2 ** (attempt - 1)))
    }
  }
  throw lastError
}

Call it as fluxWrite('/cards', 'POST', { boardId, columnId, title }). If your agent can be stopped and resumed, go one step further and save the key in the agent's own local state (not on a shared card) before the first attempt, so a new session retries with the old key instead of making a new one.

Which Flux writes take a key?

As of October 2026, these REST routes read X-Idempotency-Key:

  • Cards: create, update and delete (POST, PATCH and DELETE /api/cards), plus /api/cards/move, /api/cards/reorder and /api/cards/duplicate.
  • Columns: create, update and delete (/api/columns), plus column move, reorder, duplicate and "move all cards".
  • Boards: create (POST /api/boards) and update a single board (PATCH /api/boards/{boardId}). The two board delete routes also read the header, but they reject API keys with 403, so only a signed-in session can delete a board.

Comments, checklist items and labels do not read the header. A retried comment is a second comment, so treat those like the unkeyed MCP calls below. The full route list is in the API docs.

What about agents that use the Flux MCP server?

The Flux MCP server has no idempotency key, and its tools do not declare MCP annotations. Flux records keys on activity log entries, and changes made over MCP are not written to the activity log yet, so there is nothing for a key to match against. What you can do is know which tools are safe to repeat. From the tool code:

MCP tool What a retry with the same arguments does Safe to retry blindly?
create_card Creates a second card with a new ID No
add_comment Posts the comment twice No
create_column Adds a second column with the same title No
update_card Writes the same values again; only the card's version number goes up Yes, in effect
reorder_cards Sets the same columns and positions again Yes, in effect
move_card Leaves the card in the target column, moved to the bottom again Mostly; the position can change

For the three that are not safe, have the agent look before it retries. search_cards matches card titles (not descriptions) and returns up to 20 results, and get_board lists every column and its active cards, so the agent can check whether the first create_card landed. For add_comment, get_card returns the card's comments. Put the rule in the agent's instructions: "If a write tool times out, read the board before you call it again." If a write must never duplicate, such as cards filed by an unattended QA run, send it through the REST API with a key instead.

Idempotency keys or version checks: which problem does each solve?

They solve different problems, and agents that write need both. An idempotency key protects you from your own retry. A version check protects you from someone else's change: it stops a lost update, where an agent reads a card, a person edits it, and the agent then writes its stale copy over the person's edit. This pattern is called optimistic concurrency, or optimistic locking.

In Flux every card, column and board has a version number that goes up when it changes. Send the version you read as expectedVersion on PATCH /api/cards, PATCH /api/columns or PATCH /api/boards/{boardId}, and Flux answers 409 Conflict if the number has moved. That matches the meaning of 409 in RFC 9110: a conflict with the current state that "the user might be able to resolve" before resubmitting.

# 1. Read the card and note its version
curl -sS https://flux.umin.ai/api/cards/CARD_UUID -H "Authorization: Bearer $FLUX_API_KEY"
#    {"id":"CARD_UUID","title":"...","version":7, ...}

# 2. Send the change with the version you read, under a NEW key (never reuse the create call's key)
KEY=$(uuidgen)
curl -sS -X PATCH https://flux.umin.ai/api/cards \
  -H "Authorization: Bearer $FLUX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: $KEY" \
  -d '{"cardId":"CARD_UUID","expectedVersion":7,"description":"Plan v2: ..."}'

# If someone changed the card in between, Flux answers 409:
#   {"error":"Card was modified by another user (expected v7, current v8). Please refresh.","code":"CONFLICT"}

The two work well together because Flux checks the key before the version. If the first PATCH landed and its response was lost, the retry with the same key returns the stored result. It does not fail with a 409 against the agent's own change. On a real 409, the agent should read the card again and decide: merge its change into the new text, or leave a comment for a person. Resending the same body with the new version number would overwrite the edit the check just caught.

Two honest limits. First, the version check is optional: Flux only compares when you send expectedVersion. Second, Flux reads the version, compares it, then updates, and the update itself does not re-check the version. That closes most of the gap between your read and your write, but two writes landing in the same instant can both pass. It is a guard against stale data, not a lock, and it is not a way to claim tasks. For claiming, see how to coordinate multiple AI coding agents.

A retry checklist for agent tool code

Retry-safe agent code follows eight rules. Each one is covered above.

  1. One key per intended change. Create a UUID when the agent decides to make a change, not per HTTP attempt.
  2. Save the key before the first attempt, in the agent's own local state (not on a shared card), so a restarted session reuses it.
  3. Reuse it only for the same change. Flux does not compare bodies, so a reused key silently skips the new change.
  4. Retry only on timeouts, network errors, 429 and 5xx. Any other 4xx means the request itself needs fixing.
  5. Never send two copies at once. Wait for the first attempt to fail or time out before you retry.
  6. Back off with jitter and cap the attempts, so a struggling server is not hit harder.
  7. Send expectedVersion on edits, and treat a 409 as "read again and decide", never "retry".
  8. For unkeyed writes, look before you retry. That covers MCP's create_card, add_comment and create_column, and REST comments.

Card, column and board writes sent through the REST API also land in the board's activity log, under the name of the person who created the API key. So you can see what an agent changed, and undo its card and column changes from the history. Our post on event-sourced activity logs explains how that history is built, and the kanban board for AI agents guide shows how agents and people share one board.

// FAQ
06 questions
01

What is an idempotency key?

+

An idempotency key is a unique value, usually a random UUID, that a client attaches to a write request. The server remembers which keys it has already processed. When the same key arrives again, the server returns the earlier result instead of repeating the write, so a retry after a timeout cannot create a duplicate.

02

Is POST idempotent?

+

No. RFC 9110 defines PUT, DELETE and the safe methods (such as GET) as idempotent, and POST is not among them. PATCH (RFC 5789) is not guaranteed to be idempotent either. Two identical POSTs can create two resources. APIs make POST and PATCH safe to retry by accepting an idempotency key, the approach described in the IETF Idempotency-Key draft.

03

Do Flux MCP tools accept an idempotency key?

+

No, not as of October 2026. The X-Idempotency-Key header works on the Flux REST API. Over MCP, a retried create_card or add_comment makes a second card or comment, so check the board before retrying those calls, or use the REST API for writes that must never duplicate.

04

How long does Flux keep an idempotency key?

+

Flux stores the key on the activity log entry for the change, and there is no fixed expiry window, so a retry hours later is still recognized. Stripe API v1 works differently: its docs say keys can be removed once they are at least 24 hours old.

05

Can I reuse an idempotency key for a different request?

+

No. Flux does not compare request bodies, so a reused key returns the earlier result and the new change is skipped. Create a fresh UUID for each change you intend to make, and reuse it only to retry that same change.

06

What is the difference between an idempotency key and a version check?

+

An idempotency key stops your own retry from applying a change twice. A version check (optimistic concurrency) stops you from overwriting a change someone else made since you read the data. In Flux, send expectedVersion on PATCH /api/cards to get a 409 Conflict instead of a silent overwrite. Agents that write should use both.

// Retry-safe agent writes

Agents that retry. Boards without duplicates.

Give your agents a Flux API key and send each write with an idempotency key. Free to start, and agents never take a seat.