Pular para o conteúdo
Node.js

Node.js: consultando CPF e CNPJ no SPC

Paloma Macetko
Ilustracao de um unicornio diante de um cofre de cristal com um medidor de pontuacao, ao lado de uma coruja segurando um pergaminho carimbado

Olá meus Unicórnios! 🦄✨

Sabe quando você abre a documentação de uma integração e a primeira palavra que aparece é SOAP? 😅 Pois é. Foi assim que começou a minha conversa com o SPC Brasil — aquele serviço que responde se um CPF ou CNPJ tem restrição, e com que pontuação.

Meu primeiro impulso foi procurar uma biblioteca de SOAP para Node.js. Instalei, li o README, briguei com o WSDL por um tempo... e então percebi uma coisa que mudou o artigo inteiro: eu não precisava de nada disso. Uma consulta ao SPC é um POST com um XML no corpo e um cabeçalho de autenticação. O fetch que já vem no Node dá conta, e o envelope é uma string.

Então é isso que este tutorial faz: consulta de CPF e CNPJ no SPC, com Node.js puro, zero dependência. Um arquivo só, lido de cima para baixo. E, pelo caminho, quatro armadilhas que me custaram tempo — uma delas é o bug mais comum de quem integra com serviço de crédito, e aposto que você já cometeu. 👀

🧭 O desenho da coisa

Antes do código, o mapa. Uma consulta ao SPC tem quatro etapas, e três delas acontecem antes de sair da sua máquina:

documento digitado ("529.982.247-25")
        ↓  limpar        tira ponto, traço e barra
        ↓  classificar   11 dígitos = CPF, 14 = CNPJ
        ↓  validar       confere os dígitos verificadores
        ↓  consultar     POST com o envelope SOAP
                              ↓
                  score + pendências + classificação

Repare que a validação vem antes da consulta. Isso não é preciosismo: cada chamada ao SPC é cobrada. Se o documento tem um dígito errado de digitação, o certo é descobrir isso de graça, na sua máquina, e não pagar para o serviço dizer o mesmo. 💸

🔑 A autenticação é mais simples do que parece

O SPC autentica por usuário e senha, no bom e velho HTTP Basic. As bibliotecas de SOAP escondem isso atrás de opções de configuração, mas por baixo é só um cabeçalho com os dois valores juntos, em Base64:

// Usuario e senha viajam no cabecalho Authorization, em Base64.
function montarAutenticacao(usuario, senha) {
    const juntos = usuario + ':' + senha;
    const base64 = Buffer.from(juntos, 'utf8').toString('base64');
    return 'Basic ' + base64;
}

Três linhas. É literalmente isso que a biblioteca faria por você. E aqui vale a regra que eu não abro mão: usuário e senha saem do ambiente, nunca do arquivo.

// As credenciais saem SEMPRE do ambiente. Nunca escreva usuario e senha aqui.
const USUARIO = process.env.SPC_USUARIO;
const SENHA = process.env.SPC_SENHA;
const ENDERECO = process.env.SPC_ENDERECO;

Nada de deixar a credencial no código "para trocar depois". O depois não vem, e aí ela já foi para o repositório. 🙈

🧹 A limpeza que evita a recusa

Esta foi a primeira pedra no caminho. O usuário digita 529.982.247-25, com a máscara bonitinha do formulário — e o SPC recusa. Ele quer só os dígitos.

// Tira ponto, traco e barra. O SPC recusa o documento formatado.
function limparDocumento(documento) {
    let limpo = '';
    for (let i = 0; i < documento.length; i++) {
        const caractere = documento[i];
        if (caractere >= '0' && caractere <= '9') {
            limpo = limpo + caractere;
        }
    }
    return limpo;
}

Um for simples, que mantém apenas o que está entre 0 e 9. Poderia ser uma expressão regular de uma linha, e eu quase escrevi assim — mas repare no que este formato me deu de graça: ele devolve texto. Guarde essa informação por cinco minutos, porque ela é o coração da próxima seção. 🎯

Com o documento limpo, descobrir o tipo é contar caracteres:

// O tipo vai junto do documento: 'F' para CPF, 'J' para CNPJ.
function descobrirTipoPessoa(documento) {
    if (documento.length === 11) {
        return 'F';
    }
    if (documento.length === 14) {
        return 'J';
    }
    return '';
}

0️⃣ A armadilha do zero à esquerda

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

Existe uma tentação enorme de tratar documento como número. Faz sentido, né? São dígitos. Só que CPF não é número, é identificador — e a diferença aparece exatamente nos documentos que começam com zero.

