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

# Idempotência

> Retry seguro nos POSTs dos dois motores — como a Idempotency-Key evita cobrança dupla

A obrigatoriedade do header `Idempotency-Key` varia por motor:

* **Motor de CPF/CNPJ** (`POST /analyses`): o header é **obrigatório**. Sem ele (ou com valor vazio), a API responde `400 bad_request` com a mensagem "Idempotency-Key header required".
* **Motor de Borderô** (`POST /operations`): o header é **opcional**. Presente, funciona como nos analyses (replay devolve `200` com o body original; key reusada com payload diferente devolve `409 idempotency_conflict`). Ausente, **cada POST cria uma operação nova** — e uma cobrança nova.

O motivo para usar a key mesmo onde é opcional é simples: rede falha. Um timeout no seu lado não significa que a requisição não chegou — a análise ou operação pode ter sido criada e os tokens debitados sem que você tenha recebido a resposta. Sem idempotência, repetir a chamada criaria uma segunda cobrança. Com a `Idempotency-Key`, repetir é sempre seguro: a API reconhece a repetição e devolve a resposta original, sem cobrar de novo.

## Como funciona (os dois motores)

A deduplicação vale por uma janela de **24 horas** a partir da primeira requisição.

| Situação                                               | Resposta                                                                                  |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| Primeira requisição com a key                          | `202 Accepted` — criada, tokens debitados                                                 |
| Mesma key + mesmo conteúdo (dentro de 24h)             | `200 OK` com o mesmo body da 202 original — sem nova cobrança                             |
| Mesma key + conteúdo **diferente**                     | `409` `idempotency_conflict`                                                              |
| Mesma key com a primeira requisição ainda em andamento | `409` `idempotency_conflict` — aguarde e consulte via `GET`                               |
| Key ausente ou vazia                                   | **CPF/CNPJ:** `400` `bad_request`. **Borderô:** aceito — cada POST cria uma operação nova |

Repare no status code: `202` significa "criei agora"; `200` significa "isto é um replay — nada novo foi criado nem cobrado". Seu código pode tratar os dois como sucesso, já que o body é o mesmo.

## O que conta como "mesmo conteúdo"

A comparação cobre o payload inteiro do POST, em cada motor:

| Motor                            | O que é comparado                                                                                                                  |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **CPF/CNPJ** (`POST /analyses`)  | O body da requisição: `document`, `subject_type`, `engine_id`, `legal_basis` e demais campos                                       |
| **Borderô** (`POST /operations`) | O payload multipart: o conteúdo dos arquivos (`file` e `xmls`) e os campos (`cedente_cnpj`, `engine_id`, `purpose`, `legal_basis`) |

No borderô, reenviar exatamente o mesmo multipart com a mesma key é um replay (`200`). Trocar o arquivo, o ZIP de NFe ou qualquer campo mantendo a mesma key resulta em `409 idempotency_conflict`.

### Concorrência

Se duas requisições com a mesma key chegarem ao mesmo tempo, a primeira vence. A concorrente recebe `409` enquanto a primeira ainda está em andamento. Não gere uma key nova nesse caso: aguarde um instante e consulte o resultado via `GET` (`/analyses/{id}` ou `/operations/{id}`), usando o `id` da resposta 202 original. Se você ainda não tem o `id` (por exemplo, a primeira chamada deu timeout no seu lado), repita o POST com a **mesma** key até receber `200` (replay) ou `202` (se a primeira chamada nunca chegou ao servidor).

## Receita prática

1. **Derive a key do ID interno do seu job.** Gere um UUID no momento em que o job (ou o borderô) é criado no seu sistema e persista junto com ele. UUID é o formato recomendado. Uma key por requisição pretendida — nunca reutilize a mesma key para documentos, arquivos ou jobs diferentes.
2. **Timeout ou 5xx → repita com a MESMA key.** A resposta será a 202 original (se a primeira chamada não chegou) ou um replay 200 (se chegou). Nos dois casos, sem cobrança dupla.
3. **4xx de validação → corrija e use uma key NOVA.** A key antiga está amarrada ao conteúdo antigo: mesma key + conteúdo corrigido resulta em `409 idempotency_conflict`.

<Warning>
  Nunca gere uma key nova ao repetir uma requisição que falhou por timeout ou 5xx. Uma key nova cria uma cobrança nova — no borderô, uma por entidade única do arquivo.
</Warning>

<Note>
  Os Motores usam a **mesma base** da API 221b (`https://221b-api.sherlocker.com.br/api/v1`), mas autenticam via `Authorization: Bearer slhk_…` — **não** via `?token=`.
