Catalisa.Building Blocks
Catálogo/Identidade/People

People

Alpha

Saber quanto você sabe de uma pessoa, e saber quando parou de saber

15
Endpoints
7
Entidades
0
Provedores
Tenant
Escopo
3038
Porta
2026-09
Desde

Todo mundo guarda o CPF do cliente. Quase ninguém sabe dizer se aquele endereço foi conferido, por quem, e se ainda vale. O People guarda a resposta e a mantém honesta sozinho.

Para quem é
  • Fintech de crédito que precisa provar, por operação, que conhecia o cliente naquele dia
  • Correspondente bancário e marketplace que atendem o mesmo CPF para clientes diferentes
  • Operação com revendedor, onde cada subconta precisa ver só as pessoas dela
  • Produto regulado que responde pedido de titular sob LGPD sem virar planilha
Substitui
  • Tabela de clientes caseira com colunas de CPF, renda e endereço sem procedência
  • Planilha de controle de validade de documento
  • Rotina manual de atendimento a pedido de titular sob LGPD
O que não é
  • Cadastro de login ou autenticação do usuário final — isso é o IAM
  • Motor de score ou de decisão de crédito — o People não pontua
  • Controle antifraude documental
  • Registro de pessoa jurídica — isso é o Companies
O que dá para fazer

16 endpoints em 1 recurso.

Explorar a API →
01

Resumo executivo

O People guarda o que a sua empresa sabe sobre uma pessoa física — e, junto com cada informação, de onde ela veio, quem conferiu e até quando vale. Não é uma lista de clientes: é o registro do que foi provado sobre cada um, com prazo de validade embutido.

Na prática, ele responde uma pergunta que hoje ninguém responde sem abrir cinco telas: "para liberar esta operação, eu sei o suficiente sobre esta pessoa?" O comprovante de residência venceu ontem? O nível cai sozinho, a resposta muda de sim para não e um evento avisa quem precisa saber — sem ninguém rodar nada.

O núcleo está implementado; o bloco não está completo. status: alpha — existe código em src/people/, schema people no Postgres, 15 rotas, fachada de dois métodos e 68 testes. O que ainda NÃO existe, de propósito, está listado em §15: consentimento formal com texto versionado, merge de duplicata, relatório e expurgo do titular, prova de vida pela porta de biometria e a ponte com o customers. O núcleo deixa o gancho pronto para os cinco — tabela, coluna ou porta — sem implementá-los. Nada foi deployado. O desenho continua em openspec/changes/people-block/ e o que existe hoje ao lado dele é o bloco Customers, que segue intacto (§12).

AtributoValor
Identificadorpeople
CategoriaIdentidade
EscopoTenant e subconta
Porta (standalone)3038
Path alias@people
StatusAlpha — núcleo implementado, 2026-09
Depende deIAM (duro); File Storage (fachada, opcional); Bureaus, Biometrics e Data Extraction (portas, ainda não ligadas)

02

O problema

negócio

O cenário. Uma operação de crédito recebe um cliente pelo WhatsApp, pede uns documentos, consulta um bureau, aprova. Seis meses depois, o cliente volta. Ninguém sabe dizer o que foi conferido da primeira vez, nem se ainda vale.

O que trava hoje.

  • O dado não tem origem. No customers, monthlyIncome é uma coluna. A renda que o cliente digitou e a renda que veio do bureau ocupam o mesmo lugar. Quem lê depois não distingue, e nenhuma política consegue exigir "renda comprovada" sem gambiarra no consumidor.
  • Nada vence. Um comprovante de 2023 e um de ontem valem igual. A regra que exigia documento recente vira responsabilidade de alguém lembrar.
  • O documento fica em claro. Person.taxIdValue é String sem cifra e sem coluna de hash; a busca por CPF é igualdade em texto claro. O bloco biometrics, que nasceu depois, já guarda o sujeito por HMAC.
  • Não há subconta. Um revendedor não consegue separar as pessoas dos clientes dele: o modelo só tem organizationId.
  • Ninguém consome. customers não tem fachada e nenhum outro bloco o importa. Cada produto reimplementa "quem é essa pessoa" por dentro.

O custo de não resolver. Toda contestação de operação vira arqueologia: abrir a conversa, achar o print, torcer para o arquivo ainda estar no lugar. Toda auditoria de LGPD vira trabalho manual — o data-extraction já documenta o custo dessa lacuna no §15 dele, chamando o expurgo manual de "a lacuna mais relevante para compliance". E toda regra de negócio que dependia de "documento recente" passou a depender de disciplina humana, que é a garantia mais cara que existe.


03

Proposta de valor

negócio
AntesDepois
"O endereço dele é esse""O endereço foi conferido em bureau no dia 12/03, vale até 12/09"
A ficha não muda quando a evidência venceO nível cai sozinho na leitura seguinte, e um evento avisa
CPF em texto claro na tabelaCPF cifrado em repouso, buscável por hash
Cada produto decide o que é "conhecer o cliente"Uma pergunta só, com o requisito vindo de quem pergunta
Revendedor vê tudo da organizaçãoSubconta vê só o que é dela; fora do escopo responde 404

Procedência por atributo. Cada informação carrega origem, confiança, quando foi verificada e quando expira — e valores de origens diferentes convivem em vez de se sobrescreverem em silêncio.

Nível que cai sozinho. O nível não é coluna: é calculado na leitura sobre as evidências válidas naquele instante. Entre a expiração e a leitura não existe janela em que a ficha mente.

Uma pergunta, uma resposta. GET /people/:id/assurance?required=evidenced devolve satisfied, o nível por dimensão e o que falta. Quem pergunta traz o requisito; o bloco não precisa conhecer o produto.

LGPD, com o gancho pronto. O consentimento tem tabela, o expurgo tem onde olhar, e o relatório tem de onde ler — mas as três rotas são do próximo PR (§15). O que o núcleo já garante é o que torna as três possíveis depois: procedência em toda informação e documento cifrado com leitura em claro sob permissão própria e com evento.


04

Casos de uso reais

negócio

Caso 1 — O cedente que voltou seis meses depois Cenário ilustrativo

Contexto

Fundo de antecipação de recebíveis médicos, cerca de duas dezenas de cedentes ativos, operações mensais.

A dor

O cedente volta para uma nova cessão. O analista não sabe o que foi conferido na primeira vez. Na dúvida, pede tudo de novo — e o cedente, que já mandou aquilo, reclama.

A solução com o BB

GET /people/:id/assurance?required=evidenced antes de abrir a proposta. A resposta diz que identidade e renda seguem comprovadas e que só o comprovante de residência venceu. O fluxo pede um documento.

O resultado

De "mande tudo de novo" para "falta só isto", sem ninguém conferir manualmente.

Caso 2 — O mesmo CPF em dois clientes do mesmo revendedor Cenário ilustrativo

