---
title: Idempotency
description: Which endpoints require an Idempotency-Key, how the key is scoped, what happens on replay and on conflict, and how to choose good keys.
canonical_url: "https://cascades.crscreditapi.com/docs/concepts/idempotency"
doc_version: "1.0.0"
last_updated: "2026-10-05T19:24:37Z"
source: docs/concepts/idempotency.md
---

# Idempotency

A screening run pulls reports and may bill. A retried request must therefore never start a second run. Every endpoint that creates a run requires an `Idempotency-Key` header and uses it to return the original result on replay.

## Which endpoints require a key?

| Endpoint | Key scope |
|---|---|
| `POST /workflows/{id}/trigger` | Per API client (the key is stored as `execution_key = api_client:<client id>:<your key>`) |
| `POST /workflows/{id}/batches` | Per API client, compared against a SHA-256 of the `rows` JSON |
| `POST /executions/{id}/re_evaluations` | Per source execution (`reeval:<execution id>:<your key>`) |

Omitting the header returns `422 missing_idempotency_key`. The header value is limited to 200 characters (the trigger namespace prefix consumes some of the stored key, so keep values comfortably shorter).

## What happens on replay?

- **Same key, same body** → the original response, again: `202 Accepted` with the same execution or batch. Nothing new is created.
- **Same key, different body** → `409 idempotency_conflict`. The stored request is not overwritten. A trigger answers with the message below; a batch says "…with a different request." instead.
- **Concurrent identical requests** are race-safe: both callers receive the same execution; the loser of the insert race re-reads the winner's row.

```json
{
  "error": {
    "code": "idempotency_conflict",
    "message": "The Idempotency-Key has already been used with a different request payload.",
    "details": [],
    "request_id": "7d4c2f48-…",
    "doc_url": "https://cascades.crscreditapi.com/docs/api/errors/idempotency_conflict"
  }
}
```

## How do I choose a key?

Derive it from the upstream business event, not from the attempt: an application id, an order id, a submission id. Then a retry from a timeout, a crashed worker or a redeploy naturally carries the same key. Never reuse a key across distinct events, and never generate a fresh UUID per attempt — that defeats the protection entirely.

```http
Idempotency-Key: rv-app-20491        # good: the application id
Idempotency-Key: 3fa85f64-…          # fine, if you persist it before the first attempt
Idempotency-Key: $(uuidgen)          # bad: every retry is a new run
```

The same discipline applies to `external_reference`: it is not an idempotency key (two triggers with the same reference create two runs), but it is what you correlate results and webhooks back to.

## Related

- [Executions](/docs/concepts/executions)
- [Batches](/docs/concepts/batches)
- [`POST /workflows/{id}/trigger`](/docs/api/workflows/trigger-workflow)

## Sitemap

See the full [sitemap](https://cascades.crscreditapi.com/sitemap.md) for all pages.
