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

# Idempotency

> Use `Idempotency-Key` to make `POST /v1/analyze` and `POST /v1/batch` safe to retry.

Network failures happen. A request may succeed on the server but the response may never reach you. If you blindly retry, you'll create a duplicate report and pay twice.

AcreLens supports the standard `Idempotency-Key` header to prevent that.

## How it works

1. Generate a unique key for each *logical* operation — UUIDv4 is recommended.
2. Send it on `POST /v1/analyze` or `POST /v1/batch`:

```bash theme={null}
curl -X POST https://api.acrelens.com/v1/analyze \
  -H "Authorization: Bearer al_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f8a9e2b-4c3d-4e5f-6a7b-8c9d0e1f2a3b" \
  -d '{"address":"123 Cabin Rd","state":"NM","mode":"off_grid"}'
```

3. AcreLens hashes `<key>:<your-customer-id>` with SHA-256 and uses that as the cache lookup. The original response gets stored alongside a hash of your request body.
4. On retry, AcreLens compares your new request body (canonical JSON, keys sorted) to the stored hash:
   * **Same key + same body** → returns the original `202` response. No new report created. No charge.
   * **Same key + different body** → returns `409 idempotency_conflict`. Use a fresh key for the new request.
   * **No prior entry** → proceeds as a new request and stores the response.

## Lifetime

| | Value |
| - | - |
| Cache TTL | **24 hours** from first use |
| After expiry | Same key is treated as a brand-new request |
| Storage scope | Per customer (your keys never collide with other customers') |

After 24 hours, the entry is purged. Reusing the same key after that point will create a new report — so don't rely on idempotency keys for permanent deduplication.

## When to use

* **Retries after network errors** — connection timeouts, partial responses, 5xx
* **Background jobs** — Inngest step retries, Celery, BullMQ, anything queue-driven
* **User-facing forms** — generate the key on form render, attach it to the submit, prevent double-submit
* **Webhook handlers** — if your webhook handler kicks off a `/v1/analyze` request, use a deterministic key derived from the webhook event ID

## When not to use

* **Distinct logical operations** — analyzing two different properties needs two different keys
* **After the 24-hour window** — generate a fresh key for retries beyond that
* **Across different API keys** — idempotency is scoped per customer, not per `Authorization` header

## Conflict response

If you reuse a key with a different body — common bug: changing the address slightly and forgetting to regenerate the key — you'll get:

```json theme={null}
{
  "error": {
    "code": "idempotency_conflict",
    "message": "Idempotency-Key reused with a different request body.",
    "request_id": "req_01HY2Z...",
    "retriable": false,
    "docs_url": "https://docs.acrelens.com/errors/idempotency_conflict"
  }
}
```

Generate a new key for the new request and retry.

## Safe retry pattern (Node)

```javascript theme={null}
async function createReportWithRetry(body, maxRetries = 3) {
  const idempotencyKey = crypto.randomUUID();

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const res = await fetch("https://api.acrelens.com/v1/analyze", {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${process.env.ACRELENS_API_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey,
        },
        body: JSON.stringify(body),
      });

      if (res.status >= 500) {
        // Safe to retry — same key, same body
        await sleep(2 ** attempt * 1000);
        continue;
      }

      return res.json();
    } catch (err) {
      // Network error — also safe to retry with the same key
      if (attempt === maxRetries) throw err;
      await sleep(2 ** attempt * 1000);
    }
  }
}

const sleep = (ms) => new Promise(r => setTimeout(r, ms));
```

The key insight: **the same idempotency key is reused across all retry attempts**. Generating a fresh key on each retry would defeat the protection.

## Body canonicalization

Idempotency comparisons are body-aware, but field order doesn't matter. Both of these match:

```json theme={null}
{ "address": "123 Cabin Rd", "state": "NM", "mode": "off_grid" }
{ "mode": "off_grid", "state": "NM", "address": "123 Cabin Rd" }
```

But these don't:

```json theme={null}
{ "address": "123 Cabin Rd", "state": "NM", "mode": "off_grid" }
{ "address": "123 Cabin Rd ", "state": "NM", "mode": "off_grid" }
```

(Note the trailing space in the second `address`.) Whitespace, case, and field values matter. Field order does not.


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