Contexto

Plataforma vendida a um revendedor que atende duas financeiras.

A dor

As duas financeiras atendem a mesma pessoa. Num modelo com escopo só de organização, ou as duas veem o mesmo registro (vazamento) ou a segunda não consegue cadastrar (colisão).

A solução com o BB

Escopo de subconta desde o schema, unicidade por dono exato (organizationId + subaccountId + hash do documento), e 404 — nunca 403 — para quem pede o registro do vizinho: um 403 confirmaria que o registro existe.

O resultado

Cada financeira tem a sua ficha, com o seu histórico de evidência, e o revendedor vê as duas.

Caso 3 — A regra que dependia de alguém lembrar Cenário ilustrativo

Contexto

Esteira de crédito com política que exige comprovante de residência com menos de 90 dias.

A dor

A regra estava escrita na política e a verificação era manual. Operação aprovada com comprovante vencido só aparecia na auditoria.

A solução com o BB

O comprovante entra com expiresAt. No dia 91, a dimensão address cai na primeira leitura e a varredura diária publica people.assurance.expired — o workflow da esteira ouve o evento e pede a renovação antes de a operação chegar à mesa.

O resultado

A regra passa a se aplicar sozinha; a auditoria deixa de ser o primeiro lugar onde o problema aparece.

Caso 4 — O pedido do titular Cenário ilustrativo

Contexto

Operação regulada que recebe um pedido de acesso sob o art. 18 da LGPD.

A dor

Responder exige varrer banco, storage e conversas, à mão, com prazo legal correndo.

A solução com o BB

POST /people/:id/report devolve tudo — atributos com origem, documentos, verificações, consentimentos com o texto que foi aceito. POST /people/:id/erase expurga, preserva a trilha sem o dado pessoal e diz o que não pôde apagar por obrigação legal.

O resultado

O que era um mutirão vira duas chamadas, com registro do que foi feito.

⚠️ Ainda não. As duas rotas são o próximo PR (§15). Este caso descreve o destino, não o que responde hoje.


05

Mercado e diferenciais

negócio

Panorama. O mercado vende verificação, não registro. Persona, Onfido e Idwall orquestram muito bem o momento do onboarding: consultam, leem documento, batem selfie, devolvem aprovado ou reprovado. O que fazer com aquilo depois — onde guardar, como saber que envelheceu, como provar dali a dois anos o que se sabia no dia — continua sendo problema do cliente. É o espaço em que o People vive: ele não compete com a verificação, ele é o lugar onde o resultado dela mora e envelhece.

CritérioCatalisa PeoplePersonaOnfido StudioIdwall
Registro persistente com procedência por atributoSimParcialNãoParcial
Nível que decai por vencimento da evidênciaSimNãoNãoNão
Escopo de subconta (revendedor)SimNãoNãoNão
Catálogo de provedores de verificaçãoPelos blocos, um por tipoAmploAmploAmplo (Brasil)
Editor visual de esteiraNão (é o Decision Platform)SimSimSim
Dado sai no seu formatoSimParcialParcialParcial
PreçoEm definiçãoPor verificaçãoPor verificaçãoPor consulta

Nossos diferenciais.

  1. O nível decai sozinho. Não é um campo com job por trás — é derivado na leitura. Copiar exige repensar o modelo de dados inteiro, não acrescentar uma coluna.
  2. Procedência por atributo. Ter um campo "verificado: sim/não" é fácil; ter cada atributo com origem, confiança e prazo, e a política conseguindo exigir "esta origem, no máximo com tantos dias", muda o que dá para escrever como regra.
  3. Subconta no schema. Quem nasceu com um nível de tenancy raramente ganha o segundo sem reescrever a camada de acesso.

Quando escolher o concorrente. Se o seu problema é verificar e não guardar — você precisa de leitura de documento em 40 países amanhã, com certificação eIDAS — Onfido ou Persona entregam isso hoje e nós não. Se você quer um editor visual em que o time de risco monta a esteira sem engenharia, Persona é melhor nisso. E se você precisa de cobertura de fontes brasileiras pronta com esteira embutida, a Idwall resolve mais rápido. O People faz sentido quando o registro é seu ativo — quando você vai precisar, dali a dois anos, provar o que sabia naquele dia.


06

Modelo de cobrança e ROI

negócio

Precificação em definição. O bloco está especificado e nenhum preço foi fechado. O que já está decidido são os drivers.

DriverPor que eleComportamento esperado
Pessoas ativas no períodoÉ a unidade que o cliente entende e que cresce com o negócio deleCresce devagar, previsível
Verificações realizadasCada verificação tem custo de fornecedor por trásPicos no onboarding
Documentos lidosLeitura por modelo de visão tem custo por páginaProporcional ao volume de entrada

Por que a unidade é justa. Cobrar por consulta pune quem organiza o processo; cobrar por pessoa ativa alinha o preço ao valor. E consulta cacheada não é cobrada duas vezes — é a mesma disciplina que o bureaus já aplica, cujo próprio tagline é "cache que impede pagar duas vezes pelo mesmo CPF".

A conta de guardanapo. O custo evitado não é a verificação (essa você paga de qualquer jeito) — é a operação: o analista que reconfere o que já estava conferido, o mutirão de LGPD, a contestação que vira arqueologia. Quando houver preço, esta seção ganha a comparação em cenário nomeado, com a data da consulta ao preço público de cada análogo.


07

Arquitetura

flowchart TD
  Cliente["Painel / workflow / outro bloco"] -->|HTTP com JWT ou chave| Rotas
  OutroBB["Outro building block"] -->|IPeopleFacade| Servicos

  subgraph People
    Rotas["Rotas (Hono)"] --> Servicos["Serviços"]
    Servicos --> Assurance["AssuranceService (calcula na leitura)"]
    Servicos --> Repos["Repositórios (Prisma, schema people)"]
    Servicos --> Portas["Portas de saída"]
  end

  Portas -->|IBureausFacade| Bureaus
  Portas -->|IBiometricsFacade| Biometrics
  Portas -->|IFileStorageFacade| FileStorage
  Portas -->|fachada a criar| DataExtraction
  Servicos -->|EventPublisher| Stream["Stream iam-events"]
  Stream --> AuditTrail["audit-trail"]
  Stream --> Webhooks["webhooks-engine"]

Três decisões que não são óbvias.

O nível não é coluna. Se fosse, alguém escreveria nela, e a partir daí o nível vira opinião. Ele é derivado a cada leitura das evidências válidas naquele instante. A varredura diária existe só para avisar que caiu, nunca para mudar o estado — entre a expiração e o job não há janela em que a ficha mente.

