Node.js: consultando CPF e CNPJ no SPC
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('&');
saida = saida.split('<').join('<');
saida = saida.split('>').join('>');
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
Node.js: alterando o plano de energia do Windows
Como ler e trocar o plano de energia do Windows com Node.js e powercfg: por que resolver por GUID, e o acento que o TextDecoder nao decodifica.
Node.js: exportando grandes volumes do Elasticsearch
Como exportar milhões de registros do Elasticsearch para CSV com Node.js: a parede dos 10.000, a Scroll API e o scroll que fica aberto.
Node.js: gerando certificado SSL e instalando no IIS
Como emitir um certificado gratuito do Let's Encrypt com Node.js e instalar no IIS do Windows, com as armadilhas que travaram tudo.