> ## 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.

# Quickstart

> Analise seu primeiro borderô CNAB 400 com a API do Motor de Borderô

Neste guia você envia um borderô CNAB 400, acompanha o processamento até a conclusão e busca o resultado hierárquico da análise — cedente, sacados, títulos e as violações de regra (issues) de cada nó. É a mesma esteira de análise da interface web do Sherlocker, agora acessível por API.

<Note>
  Não confunda com o **Motor de Análise** ([documentado aqui](/motor-analise/introducao)), que avalia o risco de um CNPJ ou CPF individual. O Motor de Borderô analisa o borderô inteiro — e usa o Motor de Análise por baixo para cada entidade.
</Note>

## Pré-requisitos

* **Acesso ao beta**: a feature `analysis_engine` precisa estar habilitada no seu workspace. Solicite em [Solicitar acesso](/api/access).
* **Chave de API**: criada no app Sherlocker em **Settings → API Keys**. As chaves têm prefixo `slhk_` e são exibidas uma única vez — guarde em local seguro.
* **Saldo de tokens**: a operação consome tokens do mesmo pool do workspace usado no app — um débito por entidade única do borderô (veja [Cobrança](/motores/cobranca)).
* **Arquivo CNAB 400 remessa** (`.rem`) — Banco Paulista 611, 444 colunas, Windows-1252 — com no máximo 16 MB.
* **CNPJ do cedente** da operação (14 dígitos, com ou sem formatação).

A base URL de todos os exemplos é `https://221b-api.sherlocker.com.br/api/v1`.

<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>