Rodei o teste com um CPF fictício que começa com três zeros:

Digitado          : 000.100.000-46
Limpo como texto  : 00010000046  (11 dígitos)
Passado por Number: 10000046  (8 dígitos)
Válido?           : true

Isso mesmo, oito dígitos. 🤯 Três zeros simplesmente evaporaram. E o pior: nenhum erro, nenhum aviso, nada. O script segue feliz e manda oito dígitos para o SPC, que responde que o documento é inválido — e você vai passar a tarde procurando o defeito na integração, quando ele está num Number() inocente lá atrás.

É por isso que a limparDocumento monta uma string caractere a caractere, e é por isso que ela nunca chama parseInt. Uma decisão de três linhas que evita um bug de tarde inteira.

✅ Validar antes de gastar a consulta

Os dígitos verificadores do CPF são uma continha de pesos. O que quase todo mundo esquece é o caso do documento com todos os dígitos iguais:

// Confere os digitos verificadores do CPF antes de gastar uma consulta paga.
function cpfEhValido(cpf) {
    if (cpf.length !== 11) {
        return false;
    }
    // 111.111.111-11 passa na conta dos digitos, entao precisa deste corte.
    let todosIguais = true;
    for (let i = 1; i < 11; i++) {
        if (cpf[i] !== cpf[0]) {
            todosIguais = false;
        }
    }
    if (todosIguais) {
        return false;
    }
    let soma = 0;
    for (let i = 0; i < 9; i++) {
        soma = soma + Number(cpf[i]) * (10 - i);
    }
    let primeiro = (soma * 10) % 11;
    if (primeiro === 10) {
        primeiro = 0;
    }
    if (primeiro !== Number(cpf[9])) {
        return false;
    }
    soma = 0;
    for (let i = 0; i < 10; i++) {
        soma = soma + Number(cpf[i]) * (11 - i);
    }
    let segundo = (soma * 10) % 11;
    if (segundo === 10) {
        segundo = 0;
    }
    if (segundo !== Number(cpf[10])) {
        return false;
    }
    return true;
}

Aquele bloco do todosIguais parece paranoia, mas não é. 111.111.111-11 passa na conta dos dígitos verificadores — a matemática fecha direitinho. Sem esse corte, o clássico "111111111 11" que as pessoas digitam para testar formulário seria aceito como documento legítimo e viraria uma consulta paga. 😳

O CNPJ segue a mesma ideia, mudando só os pesos, que voltam para 9 depois de chegarem ao 2:

// O CNPJ usa pesos que voltam para 9 depois do 2.
function cnpjEhValido(cnpj) {
    if (cnpj.length !== 14) {
        return false;
    }
    let todosIguais = true;
    for (let i = 1; i < 14; i++) {
        if (cnpj[i] !== cnpj[0]) {
            todosIguais = false;
        }
    }
    if (todosIguais) {
        return false;
    }
    const pesosPrimeiro = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
    let soma = 0;
    for (let i = 0; i < 12; i++) {
        soma = soma + Number(cnpj[i]) * pesosPrimeiro[i];
    }
    let resto = soma % 11;
    let primeiro = 0;
    if (resto >= 2) {
        primeiro = 11 - resto;
    }
    if (primeiro !== Number(cnpj[12])) {
        return false;
    }
    const pesosSegundo = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
    soma = 0;
    for (let i = 0; i < 13; i++) {
        soma = soma + Number(cnpj[i]) * pesosSegundo[i];
    }
    resto = soma % 11;
    let segundo = 0;
    if (resto >= 2) {
        segundo = 11 - resto;
    }
    if (segundo !== Number(cnpj[13])) {
        return false;
    }
    return true;
}

📨 O envelope SOAP é uma string, e pronto

Aqui está a parte que me fez abandonar a biblioteca. O envelope que o SPC espera é este:

// O envelope e' texto puro. Repare que o documento vai entre tags,
// como string -- por isso o zero a esquerda do CPF sobrevive.
function montarEnvelope(documento, tipoPessoa) {
    return '<?xml version="1.0" encoding="UTF-8"?>' +
        '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:con="http://consulta.ws.remoting.spc.com.br/">' +
        '<soapenv:Header/>' +
        '<soapenv:Body>' +
        '<con:consultar>' +
        '<codigo-produto>' + escaparXml(CODIGO_PRODUTO) + '</codigo-produto>' +
        '<tipo-consumidor>' + escaparXml(tipoPessoa) + '</tipo-consumidor>' +
        '<documento-consumidor>' + escaparXml(documento) + '</documento-consumidor>' +
        '</con:consultar>' +
        '</soapenv:Body>' +
        '</soapenv:Envelope>';
}