Toda saída passa por fachada. O bloco nunca fala HTTP com provedor externo e nunca monta o caminho de outro bloco à mão. O motivo é concreto: o caminho real do data-extraction é /data-extraction/api/v1/data-extraction/extractions — o nome do módulo aparece duas vezes, e quem escreve de cabeça erra. Hoje o núcleo usa uma só: IFileStorageFacade, para conferir que o fileId do documento é desta organização. As portas de bureau, biometria e leitura de documento ainda não são disparadas por aqui (§15) — e quando forem, entram pelas fachadas, não por fetch solto. Fachada ausente no processo vira aviso, não queda: o documento é gravado sem a conferência do arquivo, que é honesto e não perde o dado.

Auditoria não tem fachada, e não precisa. Todo bloco publica no stream Redis iam-events via EventPublisher.safePublish, e o audit-trail lê esse mesmo stream por consumer group. O que o People precisa é publicar com organizationId e subaccountId na metadata — e ganhar entrada própria em RESOURCE_TYPE_MAPPING, senão o mapeador cai no genérico e grava o organizationId como id do recurso (bug já corrigido para baas.* e biometrics.*).

No modo monolito as fachadas resolvem para os serviços locais; em standalone, para chamadas HTTP por ModuleClient. O bloco não sabe em qual modo está.


08

Conceitos e modelo de dados

Glossário.

TermoDefinição
Nível de conhecimento (assurance)Quanto se sabe da pessoa: declared, verified, evidenced, certified. Calculado, nunca gravado
DimensãoO eixo em que o nível é medido — identity, address, income, liveness
ProcedênciaOrigem + confiança + data de verificação + prazo de um atributo
EvidênciaDocumento, verificação ou consentimento que sustenta uma dimensão
DonoO par organização + subconta. Unicidade e visibilidade são do dono exato
TombstonePonteiro deixado por um merge, que resolve o id perdido para o vencedor

Modelo de dados. Sete modelos no schema people. Toda tabela tem organizationId e subaccountId String?.

O prefixo dos modelos é People, e não Person: o nome Person já é do customers, e nome de modelo Prisma é global — o schema nem compila com os dois. Os nomes de TABELA são os da spec.

ModeloTabelaPropósitoCampos-chave
PeoplePersonpeople.peopleA pessoadocumentValue (cifrado), documentHash, documentMask, ownerId, externalRef, mergedIntoId
PeopleAttributeperson_attributesUm atributo com procedênciakey, value, source, confidence, verifiedAt, expiresAt — único por (pessoa, chave, origem)
PeopleContactperson_contactsTelefone ou e-mailkind, value, verifiedAt, verificationId
PeopleAddressperson_addressesEndereço com origemkind, source, verifiedAt, expiresAt
PeopleDocumentperson_documentsDocumento e sua leituratype, fileId, status, reading, validUntil, readingDeadlineAt
PeopleVerificationperson_verificationsUma verificação, nossa ou externaorigin, provider, outcome, grade, evidenceRef, expiresAt, idempotencyKey
PeopleConsentperson_consentsConsentimento colhido — gancho, sem rota aindapurpose, textHash, templateVersion, channel, revokedAt

ownerId não estava na spec, e precisa estar. A spec pediu @@unique([organizationId, subaccountId, documentHash]). Esse índice não impede a duplicata do caso mais comum — o da própria organização —, porque no Postgres NULL não é igual a NULL: duas linhas com subaccount_id nulo e o mesmo hash convivem sem violar nada. A coluna derivada owner_id (subaccountId ?? organizationId) resolve, é expressável em Prisma (índice parcial não é) e mantém o subaccount_id nulável de que o filtro de leitura precisa. Há prova em SQL disso no corpo do PR.

Máquina de estados do nível.

stateDiagram-v2
  [*] --> declared
  declared --> verified: fonte externa confere o dado
  verified --> evidenced: documento válido sustenta
  evidenced --> certified: prova de vida ou assinatura qualificada
  certified --> evidenced: a prova mais forte vence
  evidenced --> verified: o documento vence
  verified --> declared: a verificação vence

As setas para baixo não precisam de comando: acontecem porque a evidência deixou de estar válida no momento da leitura.

Máquina de estados do documento.

stateDiagram-v2
  [*] --> pending
  pending --> uploaded: arquivo confirmado no File Storage
  uploaded --> reading: leitura pedida ao Data Extraction
  reading --> read: leitura concluída
  reading --> pending: prazo estourado, com motivo operacional
  read --> valid: conferida e dentro da validade
  valid --> expired: passou de validUntil
  read --> rejected: conferência recusou

Atenção. reading → pending existe por um motivo medido: o consumidor assíncrono do data-extraction vem desligado por padrão e não aparece nas pilhas publicadas. A API responde 202 e a extração fica PENDING para sempre. O People não trata 202 como sucesso — ele espera, e depois desiste dizendo por quê.


09

Referência da API

