# Idempotency and concurrency

A lost HTTP response does not tell you whether a mutation committed. Give each mutation intent a durable idempotency key and retain it while recovering an uncertain result. Retry the exact request with that key only when the operation's contract permits it. For updates, also bind the intent to the resource version you reviewed using `If-Match`.

## One key per mutation intent

Mutation operations that require `Idempotency-Key` declare it in the [API reference](/api-reference/overview/) and generated SDK method inputs. The operating-system endpoints validate keys as 8–160 characters. A UUID or a short operation prefix followed by a UUID is suitable. Persist the key together with the exact operation, organization, request body, and expected resource version before sending the request.

```text
Idempotency-Key: update-proposal:5d1662de-760b-43d4-90a8-e1da4413de70
```

For the operating-system record mutations, the server scopes a key by organization and authenticated actor. It hashes the HTTP method, path, body, organization, actor ID, actor role, and `If-Match` value. Repeating the same key and request replays the stored result. Reusing the key for a different request returns `409`. Signing in as a different actor or using a replacement service credential changes the identity boundary: it is not a retry of the original actor's intent.

Do not generate a fresh key every time an HTTP request is sent. A timeout, connection reset, SDK cancellation, or browser reload can happen after the server committed. A new key can create a second mutation. Keep retries sequential rather than issuing several concurrent requests for the same intent.

## Read, review, then update

Versioned resource updates require a strong quoted `If-Match` value. Reads return the `resourceVersion` in the record envelope and, where specified by the endpoint, an `ETag` header. The SDKs accept a raw resource version and add the quotation marks for you.

```text
If-Match: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
```

The update can commit only if the expected version is still current. `428` indicates a missing or invalid precondition; weak ETags such as `W/"..."` are invalid. `412` indicates that the expected version is stale. Read the resource again and review the changed state before creating a new mutation intent. Changing `If-Match` while keeping the old key changes the request and can produce an idempotency conflict.

### TypeScript example

The example pauses a webhook from code bundled into the signed-in Studio application on its own origin. The browser sends the existing interactive session cookie with `credentials: 'include'`; the session is not a bearer token. The signed-in actor must have the role required for this mutation. `webhooks:manage` API-key/OAuth access covers webhook GET and POST, not PATCH.

```ts
import { LiquidityOsClient } from '@klineo/liquidity-os-sdk';

// Obtain these IDs from the signed-in app's current workspace and selection.
const organizationId = 'your-organization-id';
const webhookId = 'your-webhook-id';
const client = new LiquidityOsClient({
  baseUrl: new URL('/api/liquidity-studio/v2', window.location.origin).href,
  organizationId,
  credentials: 'include',
});
const current = await client.getWebhooks({
  path: { id: webhookId },
});

const intent = {
  path: { id: current.data.id },
  body: { status: 'PAUSED' as const, reason: 'Pause delivery during receiver maintenance.' },
  idempotencyKey: `pause-webhook:${crypto.randomUUID()}`,
  ifMatch: current.data.resourceVersion,
};
// Save intent in durable application storage before sending it.
// Any transport retry must load and reuse the complete saved intent.
const paused = await client.supersedeWebhooks(intent);
console.log(paused.data.status);
```

### Python example

This server-side example creates a subscription using an API key or OAuth bearer token with `webhooks:manage`. The credential owner also needs `ORGANIZATION_OWNER` or `KLINEO_OPERATOR` authority. Set `KLINEO_API_BASE_URL` to the environment's full `/api/liquidity-studio/v2` root and `KLINEO_WEBHOOK_RECEIVER_URL` to your real public HTTPS receiver.

```python
import os
import uuid
from klineo_liquidity_os import LiquidityOsClient

client = LiquidityOsClient(
    os.environ["KLINEO_API_BASE_URL"],
    organization_id=os.environ["KLINEO_ORGANIZATION_ID"],
    access_token=os.environ["KLINEO_ACCESS_TOKEN"],
)
intent = {
    "body": {
        "url": os.environ["KLINEO_WEBHOOK_RECEIVER_URL"],
        "eventTypes": [
            "record.simulation_study.completed",
            "record.proposal_workspace.validated",
        ],
        "reason": "Notify our integration when study and proposal records change.",
    },
    "idempotency_key": "create-webhook:" + str(uuid.uuid4()),
}
# Save intent in durable application storage before sending it.
# Any transport retry must load and reuse the complete saved intent.
created = client.create_webhooks(**intent)
# Write created["data"]["signingSecret"] directly to your secret manager now.
# Do not log the secret or save it alongside non-secret intent metadata.
print(created["data"]["subscription"]["id"])
```

The snippets show the SDK arguments; the comments mark the durable-storage step your integration must supply. Running either snippet again from the beginning generates a new intent. A retry must reuse the previously saved arguments instead. Webhook creation returns its signing secret only once: if creation committed but that response was lost, an identical retry returns `LIQUIDITY_OPERATING_SYSTEM_SECRET_ALREADY_RETURNED` with non-secret metadata. It cannot retrieve the secret; follow the recovery steps below.

