Idempotency
A network error or timeout leaves you unsure whether a write happened. Retrying blindly can start a second run or charge
twice. Send an Idempotency-Key header and Lantfly runs the command once: a retry with the same key gets the first
answer back.
Send a key
Every POST, PUT, PATCH and DELETE on /api/v3 accepts the header. It's optional.
curl -X POST https://lantfly.com/api/v3/actors/lantfly~tiktok-scraper/runs \
-H "Authorization: Bearer $LANTFLY_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5d0b7f6e-2a43-4c1e-9b7a-0d7c3e8f6a21" \
-d '{"input": {"hashtags": ["skincare"], "resultsPerPage": 30}}'
Rules for the key:
- 8 to 128 printable ASCII characters. A UUID v4 per command is the simplest choice. Anything else answers
400 idempotency_key_invalid. - Generate the key once per command, before the first attempt, and reuse it for every retry of that command.
- Never reuse a key for a different command.
How a retry is answered
| Situation | Answer |
|---|---|
| First request with the key | The command runs normally. |
| Same key, same body, after the first succeeded | The stored answer (same status, body and content type) with header Idempotent-Replayed: true. Nothing runs again. |
| Same key, different body | 409 idempotency_key_reused |
| Same key while the first is still running | 409 command_in_progress. Wait and retry. |
| Same key, the first never finished within an hour | 409 command_outcome_unknown. Read the resource to see what happened, then use a new key if you still need the command. |
| The first attempt failed (4xx, 5xx) | Nothing is kept. Fix the cause and send again with the same key. |
Keys are remembered for 24 hours.
Scope of a key
A key belongs to the caller (the API token, Console session or run) and to the account the request acts for, plus the route and path. The same key sent by another token, or for another organization, is a different command.
Conflicts
These 409 codes mean a replay couldn't be served:
| Code | Why | What to do |
|---|---|---|
idempotency_key_reused | The key was used with another body. | Use a new key for the new command. |
command_in_progress | The first attempt is still running. | Retry shortly with the same key. |
command_outcome_unknown | The first attempt started over an hour ago and never finished. | Read the resource, then decide with a new key. |
idempotency_response_withheld | The command succeeded, but its answer contained a secret shown only once (a new API token, a webhook signing secret, or any answer marked Cache-Control: no-store), so it wasn't stored. | The command ran. Read the resource, or rotate the secret if you lost it. |
idempotency_response_too_large | The command succeeded, but its answer was over 1 MB, so it wasn't stored. | The command ran. Read the resource. |
Where keys don't apply
- The MCP server: JSON-RPC messages carry their own
id. - Actor Standby: requests are forwarded to the actor as they are.
The Actor SDK already sends one key for all attempts of each write it makes (charges, dataset pushes, key-value writes). Do the same for anything that costs money, such as starting runs or billing calls.
Example with retries
import { randomUUID } from 'node:crypto';
async function startRun(actor, input) {
const key = randomUUID(); // once per command
for (let attempt = 1; attempt <= 4; attempt++) {
try {
const res = await fetch(`https://lantfly.com/api/v3/actors/${actor}/runs`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LANTFLY_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: JSON.stringify({ input }),
});
if (res.status === 409 || res.status >= 500) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
continue;
}
return await res.json();
} catch {
await new Promise((r) => setTimeout(r, 2 ** attempt * 500)); // network error: retry, same key
}
}
throw new Error('Run not started');
}