# Idempotent requests

Secret writes take a requestId, so a retry after a lost response returns the first result instead of writing twice.

A network failure can leave you unsure whether a write went through. Secret writes take a
`requestId`, your own ID for the request, so you can send the same request again safely: the vault
returns the first write's result instead of writing a second version.

## Which requests take a `requestId`

| Endpoint                                                             | A retry with the same `requestId`                                     |
| -------------------------------------------------------------------- | --------------------------------------------------------------------- |
| [Update a secret](https://docs.qa.clankership.net/api/secrets/update.md)                               | Returns the first write's versions                                    |
| [Set a secret across products and environments](https://docs.qa.clankership.net/api/secrets/assign.md) | Returns the first write's versions                                    |
| [Generate a secret value](https://docs.qa.clankership.net/api/secrets/generate.md)                     | Returns the first write's versions and public keys, never the values  |
| [Restore a secret version](https://docs.qa.clankership.net/api/secrets/restore-version.md)             | Returns the first restore's version                                   |
| [Create a secret](https://docs.qa.clankership.net/api/secrets/create.md)                               | Fails with `409 Conflict`, because the first attempt created the name |

`requestId` is a string of 1 to 100 characters. Use a fresh UUID for each new write, and keep it
when you retry that write.

```bash
REQUEST_ID="$(uuidgen)"
curl https://api.clankertest.com/v1/workspaces/acme/vault/assignments \
  -H "Authorization: Bearer $CLANKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"products\": [\"storefront\"],
    \"environments\": [\"staging\"],
    \"name\": \"LOG_LEVEL\",
    \"value\": \"debug\",
    \"requestId\": \"$REQUEST_ID\"
  }"
```

```json
[{ "product": "storefront", "environment": "staging", "status": "written", "version": 4 }]
```

Sending the same body again with the same `REQUEST_ID` returns the same `version: 4` and writes
nothing.

## What it guarantees

- **One version per write.** The vault stores the ID with each version it writes. A retry that
  finds the ID on a secret's environment returns that version instead of writing another, so a
  deployment never sees the value change twice.
- **Same request, same answer.** The retry must ask for the same thing: the same value, the same
  version to restore, or the same generator. If it asks for something else with an ID already used,
  it fails with `409 Conflict` and the message `Request ID was already used for a different write`.
  Nothing is saved, because each request writes in one transaction.
- **Per secret and environment.** An ID is checked against the versions of the secret and
  environment the request writes. A retry of a bulk write is answered pair by pair.
- **Values only.** The ID covers the values a request writes. A rename writes no version, so retry
  an update that also renamed the secret by its new name.
- **Before stale checks.** A retry is answered from the first write before `expectedVersions` is
  compared, so retrying a write that succeeded does not fail as stale.
- **Generation.** A retried generation returns the first versions and public keys without values,
  even with `reveal: true`, so a generated value is shown at most once. A retry that adds pairs the
  first request did not write fails with `409`, so products in one environment never end up with
  different values.

A pair the caller may not write, such as a member's production pair, comes back `skipped` and
stores nothing, so a retry skips it again.

## Creating a secret

[Create a secret](https://docs.qa.clankership.net/api/secrets/create.md) stores the `requestId` with each first value it writes, but
it checks the name before the ID: once the first attempt has created the name, a retry fails with
`409 Conflict` and the message `Secret name already exists on this product`. Treat that `409` on a
retry as success, then read the secret with [Get a secret](https://docs.qa.clankership.net/api/secrets/get.md) to confirm its values.

## The CLI

`clanker vault secret set`, `clear`, `generate` and `restore` send a new random `requestId` with
each command. To retry a command whose result you did not see, run it again with the same ID:

```bash
printf %s "$VALUE" | clanker vault secret set --product storefront --env staging --name LOG_LEVEL \
  --request-id 5b0e8f3a-2c41-4d9e-9a7b-61f2c3d4e5a6
```

## Other requests

No other endpoint takes a `requestId`. Before retrying them, know how each behaves:

- **Safe to repeat.** `GET` requests; `PATCH` requests other than secret writes, which set the same
  fields to the same values; archive and restore, which return the object unchanged when it is already archived or
  restored; and test-run file uploads, which replace the same file.
- **Fail on repeat.** Creates with a unique slug, name or pair, such as a product, an environment
  or a deployment, fail with `409 Conflict` the second time. Read the object to confirm it exists.
- **Not idempotent.** [Create a ticket](https://docs.qa.clankership.net/api/tickets/create.md),
  [Send a message](https://docs.qa.clankership.net/api/messages/send.md) and [Create a comment](https://docs.qa.clankership.net/api/comments/create.md) create a new
  object each time. Before retrying, list what exists, as
  [Create tickets from a script](https://docs.qa.clankership.net/get-started/examples/create-tickets-from-a-script.md) does.

MCP tools say the same with their `idempotentHint`; see [Permissions](https://docs.qa.clankership.net/mcp/permissions.md#tool-hints).
