---
title: "Idempotency Keys and Safe Retries | Leadsets Docs"
description: "Send an Idempotency-Key so a retried Leadsets call never counts records twice. Keys last 24 hours; a different body answers 409 idempotency_conflict."
canonical: https://www.leadsets.ai/docs/idempotency
updated: 2026-11-11
---

Guides / Idempotency

# Idempotency.

Send an `Idempotency-Key` header on REST calls that use records or scrape units. A retry with the same key returns the first result and never counts anything twice.

## Send a key

Make one key per logical operation, for example a UUID, and reuse it for every retry of that operation. The SDKs already send a key on every POST and reuse it across their own retries; pass your own to make a call safe to repeat across processes.

```bash
curl https://api.leadsets.ai/v1/people/enrich \
 -H "Authorization: Bearer $LEADSETS_KEY" \
 -H "Content-Type: application/json" \
 -H "Idempotency-Key: 5d0c9a52-8a3e-4f0b-9b1e-2f6c1d7e8a90" \
 -d '{"linkedin_url": "https://www.linkedin.com/in/mayachen"}'
```

```ts
import { randomUUID } from "node:crypto";
import { Leadsets } from "@leadsetsai/sdk";

const ls = new Leadsets(process.env.LEADSETS_KEY);
const res = await ls.people.enrich(
 { linkedin_url: "https://www.linkedin.com/in/mayachen" },
 { idempotencyKey: randomUUID() },
);
```

```python
import uuid
import os
from leadsets import Leadsets

ls = Leadsets(os.environ["LEADSETS_KEY"])
res = ls.people.enrich(
   linkedin_url="https://www.linkedin.com/in/mayachen",
   idempotency_key=str(uuid.uuid4()),
)
```

## Rules

- A key is any string up to 255 characters. Longer keys answer 400 `invalid_request`.
- Keys are scoped to your workspace and the endpoint: the same key on two endpoints is two separate keys.
- A successful result is kept for 24 hours. A retry in that time gets the same body back with the header `Idempotent-Replayed: true`, and nothing is counted: no records, no request, no scrape units.
- Errors are not kept. After a 429, a 5xx or any other error, a retry with the same key runs the call again.
- The same key with a different body answers 409 `idempotency_conflict`.
- While the first call with a key is still running, another call with that key also answers 409 `idempotency_conflict` ("still running"). The hold lasts up to 120 seconds; wait a moment and retry with the same key.
- `GET /v1/usage` ignores the header, since it costs nothing.
- MCP tool calls don't take a key. Agents should retry by the error `type`, as [Errors and retries](https://www.leadsets.ai/docs/errors) describes.

## A retry loop

Without an SDK, keep the key outside the loop and honour `Retry-After`:

```bash
# curl --retry resends the same headers, so the key stays the same,
# and waits for Retry-After on 429 and 503.
curl https://api.leadsets.ai/v1/people/enrich --retry 3 \
 -H "Authorization: Bearer $LEADSETS_KEY" \
 -H "Content-Type: application/json" \
 -H "Idempotency-Key: $(uuidgen)" \
 -d '{"linkedin_url": "https://www.linkedin.com/in/mayachen"}'
```

```ts
import { randomUUID } from "node:crypto";

async function enrich(body: object) {
 const key = randomUUID();
 for (let attempt = 0; attempt < 5; attempt++) {
   const res = await fetch("https://api.leadsets.ai/v1/people/enrich", {
     method: "POST",
     headers: {
       Authorization: `Bearer ${process.env.LEADSETS_KEY}`,
       "Content-Type": "application/json",
       "Idempotency-Key": key,
     },
     body: JSON.stringify(body),
   });
   if (res.status !== 429 && res.status < 500) return res.json();
   const wait = Number(res.headers.get("retry-after") ?? 2 ** attempt);
   await new Promise((r) => setTimeout(r, wait * 1000));
 }
}
```

```python
import os, time, uuid
import requests

def enrich(body):
   key = str(uuid.uuid4())
   for attempt in range(5):
       res = requests.post(
           "https://api.leadsets.ai/v1/people/enrich",
           headers={
               "Authorization": "Bearer " + os.environ["LEADSETS_KEY"],
               "Idempotency-Key": key,
           },
           json=body,
       )
       if res.status_code != 429 and res.status_code < 500:
           return res.json()
       time.sleep(float(res.headers.get("Retry-After", 2 ** attempt)))
```

**A full window can mean a long wait.** When `window_full` says the window resets in hours, `Retry-After` says so too. Decide whether to wait or stop the run; the error body has `resets_at`.

[Previous
**← Pagination**](https://www.leadsets.ai/docs/pagination) [Next
**Webhooks →**](https://www.leadsets.ai/docs/webhooks)

Was this page useful? [Tell us](mailto:support@leadsets.ai?subject=Docs%20feedback%3A%20%2Fdocs%2Fidempotency)

---

Leadsets by Mobylogix LLC, 10160 hwy 242, Conroe, TX, 77385, US. Facts for AI assistants: https://www.leadsets.ai/llm · Site index: https://www.leadsets.ai/llms.txt · Every page: https://www.leadsets.ai/llms-full.txt