Escrever XML concatenando string tem má fama, e com razão — mas repare no detalhe que torna isso seguro aqui: o documento passa pela escaparXml antes de entrar, e a essa altura ele já foi limpo e só tem dígitos. Não há como injetar tag nenhuma por ali.

E repare no que eu comentei na linha: o documento entra entre tags, como texto. É o zero à esquerda sobrevivendo até o último metro. 🛡️

📖 Lendo a resposta sem parser de XML

A resposta do SPC é XML, e a tentação agora é instalar um parser. Mas a estrutura que interessa é rasa — meia dúzia de tags com valores simples — então uma expressão regular por tag resolve:

// Le o conteudo da primeira tag com esse nome. Serve para a resposta do
// SPC, que e' rasa: nao vale como parser de XML de proposito geral.
function lerTag(xml, nome) {
    const expressao = new RegExp('<' + nome + '[^>]*>([\\s\\S]*?)</' + nome + '>');
    const achado = xml.match(expressao);
    if (achado === null) {
        return '';
    }
    return achado[1].trim();
}

Preciso ser honesta sobre o que isso é e o que não é: não vale como parser de XML de propósito geral. Se as tags se repetissem em níveis diferentes, ou se houvesse namespace variando, isso quebraria. Para os campos de score e pendência do SPC, funciona e economiza uma dependência. Está escrito no comentário justamente para o próximo leitor não sair usando isso em outro lugar. 😉

💥 "Sem restrição" não é erro — o bug clássico

Chegamos ao erro que eu vejo com mais frequência em integração de crédito, e que é sutil o bastante para passar em revisão de código.

São três situações diferentes, e elas costumam ser tratadas como duas:

1. consultado, COM restrição   → HTTP 200, quantidade-total = 3
2. consultado, SEM restrição   → HTTP 200, quantidade-total = 0
3. documento não encontrado    → HTTP 500, soap:Fault

O caso 2 é uma consulta bem-sucedida. O serviço respondeu, cobrou, e a resposta é a melhor possível: a pessoa está limpa. Só que quem escreve if (!resultado.pendencias) { erro() } transforma o melhor cliente da fila num erro de sistema. 😱

    const score = lerTag(corpo, 'score');
    const tipoCliente = lerTag(corpo, 'tipo-cliente-score');
    const mensagem = lerTag(corpo, 'mesagem-interpretativa-score');
    const quantidadePendencias = lerTag(corpo, 'quantidade-total');

    // "Sem pendencia" e' consulta bem-sucedida, nao erro: o SPC responde
    // 200 com a contagem em zero. Tratar isso como falha e' o bug classico.
    let pendencias = 0;
    if (quantidadePendencias !== '') {
        pendencias = Number(quantidadePendencias);
    }

E o caso 3, esse sim, chega de um jeito que engana: HTTP 500, com a mensagem dentro de um soap:Fault. Ou seja, um documento que simplesmente não está na base devolve status de erro de servidor. Por isso a leitura da falha vem antes da checagem de status:

    if (resposta.status === 401 || resposta.status === 403) {
        throw new Error('Usuario ou senha do SPC recusados (HTTP ' + resposta.status + ').');
    }

    const falha = lerFalha(corpo);
    if (falha !== '') {
        throw new Error('O SPC recusou a consulta: ' + falha);
    }

    if (resposta.status !== 200) {
        throw new Error('O SPC respondeu HTTP ' + resposta.status + '.');
    }

Se eu conferisse status !== 200 primeiro, o usuário receberia "o SPC respondeu HTTP 500" em vez de "documento não encontrado na base" — tecnicamente verdade, e completamente inútil para quem está no atendimento. A ordem das checagens é a mensagem de erro.

⏱️ O tempo limite que ninguém lembra

Uma requisição sem prazo pode ficar pendurada até o fim dos tempos, segurando a tela do usuário. O Node moderno resolve isso com uma linha:

    // Sem o limite de tempo a consulta pode ficar pendurada para sempre.
    const relogio = AbortSignal.timeout(TEMPO_LIMITE);

    let resposta;
    try {
        resposta = await fetch(ENDERECO, {
            method: 'POST',
            headers: {
                'Content-Type': 'text/xml; charset=utf-8',
                'Authorization': montarAutenticacao(USUARIO, SENHA),
                'SOAPAction': 'consultar'
            },
            body: envelope,
            signal: relogio
        });
    } catch (erro) {
        if (erro.name === 'TimeoutError') {
            throw new Error('O SPC nao respondeu em ' + (TEMPO_LIMITE / 1000) + ' segundos.');
        }
        throw new Error('Falha de rede ao falar com o SPC: ' + erro.message);
    }

