Docs / Guides
Idempotency keys
How the Idempotency-Key header lets you retry job-creating requests safely, what the 24-hour window covers, and how to handle conflicts.
Last updated April 16, 2026
#Why idempotency keys exist
Networks fail in the least convenient way possible: after the server has done the work but before the client has heard about it. If your code calls POST /v2/jobs and the connection drops before the response arrives, you have no way to know whether the job exists. Retrying blindly may create a second job, and your customer receives two invoices.
An idempotency key removes the ambiguity. You attach a unique key to the request; if Tend has already processed a request with that key, it returns the original result instead of doing the work again. The retry becomes safe by construction.
#How the header works
Send an Idempotency-Key header on any POST request. The value is an arbitrary string up to 255 characters; a random UUID (v4) is a good default. Tend stores the key alongside a fingerprint of the request body and the response it produced. Keys are scoped to the project and environment, so a key used with tnd_dev_ credentials never collides with one used in production.
curl https://api.tendcomputer.com/v2/jobs \
-H "Authorization: Bearer tnd_live_8f2c1a9d4e7b" \
-H "Idempotency-Key: 6b1d2f0a-3c54-4a8e-9f1b-2d7e5c8a4b90" \
-H "Content-Type: application/json" \
-d '{
"target": {"url": "https://api.example-shop.dev/internal/invoices/send"},
"payload": {"invoice_id": "inv_20482"}
}'import uuid
key = str(uuid.uuid4())
job = client.jobs.create(
target={"url": "https://api.example-shop.dev/internal/invoices/send"},
payload={"invoice_id": "inv_20482"},
idempotency_key=key,
)import { randomUUID } from "node:crypto";
const key = randomUUID();
const job = await client.jobs.create(
{
target: { url: "https://api.example-shop.dev/internal/invoices/send" },
payload: { invoice_id: "inv_20482" },
},
{ idempotencyKey: key }
);key := uuid.NewString()
job, err := client.Jobs.Create(ctx, &tend.JobParams{
Target: tend.Target{URL: "https://api.example-shop.dev/internal/invoices/send"},
Payload: map[string]any{"invoice_id": "inv_20482"},
IdempotencyKey: key,
})The official SDKs already retry connection errors and 5xx responses automatically. When you pass idempotency_key, the SDK reuses it across those internal retries; when you do not, the SDK generates a key for you on POST requests so its own retries are safe.
#The 24-hour window
Tend remembers each key for 24 hours from the first request that used it. Inside that window, a repeat request with the same key and the same body returns the stored response, with the same status code and body, and adds the response header Idempotent-Replayed: true. No new job is created, no quota is consumed, and no new run is scheduled.
After 24 hours the key is forgotten. A request that reuses an expired key is treated as brand new and will create another job. If your own retry logic can stretch beyond a day, derive the key from a stable business identifier so you can reason about duplicates yourself, and add a uniqueness check in your own database.
| Situation | Result |
|---|---|
| First request with a new key | Processed normally; response stored for 24 hours |
| Same key, same body, within 24 hours | Stored response returned with Idempotent-Replayed: true |
| Same key, different body, within 24 hours | 409 idempotency_conflict |
| Same key after 24 hours | Treated as a new request |
| Request still in flight when the repeat arrives | Repeat waits for the first to finish, then returns its response |
#Handling idempotency conflicts
If you send a key that was already used with a different request body, Tend refuses to guess which one you meant and returns 409 idempotency_conflict. This nearly always signals a bug: the key was generated once and reused across logically different operations, or the payload was mutated between retries.
{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key was already used with a different request body.",
"idempotency_key": "6b1d2f0a-3c54-4a8e-9f1b-2d7e5c8a4b90",
"original_request_id": "req_01JB3N8XK4TE"
}
}Body comparison is performed on the canonicalized JSON, so differences in key order or whitespace do not cause a conflict. Differences in values do, including changes to payload, retry, run_at, and region.
#Choosing good keys
A key should identify an intended operation, not an attempt. Two strategies work well, depending on how your application is structured.
- Random keys. Generate a UUID when the user action begins and store it with the record. Every retry of that action reuses the stored key. This is the simplest option and the one we recommend by default.
- Derived keys. Build the key from a business identifier, such as
invoice-send:inv_20482. This gives you deduplication even across process restarts, but you must ensure the identifier really is unique per intended job, and remember that it only holds for 24 hours. - Avoid timestamps, counters that reset, and anything that can differ between two attempts of the same operation.
Idempotency keys apply to POST requests. GET, PUT, PATCH, and DELETE are already safe to repeat in the sense that matters for this feature, so the header is ignored on them.
#Idempotency keys and delayed jobs
Idempotency keys pair naturally with delayed one-off jobs. When you schedule a reminder for a customer with run_at, a key derived from the customer and the reminder type ensures that a double-clicked button creates exactly one job. Note that the key protects the creation request, not the eventual execution: if the job itself is delivered more than once because of an ambiguous timeout, your handler must still deduplicate, for example by checking the Tend-Run-Id header.
curl https://api.tendcomputer.com/v2/jobs \
-H "Authorization: Bearer tnd_live_8f2c1a9d4e7b" \
-H "Idempotency-Key: trial-reminder:cus_5521:day-12" \
-H "Content-Type: application/json" \
-d '{
"target": {"url": "https://api.example-shop.dev/internal/reminders"},
"payload": {"customer_id": "cus_5521", "kind": "trial-day-12"},
"run_at": "2026-04-28T15:00:00Z"
}'job = client.jobs.create(
target={"url": "https://api.example-shop.dev/internal/reminders"},
payload={"customer_id": "cus_5521", "kind": "trial-day-12"},
run_at="2026-04-28T15:00:00Z",
idempotency_key="trial-reminder:cus_5521:day-12",
)const job = await client.jobs.create(
{
target: { url: "https://api.example-shop.dev/internal/reminders" },
payload: { customer_id: "cus_5521", kind: "trial-day-12" },
run_at: "2026-04-28T15:00:00Z",
},
{ idempotencyKey: "trial-reminder:cus_5521:day-12" }
);