POST /api/v1/flows/{flowId}/runs is the only endpoint that creates expensive (and externally-visible) state — a fresh container, a billable invocation, a webhook delivery. Idempotency keys let you retry the call as many times as you want and only ever produce one run.
How to send a key
Two ways — they’re equivalent. Pick whichever your HTTP layer makes easy:
POST /api/v1/flows/{flowId}/runs HTTP/1.1
Authorization: Bearer flo_...
Idempotency-Key: order-9182-attempt-3
Content-Type: application/json
{ "prompt": "Process order 9182" }
Or in the body:
{
"prompt": "Process order 9182",
"idempotencyKey": "order-9182-attempt-3"
}
The header wins if both are present.
What you get back
- First call — fresh run created, returns
201 Created. - Repeat within 24 hours, same
(projectId, key)— returns200 OKwith the originalrunIdand areused: trueflag in the body:
{
"data": {
"runId": "0d29ff4e-...",
"status": "running",
"reused": true
}
}
Your client should treat reused: true as a successful trigger, not an error.
Key requirements
| Property | Rule |
|---|---|
| Length | 1–200 chars |
| Charset | Anything UTF-8. We don’t validate further; pick what’s useful for your debugging. |
| Uniqueness | Unique per (projectId, key). Reusing the same key across projects is fine — they’re scoped. |
| TTL | 24 hours from the first call. After that, the same key starts a new run. |
A common pattern: <external-id>-<attempt-no> so retries dedupe but a follow-up “rerun manually” call gets a fresh run.
What gets compared
We compare on (projectId, idempotencyKey) only — not on the request body. If you send the same key with a different prompt on a retry, we return the original run (with the original prompt) and the new prompt is ignored.
This is the standard tradeoff: stable retries vs sneaky body changes. Use a different key when you genuinely want different inputs.
What’s NOT idempotent
Every other endpoint:
POST /runs/{runId}/messages— sends the message every time. If your retry hits twice, the agent gets two copies.POST /runs/{runId}/files— writes each file every time. Same path overwrites; different paths accumulate.POST /runs/{runId}/stop— idempotent in effect (already-stopped returns 200 withalreadyStopped: true), so safe to retry.DELETE /runs/{runId}— first 204, subsequent 404.
If you need exactly-once semantics on follow-up messages or file injects, deduplicate on your side before calling.