Trinta segundos, e o catch separa dois problemas que parecem um só: demorou demais é diferente de não consegui falar com o servidor. Quem está lendo o log agradece. 🙏

🎬 O script rodando

Para exercitar os caminhos sem gastar consulta paga, o script conversou com um servidor de mentira — vinte linhas de http.createServer respondendo no formato das tags reais. Primeiro, um CPF fictício sem restrição:

node consulta-spc.js 529.982.247-25
Documento .....: 52998224725
Tipo ..........: CPF
Score .........: 812
Classificacao .: NAO_RESTRITO
Leitura .......: Risco baixo de inadimplencia
Pendencias ....: 0
Situacao ......: SEM RESTRICAO

Repare: Pendencias: 0 e mesmo assim o script diz SEM RESTRICAO, não "erro". É o bug da seção anterior não acontecendo. 🎉

Agora um documento com pendências:

Documento .....: 11144477735
Tipo ..........: CPF
Score .........: 214
Classificacao .: RESTRITO
Leitura .......: Risco alto de inadimplencia
Pendencias ....: 3
Situacao ......: COM RESTRICAO

E os caminhos que dão errado, que são os que realmente importam:

Nao deu certo: O SPC recusou a consulta: Documento nao encontrado na base
Nao deu certo: CPF invalido: 12345678900
Nao deu certo: Documento precisa ter 11 digitos (CPF) ou 14 (CNPJ).
Nao deu certo: Usuario ou senha do SPC recusados (HTTP 401).
Nao deu certo: Defina SPC_USUARIO, SPC_SENHA e SPC_ENDERECO no ambiente.
Nao deu certo: Falha de rede ao falar com o SPC: fetch failed

Seis mensagens, seis problemas distintos, cada uma dizendo em português o que houve. Nenhuma delas é [object Object] nem uma pilha de erro de 40 linhas. 💛

🧾 O arquivo inteiro

É este o arquivo que produziu todas as saídas acima — sem classe, sem herança, sem camada de abstração. Função solta e um main() no fim:

// Consulta de CPF/CNPJ no SPC Brasil (SOAP) usando apenas o Node.js puro.
// Nada de biblioteca externa: o envelope SOAP e' uma string e a resposta
// e' lida com expressoes regulares simples.

// As credenciais saem SEMPRE do ambiente. Nunca escreva usuario e senha aqui.
const USUARIO = process.env.SPC_USUARIO;
const SENHA = process.env.SPC_SENHA;
const ENDERECO = process.env.SPC_ENDERECO;

const CODIGO_PRODUTO = '325';
const TEMPO_LIMITE = 30000;

// Tira ponto, traco e barra. O SPC recusa o documento formatado.
function limparDocumento(documento) {
    let limpo = '';
    for (let i = 0; i < documento.length; i++) {
        const caractere = documento[i];
        if (caractere >= '0' && caractere <= '9') {
            limpo = limpo + caractere;
        }
    }
    return limpo;
}

// O tipo vai junto do documento: 'F' para CPF, 'J' para CNPJ.
function descobrirTipoPessoa(documento) {
    if (documento.length === 11) {
        return 'F';
    }
    if (documento.length === 14) {
        return 'J';
    }
    return '';
}

// Confere os digitos verificadores do CPF antes de gastar uma consulta paga.
function cpfEhValido(cpf) {
    if (cpf.length !== 11) {
        return false;
    }
    // 111.111.111-11 passa na conta dos digitos, entao precisa deste corte.
    let todosIguais = true;
    for (let i = 1; i < 11; i++) {
        if (cpf[i] !== cpf[0]) {
            todosIguais = false;
        }
    }
    if (todosIguais) {
        return false;
    }
    let soma = 0;
    for (let i = 0; i < 9; i++) {
        soma = soma + Number(cpf[i]) * (10 - i);
    }
    let primeiro = (soma * 10) % 11;
    if (primeiro === 10) {
        primeiro = 0;
    }
    if (primeiro !== Number(cpf[9])) {
        return false;
    }
    soma = 0;
    for (let i = 0; i < 10; i++) {
        soma = soma + Number(cpf[i]) * (11 - i);
    }
    let segundo = (soma * 10) % 11;
    if (segundo === 10) {
        segundo = 0;
    }
    if (segundo !== Number(cpf[10])) {
        return false;
    }
    return true;
}

