Skip to main content

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.

Start a run, safely retryable
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​

SituationAnswer
First request with the keyThe command runs normally.
Same key, same body, after the first succeededThe stored answer (same status, body and content type) with header Idempotent-Replayed: true. Nothing runs again.
Same key, different body409 idempotency_key_reused
Same key while the first is still running409 command_in_progress. Wait and retry.
Same key, the first never finished within an hour409 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:

CodeWhyWhat to do
idempotency_key_reusedThe key was used with another body.Use a new key for the new command.
command_in_progressThe first attempt is still running.Retry shortly with the same key.
command_outcome_unknownThe first attempt started over an hour ago and never finished.Read the resource, then decide with a new key.
idempotency_response_withheldThe 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_largeThe 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​

retry.mjs
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');
}