Pular para o conteúdo
Node.js

Node.js: consultando CPF e CNPJ no Serasa

Paloma Macetko
Ilustração colorida de um unicórnio de crina arco-íris apontando uma varinha para um pergaminho com um documento, revelando um medidor de pontuação, ao lado de uma coruja de óculos com um selo de aprovação e um castelo com velas flutuantes

Olá meus Unicórnios! 🦄✨

Sabe quando você olha para uma tarefa e pensa "ah, isso é só um GET numa API"? 😅 Pois é. Era o que eu pensava quando precisei consultar CPF e CNPJ no Serasa Experian — puxar a pontuação de crédito e as pendências de alguém antes de fechar um contrato.

E é um GET, sim. Mas entre o "é só um GET" e o "está funcionando" tem uma lista de detalhes que a documentação não conta com todas as letras: o documento não vai na URL, o erro chega em dois formatos diferentes, e — o meu favorito, o que me custou mais tempo — a consulta pode responder HTTP 200 dizendo absolutamente nada.

Este artigo é o caminho inteiro em Node.js puro: login, token, consulta e leitura do retorno. Sem biblioteca nenhuma, porque o fetch já vem embutido no Node desde a versão 18. 🎉

🔑 Primeiro o login, depois a consulta

A API do Serasa trabalha em dois passos, e não tem como pular o primeiro:

POST /security/iam/v1/client-identities/login   ->  accessToken
GET  /credit-services/.../creditreport                ->  o relatorio

O login é HTTP Basic: usuário e senha codificados em base64 no cabeçalho Authorization. E aqui vai o primeiro detalhe que engana — o corpo do POST vai vazio. É intuitivo tentar mandar as credenciais num JSON no corpo, e não é assim que funciona: elas viajam no cabeçalho, e o corpo é uma string vazia mesmo.

