Comprovante de Residência
Este endpoint recebe um comprovante de residência — conta de luz, água, telefone, internet — lê o endereço impresso nele e confere esse endereço com a base dos Correios.
A resposta separa as duas coisas com clareza:
extraction— o endereço que estava escrito no comprovante.postOfficeData— o endereço que os Correios têm cadastrado para aquele CEP, mais ummatchesdizendo, campo a campo, se os dois batem.
É essa comparação que dá valor à resposta. Um CEP lido corretamente mas com logradouro divergente é um sinal diferente de um endereço que confere inteiro, e o matches é onde essa diferença aparece.
Este endpoint não verifica se o documento enviado é mesmo um comprovante de residência: ele assume que é, e tenta extrair um endereço dali. Também não confere o titular da conta contra a Receita Federal, e não lê documentos de identificação — para isso existe o Full OCR.
PDF original entrega mais que foto de conta impressa
Para uma lista de emissores conhecidos, um PDF com o texto embutido — o arquivo que você baixa do site da concessionária — é lido direto do texto do PDF, sem passar por OCR. Esse caminho é mais preciso, e é o único em que o número e o complemento do endereço são efetivamente preenchidos.
Uma foto ou um print da conta cai no caminho de OCR, que é mais sujeito a erro de leitura. Sempre que o seu fluxo permitir, peça o PDF original.
Request
POST/proof-of-residence/v3Headers
Authorization: ApiKey <sua-chave-de-api>Este endpoint também aceita o token JWT, no formato Authorization: Bearer <accessToken>. Veja Token JWT.
Parâmetros
Este endpoint não tem parâmetros de query string. Os que você enviar são ignorados, sem erro.
Arquivos aceitos
| Item | Valor |
|---|---|
| Formatos | application/pdf, image/png, image/jpeg |
| Quantidade por chamada | 1 arquivo |
| Tamanho máximo (multipart) | 15 MB |
O nome do campo do formulário é livre — ele volta na resposta, em data[].fieldname e em metadata.filesInfo[].fieldname. Enviar mais de um arquivo é recusado com 422.
Além do multipart/form-data, o arquivo pode ser enviado em JSON, no campo base64, como descrito em Envio de arquivos.
Exemplo Request
curl -i -X POST 'https://api-homolog.nxcd.app/proof-of-residence/v3' \
--header 'Authorization: ApiKey SUA_CHAVE_AQUI' \
--form 'comprovante=@./conta-de-luz.pdf'Response
Esta seção descreve a v3, a versão recomendada. A v2 e a v4 devolvem um data com outro formato — veja Versões.
O envelope é o padrão da API:
| Campo | Descrição | Tipo |
|---|---|---|
| id | Identificador único da requisição | String |
| version | Versão da API que atendeu a chamada | String |
| data | Lista com uma entrada por endereço reconhecido. Vem vazia quando nenhum endereço foi validado. | Object[] |
| metadata | Metadados da requisição | Object |
‼️ data vazio é resposta normal, não erro
Na v3, no caminho de OCR, um endereço cujo CEP não foi localizado na base dos Correios é descartado da resposta. Se o comprovante não rendeu nenhum CEP válido, você recebe 200 com "data": [] e metadata.filesInfo normalmente preenchido. No caminho de leitura direta do PDF esse descarte não acontece: o resultado volta com os matches em false.
Verifique o tamanho de data antes de ler data[0] — um código que assume data[0].extraction recebe um erro em vez de um resultado vazio.
Cada item de data traz:
| Campo | Descrição | Tipo |
|---|---|---|
| page | Página do arquivo em que o endereço foi encontrado. Começa em 0. | Number |
| fieldname | Nome do campo em que o arquivo foi enviado | String |
Mais os três blocos abaixo.
data[].extraction
O endereço como estava escrito no comprovante, sem nenhuma correção.
| Campo | Descrição | Tipo |
|---|---|---|
| extraction.zipCode | CEP lido, sem máscara | String |
| extraction.address | Logradouro lido | String |
| extraction.number | Número lido | String |
| extraction.district | Bairro lido | String |
| extraction.city | Cidade lida | String |
| extraction.state | UF lida | String |
| extraction.addressComplement | Complemento lido | String |
🚧 number e addressComplement dependem do caminho de leitura
No caminho de OCR — imagens, e PDFs de emissores não reconhecidos — esses dois campos ainda não são extraídos e voltam vazios.
Eles só são preenchidos no caminho de leitura direta do PDF, descrito no início desta página. Não construa uma regra de negócio que dependa deles sem tratar o vazio.
{
"extraction": {
"zipCode": "84145000",
"address": "R RIO SOLIMOES",
"number": "169",
"district": "CENTRO",
"city": "CARAMBEI",
"state": "PR",
"addressComplement": "CASA 01"
}
}data[].postOfficeData
O endereço que os Correios têm cadastrado para o CEP lido, e o resultado da comparação com o que estava no comprovante.
| Campo | Descrição | Tipo |
|---|---|---|
| postOfficeData.zipCode | CEP conforme os Correios | String |
| postOfficeData.address | Logradouro conforme os Correios | String |
| postOfficeData.district | Bairro conforme os Correios | String |
| postOfficeData.city | Cidade conforme os Correios | String |
| postOfficeData.state | UF conforme os Correios | String |
| postOfficeData.matches.zipCode | true quando o CEP lido foi localizado na base dos Correios | Boolean |
| postOfficeData.matches.address | true quando o logradouro lido corresponde ao dos Correios | Boolean |
| postOfficeData.matches.district | true quando o bairro corresponde | Boolean |
| postOfficeData.matches.city | true quando a cidade corresponde | Boolean |
| postOfficeData.matches.state | true quando a UF corresponde | Boolean |
Como ler os matches
matches.zipCode é o mais importante: ele diz se o CEP existe. Os outros quatro comparam texto — a leitura do OCR contra o cadastro dos Correios — e um false neles significa "não confere", o que pode ser tanto endereço divergente quanto erro de leitura.
postOfficeData traz o endereço dos Correios, não o do comprovante. Quando você quer mostrar o endereço ao usuário e os matches conferem, o dos Correios é o mais confiável dos dois; o número e o complemento, porém, os Correios não têm — esses só existem em extraction.
{
"postOfficeData": {
"zipCode": "84145000",
"address": "Rua Rio Solimoes",
"district": "Centro",
"city": "Carambei",
"state": "PR",
"matches": {
"zipCode": true,
"address": true,
"district": true,
"city": true,
"state": true
}
}
}data[].classification
| Campo | Descrição | Tipo |
|---|---|---|
| classification.type | Tipo do documento | String |
| classification.subtype | Emissor reconhecido do comprovante, quando houver | String |
| classification.country | País emissor | String |
| classification.sides | Lista com uma entrada por página analisada | Object[] |
| classification.sides[].side | Face do documento. No caminho de OCR, sempre null. | String/null |
| classification.sides[].page | Página analisada, começando em 0 | Number |
| classification.sides[].fieldname | Nome do campo em que o arquivo foi enviado | String |
| classification.sides[].confidence | Confiança da classificação | Number |
🚧 Este bloco não é um classificador
Neste endpoint, classification é em boa parte um rótulo fixo, não o resultado de uma análise. No caminho de OCR, ele vem sempre com type: "ProofOfResidence", subtype: null, country: "BRA" e confidence: 0 — inclusive quando o arquivo enviado não é um comprovante de residência.
O subtype só traz o emissor reconhecido, e a confidence só sobe de zero, no caminho de leitura direta do PDF.
Não use este bloco para decidir se o arquivo enviado era mesmo um comprovante. Se você precisa dessa verificação, faça-a antes, com o Classificador.
metadata
| Campo | Descrição | Tipo |
|---|---|---|
| metadata.filesInfo | Lista com uma entrada por arquivo enviado | Object[] |
| metadata.filesInfo[].fieldname | Nome do campo usado no envio | String |
| metadata.filesInfo[].name | Nome do arquivo enviado | String |
| metadata.filesInfo[].size | Tamanho do arquivo, em bytes | Number |
| metadata.filesInfo[].pages | Número de páginas do arquivo. Para imagens, 1. | Number |
| metadata.filesInfo[].mimetype | Tipo do arquivo enviado | String |
| metadata.filesInfo[].encoding | Codificação do arquivo no envio | String |
| metadata.filesInfo[].sha256 | Hash SHA-256 do arquivo enviado | String |
| metadata.timeSpent | Tempo de processamento da requisição, em milissegundos | Number |
Exemplo completo
Status Code: 200Comprovante lido e endereço conferido com os Correios:
{
"id": "f9ba65a4-394e-4bbc-8748-7789a1bae137",
"version": "v3",
"data": [
{
"page": 0,
"fieldname": "comprovante",
"extraction": {
"zipCode": "84145000",
"address": "R RIO SOLIMOES",
"number": "169",
"district": "CENTRO",
"city": "CARAMBEI",
"state": "PR",
"addressComplement": "CASA 01"
},
"postOfficeData": {
"zipCode": "84145000",
"address": "Rua Rio Solimoes",
"district": "Centro",
"city": "Carambei",
"state": "PR",
"matches": {
"zipCode": true,
"address": true,
"district": true,
"city": true,
"state": true
}
},
"classification": {
"type": "ProofOfResidence",
"subtype": null,
"country": "BRA",
"sides": [
{
"side": null,
"page": 0,
"fieldname": "comprovante",
"confidence": 0
}
]
}
}
],
"metadata": {
"filesInfo": [
{
"fieldname": "comprovante",
"name": "conta-de-luz.pdf",
"size": 82611,
"pages": 1,
"mimetype": "application/pdf",
"encoding": "7bit",
"sha256": "e844439599eb440b031d4ce4ebe2cfe3c37d483fb76d56b622bc77da6c435bed"
}
],
"timeSpent": 4199
}
}Endereço parcialmente conferido
Status Code: 200CEP encontrado, mas o logradouro lido não corresponde ao cadastro dos Correios:
{
"id": "6a1d1e8c-4a7d-4a5e-9f2c-1b0f2a9c7e11",
"version": "v3",
"data": [
{
"page": 0,
"fieldname": "comprovante",
"extraction": {
"zipCode": "84145000",
"address": "R RI0 S0LIM0ES",
"number": "",
"district": "CENTR0",
"city": "CARAMBEI",
"state": "PR",
"addressComplement": ""
},
"postOfficeData": {
"zipCode": "84145000",
"address": "Rua Rio Solimoes",
"district": "Centro",
"city": "Carambei",
"state": "PR",
"matches": {
"zipCode": true,
"address": false,
"district": false,
"city": true,
"state": true
}
},
"classification": {
"type": "ProofOfResidence",
"subtype": null,
"country": "BRA",
"sides": [
{ "side": null, "page": 0, "fieldname": "comprovante", "confidence": 0 }
]
}
}
],
"metadata": {
"filesInfo": [
{
"fieldname": "comprovante",
"name": "conta.jpg",
"size": 421308,
"pages": 1,
"mimetype": "image/jpeg",
"encoding": "7bit",
"sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
}
],
"timeSpent": 6210
}
}Nenhum endereço validado
Status Code: 200{
"id": "a7f0c3d2-9e51-4b8a-8f31-2d4c6b9e0a77",
"version": "v3",
"data": [],
"metadata": {
"filesInfo": [
{
"fieldname": "comprovante",
"name": "foto-borrada.jpg",
"size": 152064,
"pages": 1,
"mimetype": "image/jpeg",
"encoding": "7bit",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
],
"timeSpent": 5030
}
}Headers de resposta
| Header | Quando aparece |
|---|---|
Nextid-ReqId | Em todas as respostas. Traz o identificador da requisição, o mesmo do campo id do corpo. |
Erros
| Código | Quando ocorre |
|---|---|
| 401 Unauthorized | Chave de API ou token JWT ausente, inválido, ou sem permissão para este endpoint. |
| 404 Not Found | A versão informada na URL não existe. Só v2, v3 e v4 são aceitas. |
| 406 Not Acceptable | O corpo foi enviado como JSON, mas sem o campo base64. |
| 413 Payload Too Large | O arquivo enviado em multipart/form-data passa de 15 MB. |
| 415 Unsupported Media Type | O header Content-Type está ausente ou não é suportado. |
| 422 Unprocessable Entity | O formato do arquivo não é aceito, ou foi enviado mais de um arquivo na mesma chamada. |
| 500 Internal Server Error | Falha inesperada durante o processamento. |
O formato das respostas de erro está descrito em Códigos HTTP das respostas.
Versões
A versão recomendada é a v3, chamada em /proof-of-residence/v3.
‼️ Omitir a versão entrega a chamada para a v2
POST /proof-of-residence, sem versão no caminho, é atendido pela v2, que tem outro formato de resposta. Informe sempre a versão na URL.
| Versão | Situação | O que muda |
|---|---|---|
| v3 | Recomendada | matches como booleanos simples, classification com type/subtype/country/sides. Descarta endereços com CEP não localizado. |
| v4 | Disponível | Formato diferente: extraction deixa de ser o endereço e passa a trazer titular e identificadores da conta, e aparece um enhanced. Veja abaixo. |
| v2 | Legada | matches como objetos com matched, confidence e strategy. classification com outro vocabulário. Não descarta nada. É o que responde quando a versão é omitida na URL. |
O que muda na v4
A v4 muda o propósito do extraction. Em vez do endereço, ele passa a trazer quem é o titular e quais são os identificadores da conta — é a versão para quem precisa casar o comprovante com um cliente, e não só validar o endereço.
| Campo | Descrição | Tipo |
|---|---|---|
| extraction.name | Nome do titular, como lido na conta | String |
| extraction.address | Bloco de endereço, como lido na conta | String |
| extraction.taxNumber | CPF ou CNPJ do titular, como lido na conta | String |
| extraction.clientNumber | Número do cliente na concessionária | String |
| extraction.installation | Número da instalação ou da unidade consumidora | String |
| enhanced | Os mesmos quatro identificadores já normalizados, somados aos campos de endereço separados (zipCode, address, number, district, city, state, complement) | Object |
| postOfficeData | Mesmo formato da v3 | Object |
| classification | O emissor reconhecido do comprovante, com name, uf e version — não o type/subtype/country da v3 | Object |
🚧 Na v4 o complemento se chama complement, e não addressComplement
Nos campos de endereço da v2 e da v3, o complemento vem em addressComplement. No enhanced da v4, o mesmo dado vem em complement. É isso mesmo que a API devolve — os dois nomes convivem, cada um na sua versão.
🚧 A v4 não tem formato único
O conteúdo da v4 depende do caminho que a análise tomou. Na leitura direta do PDF, o extraction volta com os campos de endereço da v3, e enhanced e classification não aparecem. No caminho de OCR, vale a tabela acima.
Além disso, a v4 não descarta endereços com CEP não localizado, ao contrário da v3.
Se o que você precisa é validar endereço, fique na v3. Considere a v4 quando precisar do titular e dos identificadores da conta, e trate a ausência dos campos.
‼️ Trocar de versão quebra quem lê a resposta
O resultado da conferência do CEP está em data[].postOfficeData.matches.zipCode.matched na v2 e em data[].postOfficeData.matches.zipCode na v3 — booleano direto. E o endereço lido do comprovante, que na v2 e na v3 está em data[].extraction, na v4 muda de significado.
Um código escrito para uma versão não funciona na outra sem alteração.
O que muda na v2
Duas diferenças em relação à v3:
postOfficeData.matchesé mais detalhado. Cada campo vira um objeto commatched,confidence(0 a 1) estrategy(char-to-charou vazio), em vez de um booleano. O caminho para o mesmo dado deixa de sermatches.citye passa a sermatches.city.matched.classificationusa o vocabulário antigo. Traz apenasconfidenceetype: "PROOF-OF-RESIDENCE"— não hásubtype,countrynemsides.
A v2 também não descarta entradas com CEP não localizado: elas continuam em data, com os matches em false.
{
"id": "13cfec7c-b238-4820-a4d1-5173e4c1418e",
"version": "v2",
"data": [
{
"page": 0,
"fieldname": "comprovante",
"extraction": {
"zipCode": "84145000",
"address": "R RIO SOLIMOES",
"number": "",
"district": "CENTRO",
"city": "CARAMBEI",
"state": "PR",
"addressComplement": ""
},
"postOfficeData": {
"zipCode": "84145000",
"address": "Rua Rio Solimoes",
"district": "Centro",
"city": "Carambei",
"state": "PR",
"matches": {
"zipCode": { "matched": true, "confidence": 1, "strategy": "" },
"address": { "matched": true, "confidence": 0.94, "strategy": "char-to-char" },
"district": { "matched": true, "confidence": 1, "strategy": "char-to-char" },
"city": { "matched": true, "confidence": 1, "strategy": "char-to-char" },
"state": { "matched": true, "confidence": 1, "strategy": "char-to-char" }
}
},
"classification": {
"confidence": 0,
"type": "PROOF-OF-RESIDENCE"
}
}
],
"metadata": {
"filesInfo": [
{
"fieldname": "comprovante",
"name": "conta-de-luz.pdf",
"size": 82611,
"pages": 1,
"mimetype": "application/pdf",
"encoding": "7bit",
"sha256": "e844439599eb440b031d4ce4ebe2cfe3c37d483fb76d56b622bc77da6c435bed"
}
],
"timeSpent": 4199
}
}