// O CNPJ usa pesos que voltam para 9 depois do 2.
function cnpjEhValido(cnpj) {
    if (cnpj.length !== 14) {
        return false;
    }
    let todosIguais = true;
    for (let i = 1; i < 14; i++) {
        if (cnpj[i] !== cnpj[0]) {
            todosIguais = false;
        }
    }
    if (todosIguais) {
        return false;
    }
    const pesosPrimeiro = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
    let soma = 0;
    for (let i = 0; i < 12; i++) {
        soma = soma + Number(cnpj[i]) * pesosPrimeiro[i];
    }
    let resto = soma % 11;
    let primeiro = 0;
    if (resto >= 2) {
        primeiro = 11 - resto;
    }
    if (primeiro !== Number(cnpj[12])) {
        return false;
    }
    const pesosSegundo = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
    soma = 0;
    for (let i = 0; i < 13; i++) {
        soma = soma + Number(cnpj[i]) * pesosSegundo[i];
    }
    resto = soma % 11;
    let segundo = 0;
    if (resto >= 2) {
        segundo = 11 - resto;
    }
    if (segundo !== Number(cnpj[13])) {
        return false;
    }
    return true;
}

// Usuario e senha viajam no cabecalho Authorization, em Base64.
function montarAutenticacao(usuario, senha) {
    const juntos = usuario + ':' + senha;
    const base64 = Buffer.from(juntos, 'utf8').toString('base64');
    return 'Basic ' + base64;
}

// Escapa o que nao pode entrar cru dentro de uma tag XML.
function escaparXml(texto) {
    let saida = String(texto);
    saida = saida.split('&').join('&amp;');
    saida = saida.split('<').join('&lt;');
    saida = saida.split('>').join('&gt;');
    return saida;
}

// O envelope e' texto puro. Repare que o documento vai entre tags,
// como string -- por isso o zero a esquerda do CPF sobrevive.
function montarEnvelope(documento, tipoPessoa) {
    return '<?xml version="1.0" encoding="UTF-8"?>' +
        '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:con="http://consulta.ws.remoting.spc.com.br/">' +
        '<soapenv:Header/>' +
        '<soapenv:Body>' +
        '<con:consultar>' +
        '<codigo-produto>' + escaparXml(CODIGO_PRODUTO) + '</codigo-produto>' +
        '<tipo-consumidor>' + escaparXml(tipoPessoa) + '</tipo-consumidor>' +
        '<documento-consumidor>' + escaparXml(documento) + '</documento-consumidor>' +
        '</con:consultar>' +
        '</soapenv:Body>' +
        '</soapenv:Envelope>';
}

// Le o conteudo da primeira tag com esse nome. Serve para a resposta do
// SPC, que e' rasa: nao vale como parser de XML de proposito geral.
function lerTag(xml, nome) {
    const expressao = new RegExp('<' + nome + '[^>]*>([\\s\\S]*?)</' + nome + '>');
    const achado = xml.match(expressao);
    if (achado === null) {
        return '';
    }
    return achado[1].trim();
}

// O SPC devolve o erro dentro de um soap:Fault, com HTTP 500.
function lerFalha(xml) {
    const mensagem = lerTag(xml, 'faultstring');
    if (mensagem !== '') {
        return mensagem;
    }
    return '';
}