## Interpret results

The TypeScript and Python clients send the supplied key and version. They do not automatically retry mutations or persist mutation intents. Keep the original request available in your application and choose retry behavior based on the operation and response.

| Result | Action |
| --- | --- |
| Completed success | Mark the intent complete. Repeating it can replay the stored response. |
| Network failure, timeout, or an ambiguous `5xx` | Preserve the key and exact request. Retry with bounded backoff and reconcile current state. |
| `429` | Preserve the intent and respect the endpoint's rate-limit guidance before retrying. |
| `409` with a key/request conflict | Compare the saved intent to the request. Do not change the key just to bypass the conflict. |
| `409` with an unresolved processing claim | Preserve the intent and reconcile before retrying; this is not proof that no mutation happened. |
| `412` stale version | Read and review current state, then create a new key for the newly reviewed intent. |
| `428` invalid or missing `If-Match` | Read the required resource version and form a corrected intent. |
| Definitive validation or authorization failure | Resolve the cause and review the intent. A stored completed client failure requires a new key for the renewed intent; a rejection before the mutation handler is not stored. Follow the operation-specific guidance below. |

For the ordinary operating-system record mutation handler, successful responses and handled `4xx` responses are stored for replay, except that `LIQUIDITY_STUDIO_REAUTHENTICATION_REQUIRED` is left unrecorded. Failed domain changes are rolled back before a handled error is stored. Unhandled server failures roll back the transaction. The API can mark ordinary response replays with `Idempotency-Replayed: true`. SDK methods return the decoded response body; configure TypeScript `onResponse(response)` to inspect `response.headers`, or Python `on_response(status, headers)` to inspect successful response headers. Python HTTP failures expose rate-limit guidance through `LiquidityOsApiError.retry_after`. Observers should inspect headers without consuming response bodies or throwing, because response decoding continues afterward.

Idempotency is an intent identity, not a permanent lock on the resource. Other valid intents can update it, so a replayed success may describe an older version. Read the resource again when current state matters. Endpoint-specific workflows can have additional conflict or admission rules; follow the error code and the endpoint's contract.

## Authentication failures and stored results

An HTTP `401` alone does not determine whether its result was stored. Distinguish where the operation rejected the request:

| Rejection boundary | Stored mutation result | Recovery |
| --- | --- | --- |
| Session, service authentication, or service-scope middleware before the mutation handler | None created by that rejection | Restore authorized access and review the intent. Retain the original key if the actor and exact intended request are unchanged. |
| Recent-authentication guard inside the ordinary v2 record mutation handler | That guard's rejection is left unrecorded | Complete interactive reauthentication, review current evidence, and retain the original key for an unchanged intent. |
| Recent-authentication guard inside credential creation or rotation's one-time-secret mutation handler | Completed non-retryable `401` | Reauthentication with the same key replays the stored failure. After confirming this definitive result, reauthenticate, review the action and current resource version, then submit a renewed intent with a new key. |
| Another handled non-retryable client failure completed by a mutation handler | Stored failure that an identical request replays | Resolve the cause, review the corrected intent, and assign it a new key after establishing the original result. |

Keep the original request and result for audit and troubleshooting. If the response was lost, a transport failure occurred, or completion is otherwise uncertain, preserve the original key and recover or reconcile that attempt first. Do not automatically create a new key or retry every authorization failure. A different actor or service credential changes the request identity boundary; it cannot recover a prior actor's intent by presenting the same key.

## One-time secrets are different

Developer credential issuance, developer credential rotation, and webhook subscription creation return their secret only in the first successful response. Their idempotency record contains non-secret resource metadata. Identical later requests return `409` with `LIQUIDITY_OPERATING_SYSTEM_SECRET_ALREADY_RETURNED`; they cannot replay the secret.

Store a newly issued secret directly in your secret manager as soon as it is received. If the first response was lost after commit, use the non-secret metadata and an authorized administrator to recover safely: rotate or revoke the developer credential, or revoke and replace the webhook subscription. Do not repeatedly create fresh keys hoping to retrieve the original secret. Secret delivery is at-most-once; resource creation remains tied to the original idempotent intent.

## Webhook deduplication is separate

`Idempotency-Key` protects your outbound API mutation. A webhook's `x-klineo-webhook-delivery` protects your inbound event handling when you use a durable unique inbox. They are different identifiers with different lifecycles. Follow the [webhook verification and replay guidance](/developers/webhooks) before acknowledging a delivery.

Neither idempotency nor `If-Match` grants execution authority. A successful preparation request, a replayed response, or a webhook acknowledgment cannot substitute for required approvals, policy checks, Safe authorization, or verified execution receipts.