<Steps>
  <Step title="Crie a operação">
    Envie um `POST /operations` como **`multipart/form-data`** — o arquivo vai direto no form, sem base64:

    | Parte          | Tipo    | Obrigatória | Descrição                                                              |
    | -------------- | ------- | ----------- | ---------------------------------------------------------------------- |
    | `file`         | arquivo | **Sim**     | CNAB 400 remessa (`.rem`). Máx. 16 MB                                  |
    | `xmls`         | arquivo | Não         | ZIP com os XMLs de NFe (`nfeProc`) dos títulos. Máx. 32 MB             |
    | `cedente_cnpj` | texto   | **Sim**     | CNPJ do cedente, 14 dígitos ou formatado (dígito verificador validado) |
    | `engine_id`    | texto   | Não         | ID da engine; omitido ou `template-padrao` → template do sistema       |
    | `purpose`      | texto   | Não         | Finalidade da análise (auditoria LGPD)                                 |
    | `legal_basis`  | texto   | Não         | Base legal LGPD para os sacados PF                                     |

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST "https://221b-api.sherlocker.com.br/api/v1/operations" \
        -H "Authorization: Bearer slhk_sua_chave_aqui" \
        -F "file=@bordero.rem" \
        -F "xmls=@nfes.zip" \
        -F "cedente_cnpj=12345678000195" \
        -F "engine_id=template-padrao"
      ```

      ```python Python theme={null}
      import requests

      with open("bordero.rem", "rb") as cnab, open("nfes.zip", "rb") as xmls:
          resp = requests.post(
              "https://221b-api.sherlocker.com.br/api/v1/operations",
              headers={"Authorization": "Bearer slhk_sua_chave_aqui"},
              files={"file": cnab, "xmls": xmls},
              data={"cedente_cnpj": "12345678000195", "engine_id": "template-padrao"},
          )
      print(resp.status_code)  # 202
      operation = resp.json()
      operation_id = operation["operation_id"]
      ```

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

      const form = new FormData();
      form.append("file", await openAsBlob("bordero.rem"), "bordero.rem");
      form.append("xmls", await openAsBlob("nfes.zip"), "nfes.zip");
      form.append("cedente_cnpj", "12345678000195");
      form.append("engine_id", "template-padrao");

      const resp = await fetch("https://221b-api.sherlocker.com.br/api/v1/operations", {
        method: "POST",
        headers: { Authorization: "Bearer slhk_sua_chave_aqui" },
        body: form,
      });
      console.log(resp.status); // 202
      const operation = await resp.json();
      const operation_id = operation.operation_id;
      ```
    </CodeGroup>

    **Resposta (202 Accepted):**

    ```json theme={null}
    {
      "operation_id": "b2c3d4e5-0000-0000-0000-000000000001",
      "status": "queued",
      "created_at": "2026-07-09T12:00:00.000Z"
    }
    ```

    Guarde o `operation_id`: é com ele que você consulta o status e o resultado. A cobrança (uma por entidade única do borderô — cedente + sacados únicos) acontece normalmente na criação, mas **não aparece no payload**: acompanhe débitos e estornos no extrato de tokens do workspace, no app Sherlocker.

    <Tip>
      O header `Idempotency-Key` é **opcional**. Se você enviá-lo, reenviar o mesmo payload com a mesma key devolve `200` com o body original (sem nova operação nem nova cobrança); a mesma key com payload diferente devolve `409 idempotency_conflict`. Sem o header, cada POST cria uma operação nova. Veja [Idempotência](/motores/idempotencia).
    </Tip>
  </Step>

  <Step title="Acompanhe até a conclusão">
    Consulte `GET /operations/{id}` até o status ficar terminal (`completed` ou `failed`). O vocabulário de status é `queued` → `processing` → `completed` | `failed`. Não há header `Retry-After` nem campo de intervalo no body — consulte a cada **\~2 segundos** (consultar mais rápido não acelera o resultado, e o limite global é de 100 requisições por minuto por IP):

    <CodeGroup>
      ```bash cURL theme={null}
      OPERATION_ID="b2c3d4e5-0000-0000-0000-000000000001"

      while true; do
        RESP=$(curl -s "https://221b-api.sherlocker.com.br/api/v1/operations/$OPERATION_ID" \
          -H "Authorization: Bearer slhk_sua_chave_aqui")
        STATUS=$(echo "$RESP" | jq -r '.status')
        echo "status: $STATUS"

        if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
          echo "$RESP" | jq .
          break
        fi

        sleep 2
      done
      ```

      ```python Python theme={null}
      import time

      while True:
          resp = requests.get(
              f"https://221b-api.sherlocker.com.br/api/v1/operations/{operation_id}",
              headers={"Authorization": "Bearer slhk_sua_chave_aqui"},
          )
          op = resp.json()
          print(f"status: {op['status']}")

          if op["status"] in ("completed", "failed"):
              break

          time.sleep(2)
      ```

      ```javascript Node.js theme={null}
      let op;
      while (true) {
        const resp = await fetch(`https://221b-api.sherlocker.com.br/api/v1/operations/${operation_id}`, {
          headers: { Authorization: "Bearer slhk_sua_chave_aqui" },
        });
        op = await resp.json();
        console.log(`status: ${op.status}`);

        if (op.status === "completed" || op.status === "failed") break;

        await new Promise((r) => setTimeout(r, 2000));
      }
      ```
    </CodeGroup>

    **Resposta (200) — o envelope de status traz sempre todos os campos:**

    ```json theme={null}
    {
      "id": "b2c3d4e5-0000-0000-0000-000000000001",
      "status": "completed",
      "file_name": "bordero.rem",
      "file_size": 4440,
      "engine_id": "0b3c6c7e-9d2a-4f1b-8e5d-2a1b3c4d5e6f",
      "titulo_count": 12,
      "approved_count": 9,
      "blocked_count": 2,
      "alert_count": 1,
      "created_at": "2026-07-09T12:00:00.000Z",
      "queued_at": "2026-07-09T12:00:00.000Z",
      "processing_at": "2026-07-09T12:00:02.000Z",
      "completed_at": "2026-07-09T12:02:15.000Z",
      "error": null
    }
    ```

    * `titulo_count`, `approved_count`, `blocked_count` e `alert_count` são `null` até `completed`. A soma `approved_count + blocked_count + alert_count` sempre fecha com `titulo_count` — `alert_count` inclui os títulos com análise incompleta (fonte de dados indisponível).
    * `completed_at` só aparece preenchido em `completed`; `error` (string) só em `failed`.
    * Operações que terminam em `failed` são estornadas automaticamente. Por ora a API é polling-only (sem webhooks).
  </Step>

  <Step title="Busque o resultado hierárquico">
    Com a operação `completed`, chame o `GET /operations/{id}/result`. Por padrão (`?filter=issues`) a resposta poda os títulos aprovados e os sacados sem problemas — você recebe **só o que precisa de atenção**:

    ```bash cURL theme={null}
    curl "https://221b-api.sherlocker.com.br/api/v1/operations/$OPERATION_ID/result" \
      -H "Authorization: Bearer slhk_sua_chave_aqui"
    ```

    **Resposta (200):**

    ```json theme={null}
    {
      "operation_id": "b2c3d4e5-0000-0000-0000-000000000001",
      "cedente": {
        "id": "c0a80001-0000-0000-0000-000000000001",
        "cnpj": "12345678000195",
        "razao_social": "CEDENTE LTDA",
        "status": "blocked",
        "issues": [
          {
            "rule_id": "CEDENTE_PROTESTOS",
            "rule_name": "Protestos",
            "category": "blocking",
            "status": "blocked",
            "message": "Cedente: Protestos encontrados",
            "detail": { "field": "protestos", "actual": "3", "expected": "0" },
            "data_sources": [
              { "name": "cenprot", "fetched_at": "2026-07-06T09:00:00.000Z", "age_days": 3 }
            ]
          }
        ],
        "sacados": [
          {
            "id": "c0a80001-0000-0000-0000-000000000002",
            "cnpj_cpf": "98765432000188",
            "razao_social": "SACADO SA",
            "tipo": "PJ",
            "status": "alerted",
            "issues": [],
            "titulos": [
              {
                "id": "c0a80001-0000-0000-0000-000000000003",
                "numero": "DOC-001",
                "valor": 1234.56,
                "data_vencimento": "2026-08-01",
                "nfe_chave": "35230612345678000195550010000000011000000011",
                "status": "alerted",
                "issues": [
                  {
                    "rule_id": "TITULO_NFE_DIVERGENCIA",
                    "rule_name": "Divergência com NFe",
                    "category": "alert",
                    "status": "alerted",
                    "message": "Título: valor diverge da NFe",
                    "detail": { "field": "valor", "actual": "1234.56", "expected": "1230.00" },
                    "data_sources": [
                      { "name": "nfe", "fetched_at": "2026-07-09T12:01:00.000Z", "age_days": 0 }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      },
      "summary": {
        "total_titulos": 12,
        "approved": 9,
        "blocked": 2,
        "alerted": 1,
        "total_value": 100000,
        "approved_value": 80000,
        "blocked_value": 15000,
        "alerted_value": 5000,
        "sacados": { "total": 5, "with_issues": 2 },
        "cedente": { "status": "blocked" }
      },
      "engine_id": "0b3c6c7e-9d2a-4f1b-8e5d-2a1b3c4d5e6f",
      "engine_name": "Motor Borderô",
      "timestamp": "2026-07-09T12:02:15.000Z",
      "coverage": [
        { "provider": "rfb", "requested": 6, "succeeded": 5, "failed_keys": ["98765432000188"] }
      ],
      "degraded_rules": [
        { "rule_id": "SACADO_SERASA", "provider": "serasa", "titulos_affected": 2 }
      ]
    }
    ```

    Variações do endpoint:

    * **`?filter=all`** — devolve a árvore completa, incluindo títulos aprovados e sacados sem problemas. Qualquer outro valor retorna `400` com a mensagem `Invalid filter value. Use "issues" or "all".`.
    * **`?include=execution_plan`** — lista separada por vírgulas de blocos extras; adiciona o campo `execution_plan` à resposta (hoje sempre `[]`).
    * Chamar o `/result` antes de a operação chegar a `completed` retorna [`409 conflict`](/problems/conflict) com detail no formato `Operation has not completed yet. Current status: processing`.

    <Note>
      O `summary` **sempre** reflete o dataset completo — ele não muda com `?filter`.
    </Note>
  </Step>

  <Step title="Interprete o resultado">
    O resultado é uma árvore: **cedente → sacados → títulos**, cada nó com um `status` e uma lista de `issues` (violações de regra).

    | Conceito                | Como ler                                                                                                                                                                                                               |
    | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `status` de qualquer nó | `approved` \| `blocked` \| `alerted`                                                                                                                                                                                   |
    | Rollup (worst-of)       | O status do título deriva das issues do título; o do sacado é o pior entre as issues próprias e os títulos; o do cedente é o pior entre as issues próprias e os sacados                                                |
    | `issues[]`              | Cada issue traz `rule_id`, `rule_name`, `category` (`blocking` \| `alert`), `status` (`blocked` \| `alerted`), `message`, `detail` (`{field, actual, expected}`) e `data_sources` (`{name, fetched_at, age_days}`)     |
    | `summary`               | Contagens e valores do dataset **completo**: `total_titulos`, `approved`/`blocked`/`alerted`, `total_value`/`approved_value`/`blocked_value`/`alerted_value`, `sacados.total`/`sacados.with_issues` e `cedente.status` |
    | `coverage[]`            | Por provider consultado: `requested`, `succeeded` e `failed_keys` (documentos que falharam)                                                                                                                            |
    | `degraded_rules[]`      | Regras que não puderam ser avaliadas por fonte indisponível: `rule_id`, `provider`, `titulos_affected`                                                                                                                 |
    | `titulo.nfe_chave`      | Chave da NFe vinculada ao título (`null` se não houver); problemas de NFe aparecem como issues do título                                                                                                               |

    <Warning>
      Fonte de dados indisponível **não vira issue** — vira `degraded_rules` (e aparece em `coverage`). No `summary`, os títulos afetados contam como `alerted`. Se a sua esteira exige cobertura total, trate `degraded_rules` não-vazio na sua regra de aprovação.
    </Warning>

    Um filtro típico — listar as issues bloqueantes da operação:

    ```python Python theme={null}
    result = requests.get(
        f"https://221b-api.sherlocker.com.br/api/v1/operations/{operation_id}/result",
        headers={"Authorization": "Bearer slhk_sua_chave_aqui"},
    ).json()

    for sacado in result["cedente"]["sacados"]:
        for titulo in sacado["titulos"]:
            for issue in titulo["issues"]:
                if issue["category"] == "blocking":
                    print(f"{titulo['numero']}  sacado={sacado['cnpj_cpf']}  "
                          f"[{issue['rule_id']}] {issue['message']}")
    ```
  </Step>
</Steps>

## Tratamento de erros

Todos os erros seguem RFC 7807 (`Content-Type: application/problem+json`). Use o campo `code` para decidir o que fazer — o campo `type` é apenas um identificador. Os erros de validação do multipart chegam field-level em `errors[]`, com os campos `file`, `xmls` e `cedente_cnpj`:

**Resposta (400):**

```json theme={null}
{
  "type": "https://docs.sherlocker.com.br/problems/validation_error",
  "title": "Validation Error",
  "status": 400,
  "code": "validation_error",
  "detail": "One or more fields failed validation.",
  "errors": [
    { "field": "file", "message": "No file provided" },
    { "field": "cedente_cnpj", "message": "cedente_cnpj is required" }
  ],
  "instance": "/v1?request_id=req-uuid-here"
}
```

Outras mensagens comuns: `Invalid CNPJ format` (dígito verificador do `cedente_cnpj`), falha de parse do CNAB e arquivos acima do teto (16 MB para `file`, 32 MB para `xmls`). Como nada foi parseado, **nada é cobrado**. Os demais códigos: `401 unauthorized`, `402 insufficient_tokens`, `403 feature_not_enabled`, `409 idempotency_conflict` e `422 no_engine` — registro completo em [Erros](/motor-analise/erros).

## O que você construiu

* Um fluxo completo de análise de borderô: `POST /operations` multipart (202) → polling em `GET /operations/{id}` (`queued`/`processing` → `completed`) → resultado hierárquico em `GET /operations/{id}/result`.
* Um consumo enxuto do resultado: `?filter=issues` (padrão) entrega só os nós com problemas; `?filter=all` entrega a árvore completa.
* Interpretação de `issues`, `summary`, `coverage` e `degraded_rules` — incluindo a validação de NFe via issues do título e `nfe_chave`.

## Próximos passos

<CardGroup cols={2}>
  <Card title="Ciclo de vida" icon="arrows-rotate" href="/motores/ciclo-de-vida">
    Estados da operação, envelope de status e o endpoint /result
  </Card>

  <Card title="Idempotência" icon="repeat" href="/motores/idempotencia">
    Idempotency-Key opcional no borderô — replay e conflitos
  </Card>

  <Card title="Cobrança" icon="coins" href="/motores/cobranca">
    Modelo por entidade única, com exemplo numérico e estorno
  </Card>

  <Card title="Títulos e NFe" icon="file-invoice" href="/credito/titulos-e-nfe">
    O shape do título na árvore do resultado e a validação de NFe
  </Card>
</CardGroup>