Escrita a partir de src/people/routes/people.router.ts. São 15 rotas (mais /people/health e /people/health/ready, montadas pelo helper compartilhado). O que a spec desenhou e este PR não implementou — merge, consentimento, relatório, expurgo, config/* e rekey — saiu desta seção e está em §15, para a tabela não prometer rota que não responde.

Prefixo: /people/api/v1. Toda rota exige authMiddleware, requirePermission(...) e contexto de organização. Todas aceitam chave de subconta e o header X-Subaccount-Id (people está em SUBACCOUNT_AWARE).

Pessoas

MétodoRotaDescriçãoPermissão
POST/peopleCria ou reconhece por documento; idempotente por externalRef. 201 na criação, 200 no reconhecimentoPEOPLE_WRITE
GET/peopleLista por cursor (?cursor=&limit=&externalRef=)PEOPLE_READ
GET/people/:idFicha, documento mascarado. ?reveal=true exige PEOPLE_READ_DOCUMENT e publica eventoPEOPLE_READ
PATCH/people/:idAtualiza nome, externalRef, atributos, contatos e endereços — sempre com procedênciaPEOPLE_WRITE
GET/people/:id/attributesAtributos com origem, confiança, validade e divergência derivadaPEOPLE_READ
POST/people/resolveResolve até 100 referências (id, externalRef ou documento) numa chamada; cada item volta { person, error? }PEOPLE_READ
POST/people/contacts/resolveO melhor contato de um tipo (PHONE ou EMAIL) de até 100 pessoas cuja POSSE foi provada por fluxo interno (código enviado a ele e confirmado — PeoplePersonService.confirmContactPossession; a rota POST /people/:id/contacts/possession só responde à identidade interna de servidor, para qualquer outro chamador é 404), com o valor ainda igual ao da prova (HMAC). verifiedAt/verificationId gravados por PATCH ou POST /verifications não contam. Principal primeiro; vencido não conta; sem posse provada, contact: null. reveal: true traz o valor em claro, exige PEOPLE_READ_DOCUMENT (só JWT) e publica people.contact.revealed (só id e tipo). É o telefone do código do revínculo do banking — fachada resolveContact. Quem confirma posse hoje: a abertura de conta do banking (o código foi ao telefone informado e conferido; evento people.contact.possession_confirmed, com quem provou e sem o valor). Fora dela, só contato marcado por migração/seed tem possePEOPLE_READ

Nível de conhecimento

MétodoRotaDescriçãoPermissão
GET/people/:id/assurance?required=&dimensions=&cache= — nível por dimensão, satisfied e o que faltaPEOPLE_READ
POST/assurance/batchAté 100 referências, resposta 207 item a itemPEOPLE_READ

Evidência

MétodoRotaDescriçãoPermissão
POST/people/:id/documentsRegistra documento (tipo, emissor, validade, fileId)PEOPLE_WRITE
GET/people/:id/documentsLista documentos e situaçãoPEOPLE_READ
POST/people/:id/verificationsRegistra verificação — nossa (INTERNAL) ou trazida pelo cliente (EXTERNAL)PEOPLE_VERIFY
GET/people/:id/verificationsHistóricoPEOPLE_READ
POST/verifications/ingestLote de evidência externa, com chave de idempotência por registro; 207PEOPLE_VERIFY

Política do tenant

MétodoRotaDescriçãoPermissão
GET/config/policyO teto que evidência externa pode sustentar (externalMaxGrade) e se é o padrãoPEOPLE_READ
PUT/config/policyDefine o teto. Só a organização: subconta recebe 403 SUBACCOUNT_SCOPE_FORBIDDENPEOPLE_ADMIN

Toda verificação que entra pela rota é EXTERNAL. origin só aceita esse valor; mandar INTERNAL é recusado com 400, não ignorado. Evidência interna nasce de PeopleVerificationService.recordInternal, que não tem rota — só uma porta do bloco chama.

Referência ambígua é 409. Com chave de organização (sem X-Subaccount-Id), o mesmo externalRef ou documento pode existir em duas subcontas. O bloco não escolhe uma: resolve, assurance/batch e verifications/ingest devolvem PEOPLE_AMBIGUOUS_SUBJECT no item, e nada é gravado. Repita com a subconta.

A rota principal, em detalhe.

http
GET /people/api/v1/people/{id}/assurance?required=evidenced&dimensions=identity,income
GET /people/api/v1/people/{id}/assurance?required=evidenced&dimensions=identity,income
json
{
  "data": {
    "personId": "3f0a…",
    "level": "verified",
    "satisfied": false,
    "required": "evidenced",
    "dimensions": {
      "identity": { "level": "evidenced", "until": "2027-03-12T00:00:00.000Z",
                    "sustainedBy": [{ "kind": "document", "id": "d1…", "source": "document" }] },
      "income":   { "level": "verified",  "until": "2026-10-01T00:00:00.000Z",
                    "sustainedBy": [{ "kind": "verification", "id": "v2…", "source": "external" }] },
      "address":  { "level": "declared",  "until": null, "sustainedBy": [] },
      "liveness": { "level": "declared",  "until": null, "sustainedBy": [] }
    },
    "missing": [
      { "dimension": "income", "current": "verified", "need": "evidenced",
        "suggest": "anexe um documento de renda válido" }
    ],
    "cached": true,
    "computedAt": "2026-09-22T13:04:11.000Z"
  }
}
{
  "data": {
    "personId": "3f0a…",
    "level": "verified",
    "satisfied": false,
    "required": "evidenced",
    "dimensions": {
      "identity": { "level": "evidenced", "until": "2027-03-12T00:00:00.000Z",
                    "sustainedBy": [{ "kind": "document", "id": "d1…", "source": "document" }] },
      "income":   { "level": "verified",  "until": "2026-10-01T00:00:00.000Z",
                    "sustainedBy": [{ "kind": "verification", "id": "v2…", "source": "external" }] },
      "address":  { "level": "declared",  "until": null, "sustainedBy": [] },
      "liveness": { "level": "declared",  "until": null, "sustainedBy": [] }
    },
    "missing": [
      { "dimension": "income", "current": "verified", "need": "evidenced",
        "suggest": "anexe um documento de renda válido" }
    ],
    "cached": true,
    "computedAt": "2026-09-22T13:04:11.000Z"
  }
}

Quais dimensões entram na conta. Com ?dimensions=, as que você pediu. Sem ele, as que têm algum sinal — e identity sempre tem, porque a pessoa só existe com um documento informado. A spec não decidiu isto, e o padrão óbvio (as quatro, sempre) transformaria liveness num veto universal: toda pessoa sem biometria responderia declared e a pergunta perderia a serventia. Uma dimensão sem sinal continua visível na resposta — ela só não puxa o resultado para baixo sem ninguém ter pedido. Para exigi-la, peça-a: ?dimensions=identity,liveness.

ErroQuando
400required fora do enum, dimensão desconhecida, lote acima de 100 (PEOPLE_BATCH_TOO_LARGE), atributo sem source, origin: INTERNAL, grau externo acima do teto (PEOPLE_EXTERNAL_GRADE_ABOVE_POLICY)
403Permissão insuficiente — ?reveal=true sem PEOPLE_READ_DOCUMENT (ou com chave de API), PUT /config/policy por subconta
404Pessoa inexistente ou fora do escopo do chamador — o 404 é deliberado
409externalRef já é de outra pessoa do dono (PEOPLE_EXTERNAL_REF_CONFLICT); referência ambígua entre subcontas (PEOPLE_AMBIGUOUS_SUBJECT)
503fileId informado e File Storage indisponível (FILE_STORAGE_UNAVAILABLE) — falha fechado, não grava
207Lote: não é erro, é resposta item a item

10

Início rápido

Do zero ao primeiro nível calculado. Em ambiente local (bun run dev:people, porta 3038).

O que foi executado, e o que não foi. O fluxo abaixo — criar, perguntar, registrar evidência, perguntar de novo — está coberto por teste de integração com testcontainers (tests/integration/people/people-core-flow.test.ts, 15 casos, verdes). Os curl desta seção não foram rodados contra um serviço no ar: este PR não deploya nada. Eles são a tradução literal do que o teste exercita.

1. Autenticar.

bash
TOKEN=$(curl -s -X POST "$IAM/iam/api/v1/users/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"…","password":"…"}' | jq -r '.accessToken')
TOKEN=$(curl -s -X POST "$IAM/iam/api/v1/users/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"…","password":"…"}' | jq -r '.accessToken')

A rota é /users/login e o token vem na raiz (.accessToken), como documentam baas, biometrics e audit-trail. Até 23/09/2026 esta seção dizia /auth/login e .data.accessToken: quem seguia o início rápido parava num 404.

Se você acabou de criar o usuário pela API, ele não loga ainda. POST /iam/api/v1/users devolve status: "pendente", e o login responde 401 Invalid credentials — a mesma mensagem de senha errada, então não há como distinguir olhando a resposta. Ative antes: PATCH /iam/api/v1/users/:id com {"status":"ativo"}.

2. Criar a pessoa. Idempotente por externalRef; o mesmo documento no mesmo dono reconhece em vez de duplicar.

bash
curl -s -X POST "$BASE/people/api/v1/people" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Maria Souza","document":{"type":"BR_CPF","value":"12345678909"},
       "externalRef":"lead-8821",
       "attributes":[{"key":"income","value":"5000","source":"declared","confidence":"LOW"}]}'
curl -s -X POST "$BASE/people/api/v1/people" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Maria Souza","document":{"type":"BR_CPF","value":"12345678909"},
       "externalRef":"lead-8821",
       "attributes":[{"key":"income","value":"5000","source":"declared","confidence":"LOW"}]}'

201 na primeira vez, 200 nas seguintes. O documento volta mascarado:

json
{ "data": { "id": "3f0a…", "externalRef": "lead-8821",
            "document": { "type": "BR_CPF", "masked": "***.***.789-09" } } }
{ "data": { "id": "3f0a…", "externalRef": "lead-8821",
            "document": { "type": "BR_CPF", "masked": "***.***.789-09" } } }

3. Perguntar o nível com um requisito.

bash
curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified" \
  -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified" \
  -H "Authorization: Bearer $TOKEN"

satisfied: false, com missing dizendo o que falta.

4. Registrar a evidência — aqui, uma verificação que o cliente trouxe pronta.

bash
curl -s -X POST "$BASE/people/api/v1/people/3f0a…/verifications" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"dimension":"identity","type":"document.match","origin":"EXTERNAL",
       "provider":"idwall","outcome":"APPROVED","grade":"VERIFIED",
       "occurredAt":"2026-09-20T10:00:00.000Z","expiresAt":"2027-03-20T10:00:00.000Z",
       "evidenceRef":"protocolo-8821"}'
curl -s -X POST "$BASE/people/api/v1/people/3f0a…/verifications" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"dimension":"identity","type":"document.match","origin":"EXTERNAL",
       "provider":"idwall","outcome":"APPROVED","grade":"VERIFIED",
       "occurredAt":"2026-09-20T10:00:00.000Z","expiresAt":"2027-03-20T10:00:00.000Z",
       "evidenceRef":"protocolo-8821"}'

5. Perguntar de novo. identity sai em verified, com until — mas a resposta global ainda vem level: declared, satisfied: false:

bash
curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified" \
  -H "Authorization: Bearer $TOKEN"        # → level: declared, satisfied: false

curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified&dimensions=identity" \
  -H "Authorization: Bearer $TOKEN"        # → level: verified, satisfied: true
curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified" \
  -H "Authorization: Bearer $TOKEN"        # → level: declared, satisfied: false

curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified&dimensions=identity" \
  -H "Authorization: Bearer $TOKEN"        # → level: verified, satisfied: true

Não é defeito, é a regra de §9 "quais dimensões entram na conta": sem ?dimensions=, entram todas as que têm algum sinal — e o atributo income do passo 2, ainda que declared, é um sinal. A dimensão income entra na conta em declared e puxa o nível global para baixo. Pergunte pela dimensão que o seu ato exige (?dimensions=identity), ou registre evidência de renda também. Até 23/09/2026 este passo prometia satisfied: true sem o recorte — o smoke bateu nisto.

6. Uma tela inteira, numa chamada.

bash
curl -s -X POST "$BASE/people/api/v1/assurance/batch" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"required":"verified","refs":[{"id":"3f0a…"},{"externalRef":"lead-8821"}]}'
curl -s -X POST "$BASE/people/api/v1/assurance/batch" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"required":"verified","refs":[{"id":"3f0a…"},{"externalRef":"lead-8821"}]}'

Resposta 207: cada item resolvido por si, um ruim não derruba os outros.


11

Receitas

Receita 1 — Liberar uma operação só se o nível bastar

Objetivo. Não deixar a operação seguir com conhecimento insuficiente.

bash
RESP=$(curl -s "$BASE/people/api/v1/people/$ID/assurance?required=evidenced" -H "Authorization: Bearer $TOKEN")
echo "$RESP" | jq -e '.data.satisfied == true' >/dev/null || { echo "$RESP" | jq '.data.missing'; exit 1; }
RESP=$(curl -s "$BASE/people/api/v1/people/$ID/assurance?required=evidenced" -H "Authorization: Bearer $TOKEN")
echo "$RESP" | jq -e '.data.satisfied == true' >/dev/null || { echo "$RESP" | jq '.data.missing'; exit 1; }

Armadilha. O required vem de quem pergunta, nunca do bloco. Qual nível cada ato exige é regra do produto e vive na política da vertical — o People não conhece o ato.

Receita 2 — Uma tela com 80 pessoas, sem 80 chamadas

Objetivo. Listar a fila da mesa sem derrubar a tela.

bash
curl -s -X POST "$BASE/people/api/v1/assurance/batch" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"required":"verified","refs":[{"id":"…"},{"externalRef":"lead-8821"}]}'
curl -s -X POST "$BASE/people/api/v1/assurance/batch" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"required":"verified","refs":[{"id":"…"},{"externalRef":"lead-8821"}]}'

Resposta 207: cada item resolvido por si, um ruim não derruba os outros.

Armadilha. Não substitua isto por um laço no cliente. Uma chamada por linha de tela já nos custou uma tela de 38 s, uma resposta de 16 MB e recusa por rajada — e nenhuma das três apareceu em teste, só no cliente. Teto de 100 por lote, e paginação por cursor, nunca por offset.

Receita 3 — Renovar antes de a evidência vencer

Objetivo. Pedir o documento novo antes de a operação travar.

Assine people.assurance.expired no webhooks-engine e dispare o fluxo de renovação. O evento traz pessoa, dimensão, nível anterior e nível atual.

Armadilha. O evento é aviso, não causa. O nível já tinha caído na leitura anterior; quem depende de estado lê o assurance, não o evento.

Receita 4 — Ingerir evidência que o cliente já tem

Objetivo. Aproveitar o que já foi verificado fora, sem fingir que fomos nós.

bash
curl -s -X POST "$BASE/people/api/v1/verifications/ingest" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"items":[{"person":{"externalRef":"lead-8821"},"dimension":"income",
        "type":"income.statement","origin":"EXTERNAL","provider":"parceiro",
        "outcome":"APPROVED","grade":"VERIFIED","occurredAt":"2026-09-20T10:00:00.000Z",
        "idempotencyKey":"lote-7/linha-1"}]}'
curl -s -X POST "$BASE/people/api/v1/verifications/ingest" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"items":[{"person":{"externalRef":"lead-8821"},"dimension":"income",
        "type":"income.statement","origin":"EXTERNAL","provider":"parceiro",
        "outcome":"APPROVED","grade":"VERIFIED","occurredAt":"2026-09-20T10:00:00.000Z",
        "idempotencyKey":"lote-7/linha-1"}]}'

Armadilhas. A chave de idempotência é por registro e é do dono: rodar o lote de novo devolve 200 duplicate em vez de criar a segunda linha — é isso que faz a retomada de uma carga interrompida ser segura. Gere a chave com o id da pessoa dentro; a mesma chave apontando para outra pessoa é recusada (PEOPLE_IDEMPOTENCY_CONFLICT). E origin: EXTERNAL exige provider e occurredAt: sem quem verificou e quando, não é evidência, é afirmação — e ela fica marcada como externa para sempre, porque não somos nós que podemos auditá-la.

Receita 5 — Saber de onde veio cada informação

Objetivo. Responder "esse endereço foi conferido por quem, e ainda vale?".

bash
curl -s "$BASE/people/api/v1/people/$ID/attributes" -H "Authorization: Bearer $TOKEN" | jq '.data[]'
curl -s "$BASE/people/api/v1/people/$ID/attributes" -H "Authorization: Bearer $TOKEN" | jq '.data[]'

Cada atributo volta com o vigente (o de maior confiança entre os não vencidos), as others origens e divergent: true quando duas origens válidas afirmam valores diferentes.

Armadilha. divergent é derivado na leitura, como o nível — não há coluna para marcá-lo, e não há escrita que "resolva" a divergência. Resolver é acrescentar uma origem mais forte.

Merge de duplicata, relatório e expurgo do titular (/merge, /report, /erase) são do próximo PR — ver §15. Até lá, não existe rota para eles, e esta seção não finge que existe.


12

Integração com outros building blocks

BBComo se relacionaObrigatório
IAMEmite o token; organização, subconta e permissõesSim
File StorageGuarda o arquivo do documento; o People guarda o fileId e o confere pela fachadaLigado
BureausPorta de verificação por consulta externa, pela IBureausFacadeNão ligado (§15)
BiometricsProva de vida; a evidência assinada sustenta livenessNão ligado (§15)
Data ExtractionLê o documento enviado — ver a ressalva abaixoNão ligado (§15)
E-SignatureAssinatura que pode sustentar certifiedNão ligado (§15)
Document TemplateTexto versionado do consentimentoNão ligado (§15)
Audit TrailLê os eventos do stream e deriva a trilhaAutomático
Webhooks EngineEntrega people.* ao clienteOpcional
CompaniesAponta para pessoas como sócio, beneficiário ou representante—
CustomersAntecessor; convivência em cinco passos—
flowchart LR
  Captura["Captura (WhatsApp, portal)"] --> People
  People -->|consulta| Bureaus
  People -->|prova de vida| Biometrics
  People -->|lê documento| DataExtraction["Data Extraction"]
  People -->|guarda arquivo| FileStorage["File Storage"]
  People -->|nível basta?| Esteira["Esteira de crédito"]
  Companies -->|ponteiro de sócio| People
  People -->|eventos| Auditoria["audit-trail + webhooks-engine"]

Atenção. O consumidor assíncrono do data-extraction vem desligado por padrão (EXTRACTION_CONSUMER_ENABLED=false) e não aparece nas pilhas publicadas — a API responde 202 e a extração fica PENDING para sempre. Ligar esse consumidor é pré-requisito de qualquer fluxo do People que dependa de leitura de documento, e o instalador da vertical declara a dependência.

Convivência com o Customers, em cinco passos: fachada de compatibilidade → espelho de escrita → migração por organização com relatório de duplicata → virada de leitura → customers marcado legado. Nada é apagado no mesmo passo em que é migrado. Detalhe em openspec/changes/people-block/design.md.


13

Configuração e operação

Variáveis de ambiente.

NomeDescriçãoObrigatóriaPadrão
PEOPLE_ENCRYPTION_KEYChave de 32 bytes em hex para cifrar o documento em repousoSim—
PEOPLE_DOCUMENT_HMAC_KEYChave do HMAC de busca por documentoSim—
PEOPLE_ASSURANCE_CACHE_TTLTTL do cache do nível, em segundosNão60
PEOPLE_BATCH_MAXTeto de itens por loteNão100
PEOPLE_OUTBOUND_CONCURRENCYTeto de chamadas simultâneas às portas, por organizaçãoNão8
PEOPLE_DOCUMENT_READING_TIMEOUT_MINUTESQuanto um documento espera em READING antes de o bloco desistirNão30
PEOPLE_EXPIRY_SCAN_ENABLEDLiga a varredura de vencimento (ela só avisa)Nãotrue
MODULE_PEOPLE_URL / MODULE_PEOPLE_PORTDescoberta do módulo em standaloneSó em standalone`` / 3038

Atenção. Definir PEOPLE_ENCRYPTION_KEY ou PEOPLE_DOCUMENT_HMAC_KEY depois de já haver dado gravado torna o dado ilegível. A troca exige rewrap — é o que POST /rekey faz, e é a mesma dívida que src/shared/lib/credential-crypto.ts já registra para as credenciais.

Infraestrutura. PostgreSQL (schema people), Redis (cache do nível e stream iam-events), S3/MinIO pelo File Storage.

Limites. Lote de 100 (/assurance/batch e /verifications/ingest); /people/resolve também em 100; concorrência interna de 8; listagem só por cursor, no máximo 100 por página.

O cache do nível tem DOIS tetos, não um. O TTL configurado e o instante da próxima expiração, o que vier antes. Só o TTL não bastaria: uma evidência que vence em 5 segundos com TTL de 60 serviria 55 segundos de resposta errada — exatamente a janela que o bloco existe para não ter. A chave guarda a ENTRADA do cálculo, não a resposta, então ela serve a qualquer required; e toda escrita de evidência a invalida. A chave é (organização, pessoa) — não leva o dono de quem lê: a primeira versão levava, e a organização gravando evidência deixava a chave da subconta mentindo por até 60 s. Quem pode ler é decidido antes de o cache entrar.

Catálogo de erros.

CódigoSignificaO que fazer
VALIDATION (source ausente)Atributo sem procedênciaInforme source; não há padrão
PEOPLE_BATCH_TOO_LARGELote acima do tetoQuebre em páginas; não faça laço por linha
PEOPLE_EXTERNAL_REF_CONFLICT (409)externalRef já é de outra pessoa deste donoCorrija pelo PATCH, não pelo POST
PEOPLE_AMBIGUOUS_SUBJECT (409)Referência casa com pessoas de mais de uma subcontaRepita com X-Subaccount-Id; o bloco nunca escolhe
PEOPLE_EXTERNAL_GRADE_ABOVE_POLICY (400)Evidência externa acima do teto do tenantBaixe o grade ou mude a política com PEOPLE_ADMIN
PEOPLE_IDEMPOTENCY_CONFLICT (400)Chave de idempotência já usada para OUTRA pessoaGere a chave com o id da pessoa dentro
PEOPLE_FILE_NOT_FOUND (400)fileId não é desta organizaçãoConfira o arquivo no File Storage
FILE_STORAGE_UNAVAILABLE (503)fileId informado e a fachada não está no processoSuba o File Storage ou anexe sem fileId
SUBACCOUNT_SCOPE_UNSUPPORTEDO bloco não está em SUBACCOUNT_AWAREErro de configuração do deploy

Eventos publicados (stream iam-events, com organizationId e subaccountId na metadata; people.* tem entrada própria em RESOURCE_TYPE_MAPPING do audit-trail): people.person.created, people.person.updated, people.person.revealed, people.document.attached, people.verification.completed, people.assurance.raised, people.assurance.expired. Nenhum carrega dado pessoal — nem documento, nem documento mascarado, nem nome, nem contato: só id, dimensão, nível, origem e referência da prova. Há teste que varre os eventos publicados procurando os CPFs usados no fluxo.

Observabilidade. O que existe hoje é o log estruturado de cada varredura e de cada falha de cache. Métrica de distribuição de nível por dimensão e histograma das portas de saída ainda não existem — estão em §15, não aqui.

Instalação por vertical. Tudo que é configurável por tenant — tipos de documento e validade, finalidades de consentimento, portas ligadas, retenção, mapa de dimensões — é declarado pelo pacote da vertical e semeado pelo instalador com a chave do tenant, pelo mesmo caminho que creditBlocks já usa. Um tenant novo não nasce com o bloco vazio. O que fica de fora, de propósito: credencial de provedor, chave de cifra e texto legal próprio do cliente — segredo e jurídico não entram em template versionado. Três regras herdadas de instalações que quebraram: o pacote declara tudo o que usa (pacote que usa o não declarado instala com 201 e o tenant nasce quebrado sem aviso); a instalação é retomável e idempotente por item, com relatório do que faltou; e o conteúdo vem do banco, então a ordem é publicar, semear e só então instalar. Detalhe em openspec/changes/people-block/design.md.


14

Segurança e compliance

Isolamento. Duas camadas. O middleware compartilhado resolve organização e subconta e falha fechado: um bloco fora do mapa SUBACCOUNT_AWARE recusa chave de subconta e recusa o header, em vez de ignorá-los — por isso people entra no mapa no mesmo commit que cria o bloco. Dentro do bloco, leitura e posse usam predicados distintos: a organização enxerga tudo que é dela; a subconta enxerga só o dela; e a checagem de unicidade usa o dono exato. Recurso fora do escopo responde 404, nunca 403 — um 403 confirmaria a existência do recurso alheio.

Dado sensível. O documento é cifrado em repouso com aes-256-gcm pelo núcleo compartilhado de cifra, e a busca usa HMAC-SHA-256 — o hash resolve unicidade e busca sem ser reversível, e leva a organização dentro da entrada, então o mesmo documento em tenants diferentes não produz o mesmo hash. O número do documento anexado (person_documents.number) é cifrado pela mesma chave: RG e CNH são dado pessoal igual.

Ver o documento inteiro é um ato, não uma consulta. PEOPLE_READ devolve a máscara (***.***.789-09). O valor em claro exige ?reveal=true e a permissão separada PEOPLE_READ_DOCUMENT, e a leitura publica people.person.revealed. Isto é uma correção ao desenho: a spec dizia que o documento em claro aparecia "a quem tem PEOPLE_READ", o que tornaria a máscara decorativa — quem lê a ficha leria o CPF de qualquer jeito.

Chave de API nunca revela. A chave autoriza por padrão de recurso (allowedResources), não por RBAC; o middleware a normaliza num user com permissions: []. Portanto ?reveal=true com chave de API é sempre 403, e é deliberado: automação não precisa do CPF em claro, e uma chave vazada não consegue exportar a base. Só JWT de usuário com PEOPLE_READ_DOCUMENT vê o valor.

O que é IMPOSTO sobre a evidência, e onde.

RegraOndeO que acontece se violar
Pela rota, origin é sempre EXTERNALZod da rota (origin: literal('EXTERNAL')) e o serviço público, que nem lê origin do input400 na rota; no serviço, grava EXTERNAL mesmo que o schema fosse burlado
INTERNAL só nasce de porta do blocoPeopleVerificationService.recordInternal, sem rotaNão há como chegar por HTTP
Evidência externa obedece ao teto do tenantPeoplePolicyService.externalMaxGrade no caminho de gravação400 PEOPLE_EXTERNAL_GRADE_ABOVE_POLICY; nunca rebaixada em silêncio
O teto padrão é CERTIFIED (externa vale igual à nossa)Decisão do dono; o tenant aperta com PUT /config/policy—
Política é da organizaçãoPUT /config/policy recusa subconta403 SUBACCOUNT_SCOPE_FORBIDDEN
Evidência externa exige provider e occurredAtZod da rota400

Nada de dado pessoal em log, evento ou mensagem de erro. Os eventos carregam id, dimensão, nível e referência da prova; o erro de item de lote é genérico de propósito, porque erro de banco carrega valor de coluna e coluna aqui é dado pessoal. Retenção configurável de evidência por tenant ainda não existe (§15).

LGPD. Base legal e finalidade ficam no consentimento, com o hash do texto exibido e a versão do modelo — é o que permite provar depois qual texto a pessoa aceitou; a tabela existe e a rota é o próximo PR (§15). Relatório do titular (art. 18) e expurgo ainda não existem como rota — e o README diz isso em vez de repetir o plano. O que o núcleo já entrega para eles é o que os torna possíveis: cada informação com origem e prazo, e o documento cifrado.

Autenticação e permissões. JWT do IAM ou chave de API. Cinco permissões, cada uma com rota: PEOPLE_READ (ficha, nível, listas), PEOPLE_WRITE (pessoa, documento), PEOPLE_VERIFY (verificação e ingestão), PEOPLE_ADMIN (PUT /config/policy) e PEOPLE_READ_DOCUMENT (?reveal=true) — esta última porque ver o documento inteiro não pode vir junto com ler a ficha.

E elas estão atribuídas a papéis no catálogo padrão (src/iam/constants/permissions.ts), não só declaradas: VIEWER ganha PEOPLE_READ (e não PEOPLE_READ_DOCUMENT — quem só observa não precisa do CPF); CUSTOMER_MANAGER ganha as outras quatro, inclusive PEOPLE_ADMIN; VERIFICATION_OPERATOR ganha PEOPLE_READ e PEOPLE_VERIFY; ADMIN já recebe tudo. O data-extraction é o contra-exemplo que motivou isso: as permissões dele "não estão atribuídas a nenhum papel intermediário no catálogo padrão do IAM".


15

Limitações conhecidas

LimitaçãoDetalheGravidade
Consentimento é tabela, não rotaperson_consents existe como gancho; nenhuma rota escreve nela. Consentimento formal com texto versionado é o próximo PRPróximo passo declarado
Sem merge de duplicataA coluna merged_into_id existe como gancho; POST /merge e o tombstone nãoPróximo passo declarado
Sem relatório e sem expurgo do titularPOST /report e POST /erase são do próximo PR. Até lá, atender art. 18 aqui continua manualPróximo passo declarado
Sem POST /rekeyTrocar chave exige recifrar a base, e a rota que faz isso ainda não existe. Definir as chaves antes do primeiro registroOperação
config/* por tenant tem um botão sóconfig/policy (teto da evidência externa) existe. Tipos de documento, finalidades, portas e mapa de dimensões ainda são vocabulário de código. O instalador da vertical depende distoPróximo passo declarado
Portas de verificação não ligadasBureau, biometria e leitura de documento ainda não são disparados pelo bloco; recordInternal é o gancho por onde entram. O que funciona hoje é ingestão de evidência externa: o cliente traz o resultado no nosso vocabulárioPor desenho, neste PR
Nada deployadoStacks e docker-compose declaram o serviço; nenhum ambiente subiuDeclarado
A leitura de documento depende de um consumidor desligadoEXTRACTION_CONSUMER_ENABLED é false por padrão e não aparece nas pilhas publicadas; a API responde 202 e a extração fica PENDING para sempreBloqueante para fluxo com documento
Um provedor de visãoSó o Google está implementado no data-extraction; OpenAI e Anthropic recusam com "not yet implemented"Não anuncie multi-provedor
Sem antifraude documentalO data-extraction "lê o que está escrito e acredita"; documento adulterado é lido normalmentePor desenho — não use como controle antifraude
Três fachadas não existemdata-extraction, e-signature e document-template não têm fachada hoje; precisam ser criadas antes das portasTrabalho declarado
Troca de chave exige rewrapTrocar PEOPLE_ENCRYPTION_KEY sem migrar torna o dado ilegívelOperação, com POST /rekey
certified depende de assinatura qualificadaO e-signature entrega SIMPLE com o provedor aberto; certified exige provedor que entregue nível superiorDepende de contratação
Sem grafo de relacionamento entre pessoasperson.related_people existe no bureaus e o People guarda o resultado como atributo, sem navegaçãoRoadmap
Sem métrica de negócioDistribuição de nível por dimensão e histograma de porta não são emitidosRoadmap
Preço não definidoSó os drivers estão decididosComercial

Pessoa é do tenant, e isso é decisão de produto — não limitação técnica. O mesmo CPF em duas organizações são dois registros distintos, com histórico, consentimento e nível de conhecimento próprios. Não existe pessoa global, nem tabela comum, nem deduplicação entre organizações, nem reaproveitamento de verificação de um tenant por outro — nem quando isso economizaria uma consulta paga a bureau. O motivo é que a base legal do tratamento é de cada tenant: o consentimento que a organização A colheu não autoriza a organização B a saber nada, e a vantagem comercial de uma base compartilhada não paga o risco jurídico de tratá-la como se autorizasse. A decisão está no mecanismo, não só na regra de consulta: o document_hash é HMAC com a organização dentro da entrada, então o mesmo CPF produz hashes diferentes em tenants diferentes, não há índice sobre document_hash sozinho, e toda consulta entra pelo dono. Correlacionar as duas fichas é impossível mesmo para quem lê o banco direto. Dois testes de integração guardam isso: um provando que as duas organizações cadastram o mesmo CPF independentemente, outro provando que nenhuma rota de uma alcança a pessoa da outra — por id, por documento ou pela resolução em lote da fachada.


16

Perguntas frequentes

Por que um bloco novo em vez de melhorar o Customers? Porque a mudança é estrutural: cifrar o documento, criar coluna de hash, mudar a unicidade e partir cinco colunas largas numa tabela de atributos com procedência é reescrever o bloco por dentro, com dado em produção e sem lugar para pousar o intermediário. Como customers não tem nenhum consumidor em processo hoje, o bloco novo sai barato e a convivência é curta.

O Customers vai ser apagado? Não no mesmo passo. A sequência é fachada de compatibilidade, espelho de escrita, migração por organização com relatório de duplicata, virada de leitura e, por último, customers marcado legado e congelado. A remoção, se acontecer, é um PR separado.

O nível pode ser forçado no braço? Não. Ele é calculado na leitura. O que dá para fazer é acrescentar evidência — e aí ele sobe sozinho. Essa recusa é o ponto do bloco: nível editável é nível que vira opinião.

Como o nível cai sem ninguém rodar nada? Porque nada foi gravado. A evidência tem expiresAt; no instante em que você lê, o cálculo ignora o que já venceu. A varredura só avisa com o evento people.assurance.expired. Há teste que prova isso do jeito mais direto possível: a mesma entrada, calculada em dois instantes, dá evidenced antes da expiração e declared depois — sem nenhuma escrita entre as duas leituras.

O que já responde e o que ainda não? Respondem: criar/reconhecer, ficha com procedência, documento, verificação (nossa ou trazida de fora), ingestão em lote, nível por dimensão, lote de nível e a fachada de dois métodos. Não respondem ainda: consentimento, merge, relatório e expurgo do titular, config/* por tenant, rekey e o disparo das portas de bureau/biometria/extração. A lista completa está em §15.

O mesmo CPF pode estar em dois clientes meus? Pode, e deve. A unicidade é do dono exato — organização mais subconta. Duas financeiras do mesmo revendedor atendem a mesma pessoa sem enxergar uma à outra. E quem pede o registro do vizinho recebe 404, não 403.

Quem decide qual nível cada operação exige? Você, na política da sua vertical. O bloco recebe ?required= e responde se atende e o que falta. Ele não conhece o seu produto, de propósito.

Isso substitui a Idwall ou a Unico? Não. Elas verificam; o People guarda o resultado com procedência e prazo. Você continua contratando quem verifica — e passa a ter onde o resultado mora e envelhece.

O que acontece se o provedor de leitura de documento estiver fora? O documento fica em reading até o prazo e depois volta para pending com o motivo operacional. O que o bloco não faz é tratar o 202 como sucesso — foi exatamente assim que extrações ficaram PENDING para sempre em outro bloco.

Um tenant novo precisa de configuração manual? Não. Tipos de documento, finalidades de consentimento, portas ligadas, retenção e mapa de dimensões vêm declarados no pacote da vertical e são semeados pelo instalador com a chave do tenant. O que continua manual, de propósito, é credencial de provedor, chave de cifra e texto jurídico próprio.