Idempotency¶
Thirteen endpoints change state in a way that must not happen twice. Each one requires an
Idempotency-Key header, and rejects the request outright if it is missing:
KEY=$(uuidgen) # mint ONCE; reuse this exact value on every retry
curl -X POST https://api.trilocore.ai/api/v1/bevm/transactions \
-H "Authorization: Bearer $TRILOCORE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{"to": "0x…", "selector": "transfer(address,uint256)", "args": ["0x…", "1"]}'
import os
import uuid
import httpx
key = str(uuid.uuid4()) # mint ONCE, outside the retry loop
r = httpx.post(
"https://api.trilocore.ai/api/v1/bevm/transactions",
headers={
"Authorization": f"Bearer {os.environ['TRILOCORE_API_KEY']}",
"Idempotency-Key": key,
},
json={"to": to_address, "selector": "transfer(address,uint256)", "args": args},
)
const key = crypto.randomUUID(); // mint ONCE, outside the retry loop
const r = await fetch("https://api.trilocore.ai/api/v1/bevm/transactions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TRILOCORE_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify({ to, selector: "transfer(address,uint256)", args }),
});
The point of the header is retries. Mint one key per logical operation, then reuse that same key for every retry of it. A network timeout leaves you unable to tell whether the operation happened; retrying with the same key resolves that safely, because the second request replays the first one's result instead of performing the work again.
The failure this prevents is concrete: retrying an attestation without a key mints a second attestation, and retrying a fork transaction without one sends the transaction twice.
The header¶
| Name | Idempotency-Key |
| Value | 1–200 printable ASCII characters (0x21–0x7E), no spaces |
| Applies to | POST |
| Typical value | a UUID v4 |
A UUID is the obvious choice, but any value you can regenerate deterministically for the same logical operation works — and is better if your retry happens in a different process than the original attempt.
Never reuse a key for a different operation
The key is not a nonce you rotate per request; it is the identity of one logical operation. Mint it once, store it with the work item, and reuse it until that item succeeds or you give up on it. Minting a new key inside a retry loop defeats the entire mechanism — that is the single most common way to double-execute against an API like this one.
The endpoints that require it¶
| Method | Path |
|---|---|
POST |
/api/v1/bevm/transactions |
POST |
/api/v1/bevm/snapshots:restore |
POST |
/api/v1/bevm/deployments |
POST |
/api/v1/bevm/stateless/deployments |
POST |
/api/v1/bevm/storage-slots:write |
POST |
/api/v1/bevm/blocks |
POST |
/api/v1/arch/{architecture_id}/snapshots |
POST |
/api/v1/ide/workspaces/{workspace_id}/github/publish |
POST |
/api/v1/ide/workspaces/{workspace_id}/bindings/push |
POST |
/api/v1/workspaces/{workspace_id}/attestations |
POST |
/api/v1/workspaces/{workspace_id}/invitations |
POST |
/api/v1/workspaces/{workspace_id}/reports/{report_id}/share-links |
POST |
/api/v1/projects/{project_id}/invitations |
They have a shape in common: each either mints something durable and externally visible — an
attestation, an invitation, a share link, a published commit — or advances the state of a fork.
Each endpoint's page in the reference repeats the requirement in its
Idempotency-Key row, so you never have to hold this list in your head.
You may send the header on any POST, not just these thirteen, and get the same replay
protection. Doing so is a good default for a client library.
Replay semantics¶
A stored response is replayed when the key and the request match. The match is a fingerprint over the method, the path, and the body, so an accidental key collision with a different request is caught rather than served the wrong answer.
Two response headers tell you which happened:
idempotency-replayed: false ← this request executed
idempotency-replayed: true ← the stored response was replayed; nothing ran
Treat both as success. A replayed 201 is not a duplicate create — it is the same create, told
to you a second time.
Details worth knowing when you build a retry policy:
- Keys are scoped to you. One caller can never replay another caller's response, even with an identical key.
- Responses are kept for 24 hours. After that, the same key is a fresh request and will execute again. Any retry policy should complete well within that window.
- Not every response is storable. A response is only remembered when the service declared its
length and it was at most 1 MiB, and the status was below
500. This has a direct consequence: a retry after a5xxre-executes. Idempotency protects you from ambiguous network outcomes; it does not turn a server error into a completed operation.
Errors¶
Four errors come from the idempotency layer itself. All are raised before your request body is read, so an error here means nothing was executed.
| Status | code |
Meaning | What to do |
|---|---|---|---|
400 |
idempotency_key_required |
the endpoint requires the header and it was absent | add the header; do not retry unchanged |
400 |
invalid_idempotency_key |
the value is empty, over 200 characters, or contains a space or a non-printable character | fix the value's format |
422 |
idempotency_key_reuse |
this key was already used for a different request | you reused a key across two operations — mint a distinct key per operation |
409 |
idempotency_conflict |
an identical request with this key is still in flight | wait and retry; the response carries retry-after: 2 |
A 409 is the normal outcome of two of your own workers racing on the same work item. Honour
retry-after and try again — the second attempt will usually replay the first one's result.
Omitting the header on a required endpoint looks like this:
POST /api/v1/workspaces/{workspace_id}/attestations ← no Idempotency-Key sent
400 Bad Request
content-type: application/problem+json
{
"type": "https://docs.trilocore.ai/errors/idempotency-key-required",
"title": "Idempotency-Key required",
"status": 400,
"detail": "this endpoint requires an Idempotency-Key header",
"code": "idempotency_key_required",
"error": "idempotency_key_required",
"message": "this endpoint requires an Idempotency-Key header",
"instance": "/api/v1/workspaces/{workspace_id}/attestations",
"request_id": "req_…"
}
Three of these four are plain JSON, not problem+json
idempotency_key_required is a full RFC 9457 problem document, as shown above.
invalid_idempotency_key, idempotency_key_reuse and idempotency_conflict are not —
they are returned as application/json with a minimal body:
They carry no code, title, type or status member, and the content type is
application/json. A client that assumes every error is a problem document — reading
body.code and branching on it — will read undefined for these three and fall through to
its "unknown error" path, most likely retrying something it should not.
Until this is made consistent, read the error identifier as body.code ?? body.error. That
one expression is correct for every error the API returns, because problem documents also
carry error as a legacy alias of code. See Errors.