Klineo/Docs
Open app ↗

Developers

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 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 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.