</Note>

## Retry na prática

<Tabs>
  <Tab title="CPF/CNPJ">
    <CodeGroup>
      ```python Python theme={null}
      import requests
      import time
      import uuid

      URL = "https://221b-api.sherlocker.com.br/api/v1/analyses"
      API_KEY = "slhk_sua_chave_aqui"

      # 1 key por job interno; na pratica, gere na criacao do job e persista junto a ele
      idempotency_key = str(uuid.uuid4())

      body = {"document": "12345678000195"}

      for attempt in range(5):
          try:
              resp = requests.post(
                  URL,
                  headers={
                      "Authorization": f"Bearer {API_KEY}",
                      "Idempotency-Key": idempotency_key,
                  },
                  json=body,
                  timeout=10,
              )
          except requests.Timeout:
              time.sleep(2 ** attempt)
              continue  # retry com a MESMA key

          if resp.status_code in (202, 200):
              run = resp.json()  # 202 = criada agora; 200 = replay, sem nova cobranca
              break

          if resp.status_code >= 500:
              time.sleep(2 ** attempt)
              continue  # retry com a MESMA key

          if resp.status_code == 409:
              # concorrente em andamento: aguarde e consulte via GET /analyses/{id}
              break

          resp.raise_for_status()  # 4xx: corrija o body e use uma key NOVA
      ```

      ```javascript Node.js theme={null}
      const URL = "https://221b-api.sherlocker.com.br/api/v1/analyses";
      const API_KEY = "slhk_sua_chave_aqui";

      // 1 key por job interno; na pratica, gere na criacao do job e persista junto a ele
      const idempotencyKey = crypto.randomUUID();

      const body = { document: "12345678000195" };

      let run;
      for (let attempt = 0; attempt < 5; attempt++) {
        let resp;
        try {
          resp = await fetch(URL, {
            method: "POST",
            headers: {
              Authorization: `Bearer ${API_KEY}`,
              "Idempotency-Key": idempotencyKey,
              "Content-Type": "application/json",
            },
            body: JSON.stringify(body),
            signal: AbortSignal.timeout(10_000),
          });
        } catch {
          await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
          continue; // retry com a MESMA key
        }

        if (resp.status === 202 || resp.status === 200) {
          run = await resp.json(); // 202 = criada agora; 200 = replay, sem nova cobranca
          break;
        }
        if (resp.status >= 500) {
          await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
          continue; // retry com a MESMA key
        }
        if (resp.status === 409) {
          // concorrente em andamento: aguarde e consulte via GET /analyses/{id}
          break;
        }
        throw new Error(`HTTP ${resp.status}`); // 4xx: corrija o body e use uma key NOVA
      }
      ```
    </CodeGroup>

    **Os três cenários:**

    1. **Primeira submissão → 202.** Análise criada, tokens debitados.
    2. **Replay (mesma key, mesmo body) → 200.** O mesmo body JSON da 202, byte a byte — mesmo `id`, mesmos `tokens_charged` e `balance`, mesmo `request_id`. Nada foi criado nem cobrado de novo.
    3. **Mesma key, body diferente → 409.** Por exemplo, a mesma key com um `purpose` adicionado:

    ```bash cURL theme={null}
    curl -i -X POST "https://221b-api.sherlocker.com.br/api/v1/analyses" \
      -H "Authorization: Bearer slhk_sua_chave_aqui" \
      -H "Idempotency-Key: 9f3c2d1e-7a54-4b0c-8e2f-1d6a5b4c3e2a" \
      -H "Content-Type: application/json" \
      -d '{"document": "12345678000195", "purpose": "onboarding de cedente"}'
    ```
  </Tab>

  <Tab title="Borderô">
    No borderô, o header é **opcional** — mas em qualquer fluxo com retry automático, envie-o sempre:

    <CodeGroup>
      ```python Python theme={null}
      import requests
      import time
      import uuid

      URL = "https://221b-api.sherlocker.com.br/api/v1/operations"
      API_KEY = "slhk_sua_chave_aqui"

      # 1 key por bordero; na pratica, gere quando a remessa entra no seu sistema
      idempotency_key = str(uuid.uuid4())

      for attempt in range(5):
          try:
              with open("bordero.rem", "rb") as cnab:
                  resp = requests.post(
                      URL,
                      headers={
                          "Authorization": f"Bearer {API_KEY}",
                          "Idempotency-Key": idempotency_key,
                      },
                      files={"file": cnab},
                      data={"cedente_cnpj": "12345678000195"},
                      timeout=30,
                  )
          except requests.Timeout:
              time.sleep(2 ** attempt)
              continue  # retry com a MESMA key

          if resp.status_code in (202, 200):
              op = resp.json()  # 202 = criada agora; 200 = replay, sem nova cobranca
              break

          if resp.status_code >= 500:
              time.sleep(2 ** attempt)
              continue  # retry com a MESMA key

          if resp.status_code == 409:
              # concorrente em andamento: aguarde e consulte via GET /operations/{id}
              break

          resp.raise_for_status()  # 4xx: corrija e use uma key NOVA
      ```

      ```javascript Node.js theme={null}
      import { openAsBlob } from "node:fs";

      const URL = "https://221b-api.sherlocker.com.br/api/v1/operations";
      const API_KEY = "slhk_sua_chave_aqui";

      // 1 key por bordero; na pratica, gere quando a remessa entra no seu sistema
      const idempotencyKey = crypto.randomUUID();

      let op;
      for (let attempt = 0; attempt < 5; attempt++) {
        const form = new FormData();
        form.append("file", await openAsBlob("bordero.rem"), "bordero.rem");
        form.append("cedente_cnpj", "12345678000195");

        let resp;
        try {
          resp = await fetch(URL, {
            method: "POST",
            headers: {
              Authorization: `Bearer ${API_KEY}`,
              "Idempotency-Key": idempotencyKey,
            },
            body: form,
            signal: AbortSignal.timeout(30_000),
          });
        } catch {
          await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
          continue; // retry com a MESMA key
        }

        if (resp.status === 202 || resp.status === 200) {
          op = await resp.json(); // 202 = criada agora; 200 = replay, sem nova cobranca
          break;
        }
        if (resp.status >= 500) {
          await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
          continue; // retry com a MESMA key
        }
        if (resp.status === 409) {
          // concorrente em andamento: aguarde e consulte via GET /operations/{id}
          break;
        }
        throw new Error(`HTTP ${resp.status}`); // 4xx: corrija e use uma key NOVA
      }
      ```
    </CodeGroup>

    **Os três cenários:**

    1. **Primeira submissão → 202.** Operação criada, tokens debitados (um por entidade única do borderô). Body: `{operation_id, status: "queued", created_at}`.
    2. **Replay (mesma key, mesmo payload) → 200.** O mesmo body da 202 original — mesmo `operation_id`. Nada foi criado nem cobrado de novo.
    3. **Mesma key, payload diferente → 409.** Outro arquivo, outro ZIP de NFe, outro `cedente_cnpj`, outro `engine_id` ou outro `legal_basis` com a mesma key:

    ```bash cURL theme={null}
    curl -i -X POST "https://221b-api.sherlocker.com.br/api/v1/operations" \
      -H "Authorization: Bearer slhk_sua_chave_aqui" \
      -H "Idempotency-Key: 9f3c2d1e-7a54-4b0c-8e2f-1d6a5b4c3e2a" \
      -F "file=@outro-bordero.rem" \
      -F "cedente_cnpj=12345678000195"
    ```

    <Note>
      Sem o header `Idempotency-Key`, o borderô **não deduplica**: cada POST cria uma operação nova, com cobrança nova. Use a key sempre que houver qualquer chance de retry.
    </Note>
  </Tab>
</Tabs>

**Resposta (409, `application/problem+json`) — igual nos dois motores:**

```json theme={null}
{
  "type": "https://docs.sherlocker.com.br/problems/idempotency_conflict",
  "title": "Conflict",
  "status": 409,
  "code": "idempotency_conflict",
  "detail": "A request with the same idempotency key is already in progress or completed.",
  "instance": "/v1?request_id=req-uuid"
}
```

O mesmo `code` (`idempotency_conflict`) é retornado tanto no conflito de conteúdo quanto na concorrência. Faça branching pelo `code`, não pelo texto de `detail`, que pode variar. Detalhes em [Erros](/motor-analise/erros) e [idempotency\_conflict](/problems/idempotency_conflict).

## Próximos passos

<CardGroup cols={3}>
  <Card title="Ciclo de vida" icon="arrows-rotate" href="/motores/ciclo-de-vida">
    Status e como fazer polling em cada motor
  </Card>

  <Card title="Cobrança" icon="coins" href="/motores/cobranca">
    Quando os tokens são debitados e quando há estorno
  </Card>

  <Card title="Erros" icon="triangle-exclamation" href="/motor-analise/erros">
    Formato RFC 7807 e todos os códigos de erro
  </Card>
</CardGroup>