async function consultarDocumento(documentoBruto) {
    const documento = limparDocumento(documentoBruto);
    const tipoPessoa = descobrirTipoPessoa(documento);

    if (tipoPessoa === '') {
        throw new Error('Documento precisa ter 11 digitos (CPF) ou 14 (CNPJ).');
    }
    if (tipoPessoa === 'F' && cpfEhValido(documento) === false) {
        throw new Error('CPF invalido: ' + documento);
    }
    if (tipoPessoa === 'J' && cnpjEhValido(documento) === false) {
        throw new Error('CNPJ invalido: ' + documento);
    }
    if (!USUARIO || !SENHA || !ENDERECO) {
        throw new Error('Defina SPC_USUARIO, SPC_SENHA e SPC_ENDERECO no ambiente.');
    }

    const envelope = montarEnvelope(documento, tipoPessoa);

    // Sem o limite de tempo a consulta pode ficar pendurada para sempre.
    const relogio = AbortSignal.timeout(TEMPO_LIMITE);

    let resposta;
    try {
        resposta = await fetch(ENDERECO, {
            method: 'POST',
            headers: {
                'Content-Type': 'text/xml; charset=utf-8',
                'Authorization': montarAutenticacao(USUARIO, SENHA),
                'SOAPAction': 'consultar'
            },
            body: envelope,
            signal: relogio
        });
    } catch (erro) {
        if (erro.name === 'TimeoutError') {
            throw new Error('O SPC nao respondeu em ' + (TEMPO_LIMITE / 1000) + ' segundos.');
        }
        throw new Error('Falha de rede ao falar com o SPC: ' + erro.message);
    }

    const corpo = await resposta.text();

    if (resposta.status === 401 || resposta.status === 403) {
        throw new Error('Usuario ou senha do SPC recusados (HTTP ' + resposta.status + ').');
    }

    const falha = lerFalha(corpo);
    if (falha !== '') {
        throw new Error('O SPC recusou a consulta: ' + falha);
    }

    if (resposta.status !== 200) {
        throw new Error('O SPC respondeu HTTP ' + resposta.status + '.');
    }

    const score = lerTag(corpo, 'score');
    const tipoCliente = lerTag(corpo, 'tipo-cliente-score');
    const mensagem = lerTag(corpo, 'mesagem-interpretativa-score');
    const quantidadePendencias = lerTag(corpo, 'quantidade-total');

    // "Sem pendencia" e' consulta bem-sucedida, nao erro: o SPC responde
    // 200 com a contagem em zero. Tratar isso como falha e' o bug classico.
    let pendencias = 0;
    if (quantidadePendencias !== '') {
        pendencias = Number(quantidadePendencias);
    }

    return {
        documento: documento,
        tipoPessoa: tipoPessoa,
        score: Number(score),
        tipoCliente: tipoCliente,
        mensagem: mensagem,
        pendencias: pendencias,
        temRestricao: pendencias > 0
    };
}

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

    if (!documento) {
        console.log('Uso: node consulta-spc.js <cpf-ou-cnpj>');
        return;
    }

    try {
        const resultado = await consultarDocumento(documento);
        console.log('Documento .....: ' + resultado.documento);
        console.log('Tipo ..........: ' + (resultado.tipoPessoa === 'F' ? 'CPF' : 'CNPJ'));
        console.log('Score .........: ' + resultado.score);
        console.log('Classificacao .: ' + resultado.tipoCliente);
        console.log('Leitura .......: ' + resultado.mensagem);
        console.log('Pendencias ....: ' + resultado.pendencias);
        if (resultado.temRestricao) {
            console.log('Situacao ......: COM RESTRICAO');
        } else {
            console.log('Situacao ......: SEM RESTRICAO');
        }
    } catch (erro) {
        console.log('Nao deu certo: ' + erro.message);
        process.exitCode = 1;
    }
}

if (require.main === module) {
    main();
}

module.exports = { consultarDocumento, limparDocumento, cpfEhValido, cnpjEhValido, montarAutenticacao };

Para rodar, as credenciais vão no ambiente:

export SPC_USUARIO="seu-usuario"
export SPC_SENHA="sua-senha"
export SPC_ENDERECO="https://endereco-do-seu-contrato/consultaWebService"

node consulta-spc.js 529.982.247-25

O endereço muda conforme o ambiente do seu contrato — treinamento, homologação e produção são três endereços diferentes, e todos saem do mesmo SPC_ENDERECO. Trocar de ambiente é trocar uma variável, não editar código. ✨

🎁 O resumo das armadilhas

Quatro coisas que eu não sabia quando comecei e que agora ficam aqui registradas:

1. Documento é texto, nunca número. O Number() come o zero à esquerda sem avisar, e o CPF de 11 dígitos vira um de 8.

2. Valide antes de consultar. Sai de graça na sua máquina e evita pagar para o SPC dizer que o dígito está errado — e não esqueça o corte dos dígitos todos iguais.

3. "Sem restrição" é sucesso. Zero pendências é a melhor resposta possível, não um caso de erro.

4. Documento não encontrado chega como HTTP 500. Leia o soap:Fault antes de olhar o código de status, senão a mensagem que sobra é inútil.

E a maior de todas, que nem é sobre o SPC: nem toda integração SOAP precisa de biblioteca de SOAP. Um POST, um cabeçalho e uma string de XML resolveram aqui, e o resultado é um arquivo que qualquer pessoa lê de cima a baixo sem instalar nada. 🚀

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

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

Leia também