> ## Documentation Index
> Fetch the complete documentation index at: https://docs.meetdarwin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Retry writes safely with Idempotency-Key

> Send an Idempotency-Key header on Darwin API writes so a retried request after a timeout never creates a second contact, document or webhook endpoint.

A network can fail after Darwin has done the work but before you get the answer. Send an `Idempotency-Key` header on every write (`POST`, `PATCH`, `DELETE`) and you can retry without doing it twice.

```bash theme={null}
curl https://api.meetdarwin.ai/v1/knowledge/documents \
  -H "Authorization: Bearer $DARWIN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6c1d0b2e-8f4a-4f3e-9d61-0b7a2e5c9f10" \
  -d '{"name": "Refund policy", "content": "Refunds are available within 30 days of purchase for any annual plan. Contact billing to start one."}'
```

## How it works

* The first request with a key runs normally. Darwin stores its response for **24 hours**.
* A retry with the same key, method, path and body gets the stored response back, with the same status and body, plus the header `Idempotent-Replayed: true`. Nothing runs a second time.
* A refused request (a `4xx`) is a final answer and is replayed like a success. Fix the request and send it with a **new** key.
* A Darwin-side failure (a `5xx`) is not stored, so a retry with the same key runs the request again.
* While the first request is still running, a retry with the same key gets `409` [`idempotency_key_in_use`](/api-reference/errors#idempotency_key_in_use). Wait a few seconds and retry.
* The same key with a different method, path or body gets `422` [`idempotency_key_reused`](/api-reference/errors#idempotency_key_reused).

Keys are scoped to the API key that sent them, and must be 1 to 255 visible ASCII characters. A UUID v4 per logical operation is a good choice.

## Retry pattern

<CodeGroup>
  ```javascript Node theme={null}
  import { randomUUID } from "node:crypto";

  async function createContact(contact) {
    const key = randomUUID(); // one key for this contact, reused on every retry
    for (let attempt = 0; attempt < 5; attempt++) {
      try {
        const res = await fetch("https://api.meetdarwin.ai/v1/contacts", {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.DARWIN_API_KEY}`,
            "Content-Type": "application/json",
            "Idempotency-Key": key,
          },
          body: JSON.stringify(contact),
        });
        if (res.status === 429 || res.status === 409 || res.status >= 500) {
          const wait = Number(res.headers.get("retry-after")) || 2 ** attempt;
          await new Promise((r) => setTimeout(r, wait * 1000));
          continue;
        }
        return await res.json();
      } catch {
        await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); // network error: retry with the same key
      }
    }
    throw new Error("Darwin did not answer after 5 attempts");
  }
  ```

  ```python Python theme={null}
  import os, time, uuid, requests

  def create_contact(contact):
      key = str(uuid.uuid4())  # one key for this contact, reused on every retry
      for attempt in range(5):
          try:
              res = requests.post(
                  "https://api.meetdarwin.ai/v1/contacts",
                  json=contact,
                  headers={
                      "Authorization": f"Bearer {os.environ['DARWIN_API_KEY']}",
                      "Idempotency-Key": key,
                  },
                  timeout=30,
              )
          except requests.RequestException:
              time.sleep(2 ** attempt)
              continue
          if res.status_code in (409, 429) or res.status_code >= 500:
              time.sleep(int(res.headers.get("Retry-After", 2 ** attempt)))
              continue
          return res.json()
      raise RuntimeError("Darwin did not answer after 5 attempts")
  ```
</CodeGroup>

A `409` with the code `conflict` is not a retry case: read the object again and resolve it. The loop above retries every `409` for brevity; check `code` in production code.

## Writes that are safe without a key

Some writes give the same result however often you send them: `PATCH` with the same fields, or `DELETE` of something already gone (it returns `404`). `POST /contacts` is an upsert by email, so a repeat updates the same contact. A key still protects you from a retry racing the original, so sending one on every write is the simplest rule.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.