# DICT API v2.12.0 — extração da fonte oficial, delta v2.11→v2.12 e fechamento dos SEM-FONTE

Data: 2026-07-07. Fonte primária (autoridade máxima): página Redoc oficial do BACEN
`https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html` (renderiza a spec OpenAPI
v2.12.0 inteira) e o changelog associado `.../pix/changelog.html`. Cruzamento com nosso estado:
`docs/reports/2026-07-07-paridade-dict-v2110-matriz.md`,
`pix/backend/apps/shared/lib/shared/bacen/dict_client.ex` e
`docs/reports/2026-07-07-construtor-tabela-verdade-envio.md`.

## Nota metodológica (honestidade sobre a coleta)

A página **carregou** via WebFetch (não é SPA bloqueante para o extrator; o Redoc entrega o HTML
renderizável). O changelog `changelog.html` também carregou com entradas datadas. **Não** consegui
baixar o arquivo OpenAPI bruto (`.yaml`/`.json`): a página menciona "Download OpenAPI specification"
mas não expõe a URL do arquivo no HTML renderizado, e não há `spec-url` legível no excerto. Toda a
extração abaixo veio da página renderizada + changelog.

Aviso importante de fidelidade: o WebFetch resume a página com um modelo pequeno e, em duas consultas,
**conflou enums entre recursos**. Eu re-verifiquei os dois itens carregáveis e sinalizo o que virou:
- **key-statistics**: uma passada disse `GET /entries/{Key}/statistics`; a verificação dirigida
  confirmou que o real é `GET /entries/{Key}?IncludeStatistics=true` (o texto oficial cita o
  "parâmetro opcional `IncludeStatistics`"). **Nosso cliente já usa essa forma** — não há divergência.
- **RefundReason**: uma passada listou `FRAUD/COERCION/ACCOUNT_TAKEOVER/REFUND_REQUEST`; a verificação
  dirigida **não encontrou** o enum de RefundReason explícito na página (provável confusão com o
  `SituationType` da InfractionReport). Portanto **NÃO cravo** mudança de RefundReason pela página.

Onde a página não mostra o valor literal, o item fica marcado "nao-visto" e não foi inventado.

---

## 1) Versão vigente e changelog v2.12.0 vs v2.11.0

**Versão vigente: 2.12.0**, datada **2026-06-29** no changelog (v2.11.0 = 2026-05-11).
Base URLs oficiais confirmadas na página: homolog `https://dict-h.pi.rsfn.net.br:16522/api/v2/`,
produção `https://dict.pi.rsfn.net.br:16422/api/v2/`. Formato de erro: RFC 7807 "problem+xml",
`type` = `https://dict.pi.rsfn.net.br/api/v2/error/<ErrorType>`.

### Delta v2.11.0 → v2.12.0 (changelog oficial, alta confiança)

| # | Mudança oficial (v2.12.0) | Nos afeta? | Severidade |
|---|---|---|---|
| 1 | Novo valor **`PARTICIPANT_EXCLUSION`** adicionado a TRÊS domínios: `ClaimOperationReason`, `RefundRejectionReason` e `EntryOperationReason` | **SIM** — nossos whitelists de reason NÃO incluem `PARTICIPANT_EXCLUSION`; entrada/saída com esse motivo é rejeitada pela nossa validação | **Alta** |
| 2 | Regex da **chave CNPJ** alterada para aceitar **alfanumérico**: `^(?i)[a-z0-9]{14}$` | Parcial — nosso validador de chave CNPJ do DICT **já foi alargado** para alfanumérico; resíduo em REDA (ver §Adaptações) | **Média** |
| 3 | Regex do **número de conta** (`AccountNumber`) alterada para aceitar **alfanumérico** | Baixo — construímos o `<AccountNumber>` como string livre (sem regex digit-only no builder DICT) | **Baixa** |
| 4 | Regex do **nome empresarial** (business name) alterada restringindo asteriscos isolados | Improvável — não validamos business name com asterisco no envio | **Baixa** |
| 5 | Descrições de `ReporterParticipant` e `CounterpartyParticipant` atualizadas no objeto `InfractionReport` | Documental — semântica, sem mudança de contrato de campo | **Baixa** |