// O login e HTTP Basic: usuario e senha em base64 no cabecalho Authorization.
// O corpo do POST vai VAZIO de proposito - as credenciais nao vao no corpo.
async function fazerLogin() {
  const usuario = process.env.SERASA_USUARIO;
  const senha = process.env.SERASA_SENHA;

  if (!usuario || !senha) {
    throw new Error("Defina SERASA_USUARIO e SERASA_SENHA no ambiente.");
  }

  const basico = Buffer.from(usuario + ":" + senha).toString("base64");

  const resposta = await fetch(BASE + "/security/iam/v1/client-identities/login", {
    method: "POST",
    headers: {
      accept: "application/json",
      "content-type": "application/json",
      authorization: "Basic " + basico,
    },
    body: "",
    signal: AbortSignal.timeout(TEMPO_LIMITE),
  });

  const corpo = await resposta.json();

  if (!resposta.ok) {
    throw new Error("Login recusado: " + lerMensagemDeErro(corpo, resposta.status));
  }

Repare que as credenciais saem de process.env. Isso não é frescura de segurança: usuário e senha do Serasa dão acesso a consultar dado de pessoa e a gastar dinheiro na sua conta. Nunca, em hipótese nenhuma, deixe isso escrito no meio do arquivo — nem "só para testar agora e trocar depois", porque o "depois" tem um jeito curioso de virar um commit no repositório. 😬

🧹 Limpando o documento antes de enviar

O Serasa quer o documento só com dígitos. Se você mandar 529.982.247-25 com a pontuação, a consulta não acha ninguém. Uma linha resolve:

// Tira ponto, traco, barra e espaco. O Serasa quer so os digitos.
function limparDocumento(documento) {
  return String(documento).replace(/\D/g, "");
}

O \D é "tudo que não for dígito" — então ponto, traço, barra e espaço somem de uma vez. Simples, mas tem uma armadilha grudada nele, e essa é traiçoeira de verdade. 🙈

É o tipo de bug que passa despercebido por meses, porque só quebra para uma parte dos seus clientes. Todos os outros funcionam lindamente e ninguém desconfia de nada.

Com o documento limpo, dá para decidir o tipo pelo tamanho — e é essa decisão que escolhe qual endpoint chamar depois:

// CPF tem 11 digitos, CNPJ tem 14. Passe SEMPRE texto, nunca numero:
// Number("01234567890") vira 1234567890 e o zero da frente some.
function tipoDoDocumento(documento) {
  const limpo = limparDocumento(documento);

  if (limpo.length === 11) {
    return "CPF";
  }
  if (limpo.length === 14) {
    return "CNPJ";
  }
  return "INVALIDO";

🧮 Confira o dígito verificador antes de gastar consulta

Essa parte não é exigência do Serasa, é economia. Como toda consulta é cobrada, inclusive a que não encontra ninguém, checar o dígito verificador aqui na sua máquina — de graça, em microssegundos — evita pagar por um documento que já dava para saber que estava errado.

A conta do CPF é conhecida: dois dígitos, cada um calculado sobre os anteriores com pesos decrescentes.


// Digito verificador do CPF. Vale a pena conferir antes de gastar consulta:
// documento errado tambem e cobrado.
function cpfEhValido(documento) {
  const cpf = limparDocumento(documento);

  if (cpf.length !== 11) {
    return false;
  }
  // "11111111111" passa na conta dos digitos, mas nao existe. Barre na mao.
  if (/^(\d)\1{10}$/.test(cpf)) {
    return false;
  }

  for (let rodada = 0; rodada < 2; rodada++) {
    const ate = 9 + rodada;
    let soma = 0;

    for (let i = 0; i < ate; i++) {
      soma = soma + Number(cpf[i]) * (ate + 1 - i);
    }

    let digito = (soma * 10) % 11;
    if (digito === 10) {
      digito = 0;
    }
    if (digito !== Number(cpf[ate])) {
      return false;
    }
  }

Aquele if da expressão regular no meio é obrigatório, e é o detalhe que quase todo mundo esquece: 111.111.111-11 passa na conta dos dígitos. A matemática aprova, a realidade não — sequência repetida não é CPF válido, e precisa ser barrada na mão.

Para CNPJ a lógica é a mesma, mudando os pesos, que vão de 2 a 9 e recomeçam:

}

// Digito verificador do CNPJ. Os pesos vao de 2 a 9 e recomecam.
function cnpjEhValido(documento) {
  const cnpj = limparDocumento(documento);

  if (cnpj.length !== 14) {
    return false;
  }
  if (/^(\d)\1{13}$/.test(cnpj)) {
    return false;
  }

  for (let rodada = 0; rodada < 2; rodada++) {
    const ate = 12 + rodada;
    let peso = 2;
    let soma = 0;

    for (let i = ate - 1; i >= 0; i--) {
      soma = soma + Number(cnpj[i]) * peso;
      peso = peso + 1;
      if (peso > 9) {
        peso = 2;
      }
    }

    let digito = soma % 11;
    if (digito < 2) {
      digito = 0;
    } else {
      digito = 11 - digito;
    }
    if (digito !== Number(cnpj[ate])) {
      return false;
    }
  }

📮 O documento vai no cabeçalho, não na URL

Essa foi a que me fez reler a documentação três vezes. 😅 Por instinto a gente procura o documento na URL (?cpf=...) ou no corpo de um POST. Não é nem um nem outro: o documento viaja num cabeçalho chamado X-Document-Id, e a requisição é um GET.

E o caminho muda conforme o tipo — pessoa física e pessoa jurídica têm endpoints e nomes de relatório diferentes:

}

// O documento NAO vai na URL nem no corpo: vai no cabecalho X-Document-Id.
// PF e PJ tem caminho e nome de relatorio diferentes.
async function consultarDocumento(documento) {
  const limpo = limparDocumento(documento);
  const tipo = tipoDoDocumento(limpo);

  if (tipo === "INVALIDO") {
    throw new Error("Documento precisa ter 11 (CPF) ou 14 (CNPJ) digitos.");
  }
  if (tipo === "CPF" && !cpfEhValido(limpo)) {
    throw new Error("CPF com digito verificador invalido.");
  }
  if (tipo === "CNPJ" && !cnpjEhValido(limpo)) {
    throw new Error("CNPJ com digito verificador invalido.");
  }

  const token = await fazerLogin();

  let caminho;
  if (tipo === "CPF") {
    caminho =
      "/credit-services/person-information-report/v1/creditreport" +
      "?reportName=RELATORIO_AVANCADO_TOP_SCORE_PF_PME";
  } else {
    caminho =
      "/credit-services/business-information-report/v1/reports" +
      "?reportName=RELATORIO_AVANCADO_PJ_PME";
  }

  const resposta = await fetch(BASE + caminho, {
    method: "GET",
    headers: {
      accept: "application/json",
      authorization: "Bearer " + token,
      "X-Document-Id": limpo,
    },
    signal: AbortSignal.timeout(TEMPO_LIMITE),
  });

  const corpo = await resposta.json();

  if (!resposta.ok) {

O reportName na query string é o que define qual relatório você está comprando. Nome errado ali costuma responder erro de autorização, o que manda você caçar problema de credencial quando o problema era só o nome do produto. 🤦‍♀️

🎭 O erro chega em dois formatos diferentes

Aqui está uma das coisas que só se descobre apanhando. Quando dá errado, o Serasa às vezes devolve um objeto com message, e às vezes devolve uma lista de objetos com message:

{ "message": "..." }            <- as vezes assim
[ { "message": "..." } ]          <- e as vezes assim

Se você ler só um dos dois, metade dos erros vira undefined na sua tela e você fica sem a menor pista do que aconteceu. A função que cobre os dois casos é bem chata e bem curta:

    throw new Error("Login respondeu 200 mas sem accessToken.");
  }

  return corpo.accessToken;
}

// O erro do Serasa chega em dois formatos: as vezes um objeto com "message",
// as vezes uma LISTA de objetos. Ler so um dos dois deixa o erro virar
// "undefined" e voce fica sem saber o que aconteceu.
function lerMensagemDeErro(corpo, status) {
  if (Array.isArray(corpo) && corpo.length > 0 && corpo[0].message) {
    return corpo[0].message;

E tem uma mensagem específica que merece tratamento próprio, porque ela mente sobre o que está acontecendo:

[ERROR][USER-NOT-AUTHORIZED] Transacoes negadas: [BPCA]

Lendo isso, qualquer pessoa vai conferir usuário, senha, token, permissão de perfil. Eu conferi. Não é nada disso: na prática, essa mensagem quase sempre significa fatura em aberto na conta do Serasa. O acesso está certo, o que está faltando é pagamento. Por isso o código traduz a mensagem para algo que aponta para o lugar certo:


    // Esta mensagem parece problema de permissao, mas quase sempre e
    // fatura em aberto na conta do Serasa.
    if (mensagem.indexOf("USER-NOT-AUTHORIZED") !== -1) {
      throw new Error("Consulta negada. Verifique o pagamento da conta Serasa.");
    }
    throw new Error(mensagem);
  }

  const relatorio = corpo.reports && corpo.reports[0];

  // ARMADILHA: HTTP 200 com relatorio vazio. O documento nao foi encontrado,

😱 O 200 que não quer dizer sucesso

Se você só for ler um pedaço deste artigo, leia este. 🙏

Quando o documento não existe na base, o Serasa não responde 404. Ele responde HTTP 200, com um JSON que tem a estrutura toda montadinha — e vazia por dentro. O reports[0] chega lá, direitinho, sem reportName, sem nada.

O estrago disso é grande: resposta.ok é true, seu try não estoura, e o código segue feliz contando pendências num objeto vazio. Resultado? Zero pendências. Ou seja: "documento não encontrado" se disfarça de "nada consta", que é exatamente o oposto — um é ausência de informação, o outro é uma aprovação. 😳

  // "nao encontrado" como se fosse "nada consta".
  if (!relatorio || !relatorio.reportName) {
    throw new Error("Documento nao encontrado na base do Serasa.");
  }

  return relatorio;
}

Esse if de três linhas é a diferença entre aprovar um contrato com informação e aprovar um contrato com silêncio.

📊 Lendo o resultado: as listas vêm vazias, não ausentes

As restrições vêm agrupadas dentro de negativeData, cada uma com um nome próprio. Um relatório limpo traz todas as listas presentes e vazias — e não ausentes, que é uma diferença importante na hora de contar:

// por isso o "|| []": sem ele um campo que nao veio quebra o .length.
function resumirRestricoes(relatorio) {
  const negativos = relatorio.negativeData || {};

  function contar(grupo, lista) {
    if (!negativos[grupo]) {
      return 0;
    }
    return (negativos[grupo][lista] || []).length;
  }

  const resumo = {
    pefin: contar("pefin", "pefinResponse"),
    refin: contar("refin", "refinResponse"),
    protestos: contar("notary", "notaryResponse"),
    cheques: contar("check", "checkResponse"),
    dividas: contar("collectionRecords", "collectionRecordsResponse"),
  };

  resumo.total =
    resumo.pefin + resumo.refin + resumo.protestos + resumo.cheques + resumo.dividas;

  return resumo;

Aquele || [] é a rede de proteção: quando um grupo não vem no retorno, sem ele o .length estoura com Cannot read properties of undefined e derruba a consulta inteira por causa de um campo que só faltou.

Os nomes dos grupos são os do mercado de crédito, e vale a tradução:

Campo no JSONO que é
pefinDívida vencida com instituição financeira
refinDívida vencida já renegociada e vencida de novo
notaryProtesto em cartório
checkCheque sem fundo
collectionRecordsDívida em cobrança
score.scoreA pontuação de crédito, de 0 a 1000

⏱️ O relatório é lento — não aperte o timeout

Esse relatório demora. Não é uma consulta de chave-valor: o Serasa monta o documento consolidando várias fontes, e um minuto de espera é normal. Prepare-se para esperar. 😴

const TEMPO_LIMITE = 60000; // 60s. O relatorio e lento, nao diminua isso.

A tentação de baixar isso para 10 segundos "porque nenhuma API decente demora tanto" é grande, e o preço é caro: você paga a consulta (ela roda lá do lado deles), aborta antes da resposta chegar e fica sem o dado. Pagou e não levou.

Um detalhe do fetch ao usar AbortSignal.timeout(): o estouro chega no catch como um erro de nome TimeoutError, com uma mensagem genérica que não ajuda ninguém. Vale traduzir:

      console.log("Situacao:   " + restricoes.total + " restricao(oes)");
    }
  } catch (erro) {
    // O timeout do fetch chega como AbortError, com mensagem pouco util.
    if (erro.name === "TimeoutError" || erro.name === "AbortError") {
      console.error(
        "Erro: o Serasa demorou mais de " + TEMPO_LIMITE / 1000 + "s para responder."
      );

📄 O arquivo inteiro

É este o script completo, do jeito que ele roda — um arquivo só, de cima para baixo, sem nenhuma dependência externa:

// Consulta de CPF / CNPJ no Serasa Experian.
// Node.js 18 ou mais novo (usa o fetch embutido). Nenhuma dependencia externa.
//
// As credenciais saem SEMPRE de variavel de ambiente. Nunca escreva
// usuario e senha aqui dentro.
//
//   SERASA_USUARIO=...  SERASA_SENHA=...  node serasa.js 12345678909

// Ambiente de homologacao ("uat-") e producao tem endereco diferente.
// Comece pelo de homologacao: o de producao cobra por consulta.
const BASE = process.env.SERASA_BASE || "https://uat-api.serasaexperian.com.br";

const TEMPO_LIMITE = 60000; // 60s. O relatorio e lento, nao diminua isso.

// Tira ponto, traco, barra e espaco. O Serasa quer so os digitos.
function limparDocumento(documento) {
  return String(documento).replace(/\D/g, "");
}

// CPF tem 11 digitos, CNPJ tem 14. Passe SEMPRE texto, nunca numero:
// Number("01234567890") vira 1234567890 e o zero da frente some.
function tipoDoDocumento(documento) {
  const limpo = limparDocumento(documento);

  if (limpo.length === 11) {
    return "CPF";
  }
  if (limpo.length === 14) {
    return "CNPJ";
  }
  return "INVALIDO";
}

// Digito verificador do CPF. Vale a pena conferir antes de gastar consulta:
// documento errado tambem e cobrado.
function cpfEhValido(documento) {
  const cpf = limparDocumento(documento);

  if (cpf.length !== 11) {
    return false;
  }
  // "11111111111" passa na conta dos digitos, mas nao existe. Barre na mao.
  if (/^(\d)\1{10}$/.test(cpf)) {
    return false;
  }

  for (let rodada = 0; rodada < 2; rodada++) {
    const ate = 9 + rodada;
    let soma = 0;

    for (let i = 0; i < ate; i++) {
      soma = soma + Number(cpf[i]) * (ate + 1 - i);
    }

    let digito = (soma * 10) % 11;
    if (digito === 10) {
      digito = 0;
    }
    if (digito !== Number(cpf[ate])) {
      return false;
    }
  }

  return true;
}

// Digito verificador do CNPJ. Os pesos vao de 2 a 9 e recomecam.
function cnpjEhValido(documento) {
  const cnpj = limparDocumento(documento);

  if (cnpj.length !== 14) {
    return false;
  }
  if (/^(\d)\1{13}$/.test(cnpj)) {
    return false;
  }

  for (let rodada = 0; rodada < 2; rodada++) {
    const ate = 12 + rodada;
    let peso = 2;
    let soma = 0;

    for (let i = ate - 1; i >= 0; i--) {
      soma = soma + Number(cnpj[i]) * peso;
      peso = peso + 1;
      if (peso > 9) {
        peso = 2;
      }
    }

    let digito = soma % 11;
    if (digito < 2) {
      digito = 0;
    } else {
      digito = 11 - digito;
    }
    if (digito !== Number(cnpj[ate])) {
      return false;
    }
  }

  return true;
}

// O login e HTTP Basic: usuario e senha em base64 no cabecalho Authorization.
// O corpo do POST vai VAZIO de proposito - as credenciais nao vao no corpo.
async function fazerLogin() {
  const usuario = process.env.SERASA_USUARIO;
  const senha = process.env.SERASA_SENHA;

  if (!usuario || !senha) {
    throw new Error("Defina SERASA_USUARIO e SERASA_SENHA no ambiente.");
  }

  const basico = Buffer.from(usuario + ":" + senha).toString("base64");

  const resposta = await fetch(BASE + "/security/iam/v1/client-identities/login", {
    method: "POST",
    headers: {
      accept: "application/json",
      "content-type": "application/json",
      authorization: "Basic " + basico,
    },
    body: "",
    signal: AbortSignal.timeout(TEMPO_LIMITE),
  });

  const corpo = await resposta.json();

  if (!resposta.ok) {
    throw new Error("Login recusado: " + lerMensagemDeErro(corpo, resposta.status));
  }

  if (!corpo.accessToken) {
    throw new Error("Login respondeu 200 mas sem accessToken.");
  }

  return corpo.accessToken;
}

// O erro do Serasa chega em dois formatos: as vezes um objeto com "message",
// as vezes uma LISTA de objetos. Ler so um dos dois deixa o erro virar
// "undefined" e voce fica sem saber o que aconteceu.
function lerMensagemDeErro(corpo, status) {
  if (Array.isArray(corpo) && corpo.length > 0 && corpo[0].message) {
    return corpo[0].message;
  }
  if (corpo && corpo.message) {
    return corpo.message;
  }
  return "HTTP " + status;
}

// O documento NAO vai na URL nem no corpo: vai no cabecalho X-Document-Id.
// PF e PJ tem caminho e nome de relatorio diferentes.
async function consultarDocumento(documento) {
  const limpo = limparDocumento(documento);
  const tipo = tipoDoDocumento(limpo);

  if (tipo === "INVALIDO") {
    throw new Error("Documento precisa ter 11 (CPF) ou 14 (CNPJ) digitos.");
  }
  if (tipo === "CPF" && !cpfEhValido(limpo)) {
    throw new Error("CPF com digito verificador invalido.");
  }
  if (tipo === "CNPJ" && !cnpjEhValido(limpo)) {
    throw new Error("CNPJ com digito verificador invalido.");
  }

  const token = await fazerLogin();

  let caminho;
  if (tipo === "CPF") {
    caminho =
      "/credit-services/person-information-report/v1/creditreport" +
      "?reportName=RELATORIO_AVANCADO_TOP_SCORE_PF_PME";
  } else {
    caminho =
      "/credit-services/business-information-report/v1/reports" +
      "?reportName=RELATORIO_AVANCADO_PJ_PME";
  }

  const resposta = await fetch(BASE + caminho, {
    method: "GET",
    headers: {
      accept: "application/json",
      authorization: "Bearer " + token,
      "X-Document-Id": limpo,
    },
    signal: AbortSignal.timeout(TEMPO_LIMITE),
  });

  const corpo = await resposta.json();

  if (!resposta.ok) {
    const mensagem = lerMensagemDeErro(corpo, resposta.status);

    // Esta mensagem parece problema de permissao, mas quase sempre e
    // fatura em aberto na conta do Serasa.
    if (mensagem.indexOf("USER-NOT-AUTHORIZED") !== -1) {
      throw new Error("Consulta negada. Verifique o pagamento da conta Serasa.");
    }
    throw new Error(mensagem);
  }

  const relatorio = corpo.reports && corpo.reports[0];

  // ARMADILHA: HTTP 200 com relatorio vazio. O documento nao foi encontrado,
  // mas o Serasa responde 200 mesmo assim. Sem este if, voce trata
  // "nao encontrado" como se fosse "nada consta".
  if (!relatorio || !relatorio.reportName) {
    throw new Error("Documento nao encontrado na base do Serasa.");
  }

  return relatorio;
}

// Conta as restricoes. Um relatorio limpo traz as listas VAZIAS, nao ausentes -
// por isso o "|| []": sem ele um campo que nao veio quebra o .length.
function resumirRestricoes(relatorio) {
  const negativos = relatorio.negativeData || {};

  function contar(grupo, lista) {
    if (!negativos[grupo]) {
      return 0;
    }
    return (negativos[grupo][lista] || []).length;
  }

  const resumo = {
    pefin: contar("pefin", "pefinResponse"),
    refin: contar("refin", "refinResponse"),
    protestos: contar("notary", "notaryResponse"),
    cheques: contar("check", "checkResponse"),
    dividas: contar("collectionRecords", "collectionRecordsResponse"),
  };

  resumo.total =
    resumo.pefin + resumo.refin + resumo.protestos + resumo.cheques + resumo.dividas;

  return resumo;
}

async function main() {
  const documento = process.argv[2];

  if (!documento) {
    console.log("Uso: node serasa.js <cpf ou cnpj>");
    return;
  }

  try {
    const relatorio = await consultarDocumento(documento);
    const restricoes = resumirRestricoes(relatorio);
    const score = (relatorio.score && relatorio.score.score) || 0;

    console.log("Relatorio:  " + relatorio.reportName);
    console.log("Score:      " + score);
    console.log("PEFIN:      " + restricoes.pefin);
    console.log("REFIN:      " + restricoes.refin);
    console.log("Protestos:  " + restricoes.protestos);
    console.log("Cheques:    " + restricoes.cheques);
    console.log("Dividas:    " + restricoes.dividas);

    if (restricoes.total === 0) {
      console.log("Situacao:   nada consta");
    } else {
      console.log("Situacao:   " + restricoes.total + " restricao(oes)");
    }
  } catch (erro) {
    // O timeout do fetch chega como AbortError, com mensagem pouco util.
    if (erro.name === "TimeoutError" || erro.name === "AbortError") {
      console.error(
        "Erro: o Serasa demorou mais de " + TEMPO_LIMITE / 1000 + "s para responder."
      );
    } else {
      console.error("Erro: " + erro.message);
    }
    process.exitCode = 1;
  }
}

// Só roda a CLI quando o arquivo é chamado direto. Sem isto, quem der
// require() neste arquivo recebe a mensagem de uso na tela.
if (require.main === module) {
  main();
}

Para rodar, as credenciais vão pelo ambiente:

SERASA_USUARIO=seu_usuario SERASA_SENHA=sua_senha node serasa.js 529.982.247-25

🎯 O que sai na tela

Um documento fictício sem nenhuma restrição imprime assim — a pontuação alta e todos os contadores zerados:

Relatorio:  RELATORIO_AVANCADO_TOP_SCORE_PF_PME
Score:      782
PEFIN:      0
REFIN:      0
Protestos:  0
Cheques:    0
Dividas:    0
Situacao:   nada consta

E um com pendências, onde o interessante é ver a pontuação despencar junto:

Relatorio:  RELATORIO_AVANCADO_PJ_PME
Score:      212
PEFIN:      2
REFIN:      1
Protestos:  0
Cheques:    0
Dividas:    0
Situacao:   3 restricao(oes)

Os caminhos de erro são o que mais importa no dia a dia, e cada um diz o que fazer:

Erro: Documento nao encontrado na base do Serasa.
Erro: Consulta negada. Verifique o pagamento da conta Serasa.
Erro: CPF com digito verificador invalido.
Erro: Defina SERASA_USUARIO e SERASA_SENHA no ambiente.

Repare que o do dígito verificador nem chega a sair da sua máquina — ele é barrado antes da chamada, e essa consulta não entra na fatura. 💸

🧷 Três detalhes pequenos que salvam a pele

Fechando com o que eu gostaria de ter lido antes de começar:

Trate documento como texto, sempre. A tentação de guardar CPF como número no banco é real e o prejuízo aparece só nos documentos que começam com zero — tarde demais, e só para alguns clientes.

Comece pelo ambiente de homologação. É o mesmo código, muda só o endereço (uat-api em vez de api), e ali você erra à vontade sem cada tentativa virar linha na fatura.

Guarde o retorno inteiro. O relatório traz muito mais do que a pontuação e os contadores que este script lê, e refazer a consulta para pegar um campo que você já tinha recebido é pagar duas vezes pela mesma informação. Salve o JSON completo e leia dele depois.

No fim das contas, a parte difícil da integração com o Serasa não foi montar a requisição — foi desconfiar do sucesso. Um 200 que não é sucesso, uma mensagem de autorização que fala de pagamento, uma lista vazia que parece aprovação: o retorno da API tem mais opinião do que parece, e ler ele com atenção é o trabalho de verdade. 🔍

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

Leia também