Idempotency
Retrying a POST /pipelines/from-intent without coordinating with the server creates two pipelines. To avoid that, every mutating Agent API call accepts an idempotency_key field in the body (16–128 characters).
Replays with the same key return the same run_id / pipeline_id / artifact — never a new one.
Where the key lives
| Endpoint | Field |
|---|---|
POST /pipelines/from-intent | body idempotency_key |
POST /posts/{id}/draft | body idempotency_key |
POST /artifacts/{id}/publish | body idempotency_key |
POST /accounts/connect | body idempotency_key |
POST /fetch | body idempotency_key (optional — repeat (url, intent) pairs hit the response cache anyway) |
Read-only endpoints (GET /pipelines/{id}, GET /pipelines/{id}/inbox, etc.) ignore the field.
The MCP tools mirror the same parameter — every mutating tool exposes
idempotency_keywith the same semantics. See MCP · tools.
Format
| Property | Detail |
|---|---|
| Length | 16–128 characters. Shorter or longer returns validation_failed. |
| Charset | Free-form ASCII. UUIDv4 hex (32 chars after stripping dashes) is the recommended shape. |
| Scope | (organization_id, endpoint, key). Two different orgs can use the same key without colliding. |
| TTL | The server keeps the original response for the lifetime of the underlying resource — replays after the resource is deleted return 404 not_found, not a cached body. |
Replay states
| Situation | Outcome |
|---|---|
| Same key + same body + original completed | Returns the same run_id / resource id as the first call. Side effects do not repeat. |
| Same key + different body | The endpoint returns its normal error (typically validation_failed) — keys aren't bound to body shape, but the underlying invariants still apply. |
| Same key + original still running | The call returns the original's run_id. Poll that run; do not generate a new key. |
There is no 409 idempotency_request_in_progress HTTP today — from-intent returns immediately with the existing pipeline + the original run id.
Recommended client pattern
- Generate a UUIDv4 at the start of the logical attempt.
- Reuse it across every retry of the same attempt.
- Generate a new one only when the user takes intentional action again (it's a new action, not a retry).
const idempotencyKey = crypto.randomUUID().replaceAll("-", "");
// 32-char hex — safely within the 16–128 range.
await fetch(`${WHET_BASE_URL}/pipelines/from-intent`, {
method: "POST",
headers: {
"Authorization": `Bearer ${WHET_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
handle: "growth_dr",
intent: "Track for analytical drafts, daily.",
idempotency_key: idempotencyKey,
}),
});
What it is not
- It is not an application cache. Do not use it to "skip" requests because you already know the response — the server still validates invariants.
- It does not survive forever. Once the underlying resource is deleted, replays return
404 not_found. - It does not apply to
GET,PATCH, orDELETE. Make sure those retries are idempotent on your side.
See also: Authentication, Error taxonomy.