### Já entregues na v2.11.0 (contexto; já cobertos pela matriz 2.11)

Parâmetro `Cursor` opcional em "List Refund Requests"; exceção `DeprecatedResource` (HTTP 410) para
endpoints descontinuados; erro `ResourceConflict` (HTTP 409, "recurso sendo processado por outra
operação"); `IncludeIndirectParticipants` + campo `Participant` na resposta de `listEventNotifications`;
erros `RefundTransactionInvalid` e `TransactionTypeInvalid`; parâmetro `Status` opcional em
"List Fraud Markers"; dois endpoints de notificação de infração marcados como deprecated.

---

## 2) Contrato por recurso (extraído da página v2.12.0)

### Entries (Vínculo)
Endpoints: `POST /entries/`, `GET /entries/{Key}`, `PUT /entries/{Key}`, `POST /entries/{Key}/delete`,
`POST /keys/check`.
- `AccountType`: **CACC, SVGS, TRAN, SLRY, OTHR**.
- `KeyType`: **CPF, CNPJ, PHONE, EMAIL, EVP**. Regex CNPJ agora `^(?i)[a-z0-9]{14}$` (alfanumérico);
  CPF `^[0-9]{11}$`; EVP UUID; EMAIL ≤77 minúsculo; PHONE `^\+[1-9]\d{1,14}$`.
- `EntryOperationReason` por operação (oficial):
  - Create: `USER_REQUESTED`, `RECONCILIATION`
  - Update: `USER_REQUESTED`, `BRANCH_TRANSFER`, `RECONCILIATION`, `RFB_VALIDATION`
  - Delete: `USER_REQUESTED`, `ACCOUNT_CLOSURE`, `RECONCILIATION`, `FRAUD`, `RFB_VALIDATION`
  - `PARTICIPANT_EXCLUSION` agora consta na lista ampla de reasons (novo em 2.12).
- Headers obrigatórios de GetEntry: `PI-RequestingParticipant` (8, `^(?i)[a-z0-9]{8}`),
  `PI-PayerId` (`^([0-9]{11}|[0-9]{14})$`), `PI-EndToEndId` (E2E da pacs.008).
- Limite de chaves por titular: **nao-visto numericamente** — a página só expõe o erro
  `EntryLimitExceeded` ("número de vínculos associados a conta transacional excedeu o limite máximo"),
  sem o valor (PF 5 / PJ 20 não aparecem literalmente). Paginação: default 20, máx 200.

### Claims / Portabilidade (Reivindicação)
Endpoints: `POST /claims/`, `GET /claims/`, `GET /claims/{ClaimId}`, e os lifecycle
`/acknowledge`, `/confirm`, `/cancel`, `/complete`.
- `Claim.ClaimerAccount` inclui **`OpeningDate`** (obrigatório) — confirma que o campo
  `claimer_opening_date` é exigido no payload (nosso GAP conhecido).
- `ClaimType`: **OWNERSHIP, PORTABILITY**.
- `ClaimStatus`: **OPEN, WAITING_RESOLUTION, CONFIRMED, CANCELLED, COMPLETED**.
- `ClaimOperationReason` (cancelamento/resolução): **USER_REQUESTED, ACCOUNT_CLOSURE, FRAUD,
  DEFAULT_OPERATION, RECONCILIATION, RFB_VALIDATION, PARTICIPANT_EXCLUSION** (último novo em 2.12).
- Prazos: `ResolutionPeriodEnd` (período de resolução) e `CompletionPeriodEnd` (encerramento).

### Infraction Reports (Notificação de Infração)
Endpoints: `POST /infraction-reports/`, `GET /infraction-reports/{Id}`, `GET /infraction-reports/`,
`/acknowledge`, `/close`, `PUT /infraction-reports/{Id}`, `/cancel`. (2 endpoints legados deprecados
desde 2.11.)
- Campos: `TransactionId`, `Reason`, `SituationType`, `ReportDetails`, `ReporterParticipant`,
  `CounterpartyParticipant`, `Status`, `AnalysisResult`, `AnalysisDetails`.
- `Reason`/InfractionType: **REFUND_REQUEST, FRAUD, AML_CTF**.
- `SituationType`: **SCAM, ACCOUNT_TAKEOVER, COERCION, FRAUDULENT_ACCESS, OTHER, UNKNOWN**.
- `Status`: **OPEN, ACKNOWLEDGED, CLOSED, CANCELLED**.
- `AnalysisResult` (no close): **AGREED, DISAGREED**.

### Refunds (Solicitação de Devolução)
Endpoints: `POST /refunds/`, `GET /refunds/{RefundId}`, `GET /refunds/`, `/close`, `/cancel`.
- `RefundAnalysisResult` (no close): **TOTALLY_ACCEPTED, PARTIALLY_ACCEPTED, REJECTED** (confirma o
  enum do legado e fecha o SEM-FONTE do RefundAnalysisResult).
- `RefundRejectionReason`: ganhou **`PARTICIPANT_EXCLUSION`** em 2.12 (changelog). Os demais valores
  literais **não** apareceram de forma confiável na página (a passada que listou `NO_BALANCE/FRAUD_DISPUTE`
  não se sustentou na verificação) — **nao-visto** para o conjunto completo.
- `RefundReason`: **nao-visto** — a página renderizada não expõe o enum literal do RefundReason
  (a listagem `FRAUD/COERCION/ACCOUNT_TAKEOVER/REFUND_REQUEST` de uma passada era conflação com o
  `SituationType`). Mantemos como aberto contra a fonte oficial.
- Prazos/janelas MED: **nao-visto** numericamente (só o erro `RefundPeriodExpired`).

### Funds-Recovery (Recuperação de Valores)
Endpoints: `POST /funds-recoveries/`, `GET /funds-recoveries/{Id}`, `PUT /funds-recoveries/{Id}`,
`POST|GET /funds-recoveries/{Id}/tracking-graph` (POST **deprecado**), `POST /funds-recoveries/{Id}/block`
(**deprecado**), `GET /funds-recoveries/{Id}/infraction-reports`, `POST /funds-recoveries/{Id}/refund`,
`GET /funds-recoveries/{Id}/refunds`, `POST /funds-recoveries/{Id}/cancel`.

### CID / Sincronismo
Endpoints: `POST /cids/files/`, `GET /cids/files/{CidSetFileId}`, `GET /cids/events/`,
`GET /entries/` (consulta por CID), `POST /sync-verifications/`, `POST /keys/check`.

### Event-notifications
`GET /event-notifications/` — parâmetros: `Participant` (obrigatório, 8), `IncludeIndirectParticipants`
(bool, default false, novo em 2.11), `ModifiedAfter`/`ModifiedBefore`, `Limit` (≤200, default 20).

### Fraud Markers (Marcação de Fraude)
Endpoints: `POST /fraud-markers/`, `GET /fraud-markers/`, `GET /fraud-markers/{Id}`,
`POST /fraud-markers/{Id}/cancel`.
- `FraudType` — **5 valores, no SINGULAR**: **APPLICATION_FRAUD, MULE_ACCOUNT, SCAMMER_ACCOUNT,
  OTHER, UNKNOWN**. (Fecha o SEM-FONTE do FraudType; as formas plurais NÃO são o enum de submissão.)
- **List Fraud Markers aceita** `Participant` (obrigatório), `Status` (array, desde 2.11),
  `ModifiedAfter`, `ModifiedBefore`, `Limit`. Ou seja, `Participant`+`ModifiedAfter` **são suportados** —
  o 400 que tomamos não foi por parâmetro inexistente (ver §Adaptações item 5).

### Statistics (Antifraude)
- Person: `GET /persons/{TaxIdNumber}/statistics`. Key: `GET /entries/{Key}?IncludeStatistics=true`
  (parâmetro `IncludeStatistics`, NÃO um sub-recurso `/statistics`). Janelas de contadores citadas:
  d90 / m12 / m60. **Nosso cliente já usa exatamente esses dois caminhos.**

### Códigos de erro (problem+xml) capturados na página
`BadRequest`, `RequestSignatureInvalid`, `RequestIdAlreadyUsed`, `InvalidReason`, `ParticipantInvalid`,
`TaxIdNumberBlocked`, `UserOrAccountWithFraudRelatedRestriction`, `EntryLockedByClaim`,
`EntryKeyOwnedByDifferentPerson`, `EntryKeyInCustodyOfDifferentParticipant`,
`EntryTaxIdNumberByDifferentOwner`, `EntryBlocked`, `EntryWithFraudRelatedRestriction`,
`EntryLimitExceeded`, `TransactionNotRefundable`, `TransactionRefundable`, `TransactionTypeInvalid`,
`RefundTransactionInvalid`, `RefundAlreadyProcessedForTransaction`,
`RefundAlreadyBeingProcessedForTransaction`, `RefundPeriodExpired`, `RefundInfractionReportNotFound`,
`FundsRecoveryInvalid`, `FundsRecoveryOperationInvalid`, `FundsRecoveryTrackingGraphParameterInvalid`,
`FundsRecoveryTransactionNotIndexed`, `FundsRecoveryTransactionNotEligible`,
`FundsRecoveryTrackingTimeout`, `FundsRecoveryForTransactionAlreadyExists`, `Forbidden`, `NotFound`,
`ResourceConflict` (409), `DeprecatedResource` (410), `RateLimited` (429), `InternalServerError` (500),
`ServiceUnavailable` (503).

---

## 3) Fechamento dos SEM-FONTE da matriz 2.11 (com a fonte oficial)

| Item SEM-FONTE (matriz 2.11) | Situação agora | Valor oficial |
|---|---|---|
| Enum de motivos de **cancel de claim** | **FECHADO** | `ClaimOperationReason` = USER_REQUESTED, ACCOUNT_CLOSURE, FRAUD, DEFAULT_OPERATION, RECONCILIATION, RFB_VALIDATION, PARTICIPANT_EXCLUSION |
| Enum oficial **InfractionType/Reason** | **FECHADO** | Reason = REFUND_REQUEST, FRAUD, AML_CTF; `SituationType` = SCAM, ACCOUNT_TAKEOVER, COERCION, FRAUDULENT_ACCESS, OTHER, UNKNOWN |
| **FraudType** (5 oficiais? plural/singular?) | **FECHADO** | 5, SINGULAR: APPLICATION_FRAUD, MULE_ACCOUNT, SCAMMER_ACCOUNT, OTHER, UNKNOWN (nosso `@bacen_enum` já é idêntico) |
| **RefundAnalysisResult** (TOTALLY/PARTIALLY/REJECTED?) | **FECHADO** | TOTALLY_ACCEPTED, PARTIALLY_ACCEPTED, REJECTED |
| **fraud-markers LIST** aceita participant + ModifiedAfter? | **FECHADO** | Sim: Participant (obrigatório), Status, ModifiedAfter, ModifiedBefore, Limit |
| **RefundRejectionReason** oficial | **PARCIAL** | Confirmado que ganhou PARTICIPANT_EXCLUSION em 2.12; conjunto completo dos demais valores nao-visto na página |
| **RefundReason** oficial | **NÃO fechado** | enum literal nao-visto na página renderizada (precisa do OpenAPI bruto) |
| **Limite de chaves por titular** (PF/PJ) | **NÃO fechado** | só o erro EntryLimitExceeded aparece; valor numérico nao-visto |
| **Janelas MED** (prazos oficiais) | **NÃO fechado** | só o erro RefundPeriodExpired aparece; janelas nao-visto |
| Nomes EN dos contadores de estatística | **PARCIAL** | janelas d90/m12/m60 citadas; nomes completos dos contadores nao-visto |

**Placar: 5 SEM-FONTE fechados na íntegra + 2 parciais. 3 permanecem abertos** (RefundReason,
limite de chaves por titular, janelas MED) — dependem do arquivo OpenAPI bruto ou das planilhas
de domínios/erros, que a página renderizada não expõe literalmente.

---

## 4) Adaptações necessárias (priorizadas), com file:line do nosso lado

| # | Sev | Adaptação | Nosso ponto (arquivo:linha) |
|---|---|---|---|
| 1 | **Alta** | Aceitar `PARTICIPANT_EXCLUSION` como reason em **delete de entry** (novo em 2.12). O whitelist não o contém; entrada do BACEN com esse motivo (exclusão de participante) seria rejeitada. | `apps/shared/lib/shared/schemas/dict/entry.ex:209` (`valid_reasons`), `apps/shared/lib/shared/schemas/dict/entry.ex:16` (`@delete_reasons`), `apps/dict_service/lib/dict_service/keys.ex:281`, `apps/dict_service/lib/dict_service/keys/entry.ex:16` |
| 2 | **Alta** | Aceitar `PARTICIPANT_EXCLUSION` em **RefundRejectionReason** e em **ClaimOperationReason** (cancel). Auditar os whitelists de reason de refund/claim. | refund reasons `apps/shared/lib/shared/bacen/dict/request_builder.ex:1218` (`@refund_reasons`); claim cancel default em `apps/dict_service/lib/dict_service/claims.ex:771` |
| 3 | **Média** | Resíduo digit-only de CNPJ contra a regra alfanumérica 2.12: REDA usa `~r/^\d{14}$/`. O validador de **chave CNPJ do DICT já foi alargado** (`entry.ex:126-133`, comentário IN RFB 2.229/2024) e `spi/account.ex:58` já usa `^[0-9A-Z]{12}[0-9]{2}$`; falta o REDA. | `apps/shared/lib/shared/schemas/reda/indirect_participant.ex:43` |
| 4 | **Média** | GAPs herdados da matriz 2.11 confirmados pela fonte: `ClaimerAccount.OpeningDate` é obrigatório (nosso create_claim falha-fecha por `claimer_opening_date`); `reason`/`situation_type` no create_infraction. | claim `dict_client.ex:1642-1645`; infraction `dict_client.ex:701-706` |
| 5 | **Baixa** | fraud-markers LIST: a fonte confirma que `Participant`+`ModifiedAfter`+`Status` são suportados. O 400 que tomamos NÃO é por parâmetro inexistente — investigar valor de `Status` (deve ser array de estados válidos) ou combinação de filtros, não remover o parâmetro. | `dict_client.ex:1252-1253` (`list_fraud_markers`) |
| 6 | **Baixa** | Confirmar que tratamos `ResourceConflict` (409) e `DeprecatedResource` (410) no `ResponseParser`/retry (já existem na v2.11; validar mapeamento problem+xml). | `apps/shared/lib/shared/bacen/dict/response_parser.ex` (parse_problem_xml) |

Itens que a fonte oficial **confirmou já corretos** do nosso lado (sem ação): FraudType (5 singular,
idêntico ao `@bacen_enum`), key-statistics via `?IncludeStatistics=true`, person-statistics via
`/persons/{TaxIdNumber}/statistics`, AccountType/KeyType, headers de GetEntry, paginação máx 200.

---

## 5) O que ficou bloqueado (não inventado)

- Arquivo OpenAPI bruto (`.yaml`/`.json`): a página cita "Download OpenAPI specification" mas não
  expõe a URL do arquivo no HTML renderizado; não consegui baixá-lo por WebFetch. Sem ele, os enums
  literais de `RefundReason` e `RefundRejectionReason` completos, o limite numérico de chaves por
  titular e as janelas MED não puderam ser cravados contra a fonte. Recomendação: obter o `.yaml`
  oficial ou as planilhas de domínios/erros do BACEN para fechar os 3 itens abertos.
