PHP: faturando compras no Igest, em Portugal
Olá meus Unicórnios! 🦄✨
Sabe quando você olha para a tarefa e pensa "é só mandar um JSON com o valor da compra e pronto"? 😅 Pois é. Faturar em Portugal não é mandar um valor: é montar um documento fiscal, e documento fiscal tem regras que não perdoam um cêntimo de diferença.
Eu precisei integrar um sistema de vendas com o Igest, que é um dos serviços de faturação certificada usados por lá. E o caminho até a primeira fatura sair certa me ensinou quatro coisas que eu não sabia quando comecei: o preço que você cobra não é o valor que vai na linha, o IVA é um código de tabela e não uma percentagem, o NIF precisa chegar limpo, e — a mais assustadora — uma fatura enviada não se reenvia. 🤯
Neste artigo eu vou do zero até a fatura emitida, com um arquivo PHP único, linear, sem classe nem Composer. Só a extensão SOAP que já vem no PHP.
🧩 Por que SOAP, e por que sem WSDL
A primeira surpresa foi o protocolo. O webservice do Igest é SOAP — daqueles com envelope XML — e ele não publica um WSDL. Isso muda como você abre a conexão.
Normalmente você passaria a URL do WSDL para o SoapClient e
ele descobriria sozinho os tipos e os métodos. Aqui não dá: o primeiro
argumento vai null, e aí você tem de dizer à mão como o
envelope deve ser montado.
// Sem WSDL o SoapClient recebe null no 1o argumento, e o envelope
// precisa ser descrito a mao aqui.
function abrirCliente($endpoint)
{
return new SoapClient(null, array(
'location' => $endpoint,
'uri' => $endpoint,
'connection_timeout' => 30,
'trace' => true,
'exceptions' => true,
'style' => SOAP_RPC, // sem estes dois o servidor
'use' => SOAP_ENCODED, // nao entende o envelope
));
}
function chamar($soap, $endpoint, $funcao, $parametros)
{
// Sem soapaction/uri a resposta e "funcao desconhecida",
// mesmo com o nome absolutamente correto.
$retorno = $soap->__soapCall($funcao, $parametros, array(
'soapaction' => $endpoint . '#' . $funcao,
'uri' => $endpoint,
));
// A resposta volta como objeto aninhado; isto achata tudo em array.
return json_decode(json_encode($retorno), true);
}
Repare nos dois pares que costumam ser esquecidos:
styleeuse— semSOAP_RPCeSOAP_ENCODED, o PHP monta um envelope de outro formato e o servidor simplesmente não entende o que você mandou.soapactioneurina chamada — sem eles a resposta que volta é "função desconhecida", mesmo com o nome da função absolutamente correto. É um erro que engana, porque parece que você escreveu o nome errado. 😤
E o último detalhe: a resposta volta como objeto do PHP, aninhado. O
json_decode(json_encode(...), true) ali no fim é o truque
preguiçoso — e eficaz — para transformar tudo em array de uma vez.
🔐 A assinatura: duas datas que têm de ser a mesma
A autenticação é simples de descrever e fácil de errar. Você manda uma chave calculada assim:
// A data e gerada UMA vez: ela assina e viaja no envelope.
// Duas chamadas a date() podem cair em segundos diferentes.
$data = date('Y-m-d H:i:s');
$chave_assinada = sha1($CHAVE . $data);
Ou seja: sha1 da sua chave secreta concatenada com a data.
Só que essa mesma data também viaja no envelope, num
campo próprio. E é aí que mora a armadilha.
Se você chamar date('Y-m-d H:i:s') duas vezes — uma para
assinar e outra para preencher o campo —, existe uma chance real de as duas
caírem em segundos diferentes. A requisição sai, o servidor recalcula o
sha1 com a data que recebeu, dá diferente da que você
assinou, e a resposta é um educado "autenticação inválida". 😳
Foi exatamente o que aconteceu comigo, e eu perdi um tempo bom achando que a chave estava errada. A chave estava certa. Era o relógio.
E é a mesma variável que vai nos dois parâmetros do envelope:
new SoapParam(new SoapVar($chave_assinada, XSD_STRING), 'keyCliente'),
new SoapParam(new SoapVar($data, XSD_STRING), 'data'),
💶 O IVA: o valor da linha não é o preço que você cobrou
Se você só for ler um pedaço deste artigo, leia este. 🙏
Em Portugal a taxa normal de IVA no continente é de 23%. Quando o cliente paga 20,00 €, esses 20,00 € já têm o IVA dentro. Mas o Igest, do jeito que a gente monta aqui, quer o valor da linha sem IVA — e ele mesmo calcula o imposto e o total por cima.
Quem decide isso é uma flag chamada bIvaInc:
function montarArtigo($endpoint, $descricao, $valor_com_iva, $quantidade, $percentagem_iva, $tipo_iva)
{
$artigo = array(
'codigo' => '',
'tipo' => 'S',
'nome' => $descricao,
'valor_unitario' => valorSemIva($valor_com_iva, $percentagem_iva),
'bIvaInc' => 0, // 0 = o valor acima esta limpo, sem imposto
'quantidade' => (float) $quantidade,
'iva' => montarIva($endpoint, $percentagem_iva, $tipo_iva),
'desconto' => (float) 0,
);
return new SoapVar($artigo, SOAP_ENC_OBJECT, 'Artigo', $endpoint);
}
Com bIvaInc = 0 você está dizendo "o valor unitário que eu
mandei está limpo, sem imposto". Então o valor tem de ser
dividido antes de sair:
// O valor cobrado ja tem IVA dentro. A linha vai sem IVA, e com
// QUATRO casas: o servico multiplica pela quantidade e recalcula o total.
function valorSemIva($valor_com_iva, $percentagem_iva)
{
return round($valor_com_iva / (1 + ($percentagem_iva / 100)), 4);
}
E agora o detalhe que parece paranoia e não é: quatro casas decimais, não duas.
🧮 Por que quatro casas decimais (e não duas)
Meu primeiro instinto foi arredondar para dois — é dinheiro, dinheiro tem dois. Mas o serviço não usa o seu valor como total: ele recalcula. Ele pega o seu valor unitário, multiplica pela quantidade, aplica o IVA e chega ao total dele. Se esse total não bater com o que o cliente pagou, você emitiu uma fatura errada — e fatura errada em documento fiscal é chatice de verdade.
E repare em como ele recalcula: o serviço arredonda a incidência e o IVA separadamente, e só então soma os dois. Não é o total que ele arredonda no fim — são as duas parcelas, cada uma por si. Essa diferença muda o resultado da conta, e é ela que a varredura abaixo reproduz.
Eu quis saber o tamanho do estrago antes de decidir, então varri 2.000 combinações de valor e quantidade comparando as duas precisões, sempre contra o total esperado:
$u2 = round($valor / 1.23, 2);
$i2 = round($u2 * $q, 2);
$t2 = round($i2 + round($i2 * 0.23, 2), 2);
$u4 = round($valor / 1.23, 4);
$i4 = round($u4 * $q, 2);
$t4 = round($i4 + round($i4 * 0.23, 2), 2);
casos testados: 2000
erram com 2 casas: 1529
erram com 4 casas: 375
4 casas salva (2 erra, 4 acerta):
0.5 x2 esperado 1 / 2casas 1.01 / 4casas 1
0.5 x3 esperado 1.5 / 2casas 1.51 / 4casas 1.5
0.5 x4 esperado 2 / 2casas 2.02 / 4casas 2
0.5 x5 esperado 2.5 / 2casas 2.52 / 4casas 2.5
0.5 x6 esperado 3 / 2casas 3.03 / 4casas 3
0.5 x8 esperado 4 / 2casas 4.03 / 4casas 4
2 casas salva (4 erra, 2 acerta):
Olhe a última lista: está vazia. Não existe um caso sequer em que duas casas acertem e quatro errem. Quatro casas é estritamente melhor — e a diferença não é pequena: 1.529 desvios contra 375.
Repare também que o erro aparece quando a quantidade é maior que 1. Com quantidade 1 as duas precisões dão no mesmo, e é por isso que esse bug consegue passar despercebido por meses: os seus testes com um item só passam lindamente, e a fatura torta só aparece quando alguém compra três. 😅
🏷️ O IVA é um código, não um número
Este me pegou de jeito. Dentro da linha vai um objeto de IVA com três
campos, e o campo tipo não é a percentagem nem uma letra
qualquer que você invente:
// O campo "tipo" e um codigo de tabela, nao a percentagem nem uma letra.
function montarIva($endpoint, $percentagem, $tipo)
{
$iva = array(
'percentagem' => (float) $percentagem,
'tipo' => $tipo, // NOR (23%), INT (13%), RED (6%), ISE (isenta)
'regiao' => 'PT',
);
return new SoapVar($iva, SOAP_ENC_OBJECT, 'IVA', $endpoint);
}
Eu tinha escrito "I", de "IVA", achando que era só um
rótulo. Não é: é um código de tabela. Os válidos são
NOR (normal, 23%), INT (intermédia, 13%),
RED (reduzida, 6%) e ISE (isenta). Qualquer
outra coisa e o documento é recusado.
E repare que a percentagem vai junto com o código. Os dois têm
de combinar — mandar NOR com percentagem 6 é pedir problema.
🇵🇹 O NIF precisa chegar limpo
O NIF é o número de contribuinte português: nove dígitos.
Só que o que chega do formulário raramente é nove dígitos limpos — vem com
espaço, com ponto, às vezes com o prefixo PT na frente.
// O NIF portugues tem 9 digitos. Tudo que nao for digito cai fora,
// inclusive o prefixo "PT" que muita gente digita na frente.
function limparNif($nif)
{
$so_digitos = preg_replace('/\D/', '', (string) $nif);
// Nove digitos ou nada: NIF pela metade viraria contribuinte inexistente.
if (strlen($so_digitos) !== 9) {
return '';
}
return $so_digitos;
}
Rodando isso com as entradas que eu realmente via chegar:
"123 456 789" -> [123456789]
"123.456.789" -> [123456789]
"PT123456789" -> [123456789]
"12345678" -> []
"" -> []
A decisão de projeto que importa está nas duas últimas linhas: um NIF com o número errado de dígitos vira string vazia, não um erro. Isso é de propósito — em Portugal a fatura sem contribuinte é perfeitamente válida (é a de consumidor final). O que não pode é ir um NIF pela metade, porque aí o documento sai com um contribuinte que não existe. Vazio é honesto; torto não é.
A morada segue a mesma lógica, e por isso os três campos vão juntos ou não vão:
function montarCliente($endpoint, $cliente)
{
$dados = array(
'codigo' => (string) $cliente['codigo'],
'nome' => $cliente['nome'],
'nif' => limparNif($cliente['nif']),
'pais_code' => 'PT',
'email' => $cliente['email'],
);
// Morada incompleta e pior que morada nenhuma: os tres campos
// vao juntos ou nao vao.
if ($cliente['cod_postal'] !== '' && $cliente['morada'] !== '' && $cliente['cidade'] !== '') {
$dados['cod_postal'] = $cliente['cod_postal'];
$dados['morada'] = $cliente['morada'];
$dados['cidade'] = $cliente['cidade'];
}
return new SoapVar($dados, SOAP_ENC_OBJECT, 'Cliente', $endpoint);
}
📅 Duas datas, dois formatos
Um detalhe cruel e silencioso: o documento carrega duas datas, e elas não têm o mesmo formato.
data(a da autenticação, lá em cima) é data e hora:Y-m-d H:i:s.data_emissao, dentro do documento, é só a data:Y-m-d.
Mandar a hora junto no data_emissao é rejeição na hora. E é
o tipo de coisa que você copia de uma variável para a outra sem pensar,
porque as duas "são a data".
🚫 A fatura que não se reenvia
Agora a parte que muda como você escreve o tratamento de erro — e a
razão de eu ter tomado tanto cuidado com o catch.
Uma fatura é um documento fiscal. Ela não é um registro no seu banco que você apaga e recria: ela entra na contabilidade, ela é comunicada, ela tem número numa série. Emitir a mesma compra duas vezes não é um bug de dados — é um documento duplicado que alguém vai ter que anular formalmente depois. 😬
Só que a rede não colabora. Existe um cenário perfeitamente possível: a requisição chega ao Igest, a fatura é criada, e a resposta se perde no caminho de volta. Do lado do seu código, o que você vê é uma exceção — exatamente igual à de "não consegui conectar". Mas os dois casos são opostos: num não existe fatura nenhuma, no outro existe uma fatura que você não sabe que existe.
Por isso o tratamento separa as duas situações, e a diferença fica num campo explícito:
try {
$soap = abrirCliente($ENDPOINT);
$resposta = chamar($soap, $ENDPOINT, 'novo_documento', $parametros);
} catch (Exception $e) {
// Nao sabemos o que aconteceu do outro lado: a fatura pode
// ter sido criada e so a resposta ter se perdido.
return array(
'ok' => false,
'reenviar' => false, // conferir a mao, NUNCA repetir sozinho
'mensagem' => 'Falha de comunicacao: ' . $e->getMessage(),
);
}
if (empty($resposta['bSucesso'])) {
// Aqui ha resposta dizendo que nada foi criado: pode corrigir e reenviar.
return array(
'ok' => false,
'reenviar' => true,
'mensagem' => 'O Igest recusou o documento: ' . $resposta['descricaoErro'],
);
}
Repare no 'reenviar':
- Exceção (rede, timeout) →
false. Você não sabe o que aconteceu do outro lado. Marque para conferir à mão e siga. - Recusa (
bSucessovazio) →true. Aqui você tem uma resposta dizendo que nada foi criado. Pode corrigir o que o serviço reclamou e mandar de novo, tranquila.
É uma distinção de uma linha que evita o pior estrago possível desta integração.
🛡️ E a rede de segurança: o número do documento
Mesmo com todo o cuidado acima, o que de fato impede a duplicata é uma decisão de modelagem: o número da compra vira o número do documento.
'data_emissao' => date('Y-m-d'), // so a data, sem hora
// O numero da compra vira o numero do documento: o proprio Igest
// recusa um segundo documento com o mesmo numero na serie.
'numero_seguranca' => (string) $compra['numero'],
'numero_fatura' => (string) $compra['numero'],
Como o Igest não aceita dois documentos com o mesmo número na mesma série, uma segunda tentativa da mesma compra bate numa parede do lado dele — e não depende de você lembrar de checar. É a proteção que continua funcionando mesmo quando o seu código falha.
Foi o que eu quis ver acontecer antes de confiar. Mandando a mesma compra duas vezes seguidas:
Fatura emitida: FR AB/4021
Sem IVA: 16.26 EUR
IVA: 3.74 EUR
Total: 20 EUR
PDF: index.php?mod=fact&op=viewPDF&id=1001
Erro: O Igest recusou o documento: Documento com este numero ja existe na serie
Pode reenviar: sim
A primeira passa, a segunda bate na parede. 🎉 E repare na aritmética da primeira: 16,26 + 3,74 = 20,00, exatamente o que o cliente pagou. É esse fechamento ao cêntimo que a divisão por 1,23 com quatro casas está protegendo.
🔗 O link do PDF vem em base64
Uma última pegadinha, pequena e simpática: o linkDocumento
da resposta não é uma URL. É o caminho do documento,
codificado em base64.
Então para chegar ao PDF você decodifica e junta com o domínio do serviço:
echo base64_decode($resultado['link']);
// index.php?mod=fact&op=viewPDF&id=1001
Guarde esse link junto do número do documento na sua base. É o comprovativo que o cliente vai pedir.
🔑 Onde ficam as credenciais
Nada de chave escrita dentro do arquivo. Tudo por variável de ambiente:
// Credenciais so por variavel de ambiente - nada escrito aqui dentro.
$ENDPOINT = getenv('IGEST_ENDPOINT'); // https://<servidor>/webservice.php
$ID_ENTIDADE = (int) getenv('IGEST_ID_ENTIDADE');
$CHAVE = getenv('IGEST_CHAVE');
$EMAIL = getenv('IGEST_EMAIL');
$SERIE = getenv('IGEST_SERIE');
São cinco: o endereço do webservice, o identificador da sua entidade, a
chave secreta, o e-mail da conta e a série do documento. A série é aquele
prefixo que aparece no número da fatura — no exemplo acima,
AB/4021.
📄 O arquivo inteiro
É este, de cima a baixo. Uma função por assunto, a chamada no fim, e comentário curto em cada linha onde mora uma armadilha:
<?php
// Emissao de fatura no Igest via SOAP sem WSDL.
// Credenciais so por variavel de ambiente - nada escrito aqui dentro.
$ENDPOINT = getenv('IGEST_ENDPOINT'); // https://<servidor>/webservice.php
$ID_ENTIDADE = (int) getenv('IGEST_ID_ENTIDADE');
$CHAVE = getenv('IGEST_CHAVE');
$EMAIL = getenv('IGEST_EMAIL');
$SERIE = getenv('IGEST_SERIE');
// O NIF portugues tem 9 digitos. Tudo que nao for digito cai fora,
// inclusive o prefixo "PT" que muita gente digita na frente.
function limparNif($nif)
{
$so_digitos = preg_replace('/\D/', '', (string) $nif);
// Nove digitos ou nada: NIF pela metade viraria contribuinte inexistente.
if (strlen($so_digitos) !== 9) {
return '';
}
return $so_digitos;
}
// O valor cobrado ja tem IVA dentro. A linha vai sem IVA, e com
// QUATRO casas: o servico multiplica pela quantidade e recalcula o total.
function valorSemIva($valor_com_iva, $percentagem_iva)
{
return round($valor_com_iva / (1 + ($percentagem_iva / 100)), 4);
}
// O campo "tipo" e um codigo de tabela, nao a percentagem nem uma letra.
function montarIva($endpoint, $percentagem, $tipo)
{
$iva = array(
'percentagem' => (float) $percentagem,
'tipo' => $tipo, // NOR (23%), INT (13%), RED (6%), ISE (isenta)
'regiao' => 'PT',
);
return new SoapVar($iva, SOAP_ENC_OBJECT, 'IVA', $endpoint);
}
function montarArtigo($endpoint, $descricao, $valor_com_iva, $quantidade, $percentagem_iva, $tipo_iva)
{
$artigo = array(
'codigo' => '',
'tipo' => 'S',
'nome' => $descricao,
'valor_unitario' => valorSemIva($valor_com_iva, $percentagem_iva),
'bIvaInc' => 0, // 0 = o valor acima esta limpo, sem imposto
'quantidade' => (float) $quantidade,
'iva' => montarIva($endpoint, $percentagem_iva, $tipo_iva),
'desconto' => (float) 0,
);
return new SoapVar($artigo, SOAP_ENC_OBJECT, 'Artigo', $endpoint);
}
function montarCliente($endpoint, $cliente)
{
$dados = array(
'codigo' => (string) $cliente['codigo'],
'nome' => $cliente['nome'],
'nif' => limparNif($cliente['nif']),
'pais_code' => 'PT',
'email' => $cliente['email'],
);
// Morada incompleta e pior que morada nenhuma: os tres campos
// vao juntos ou nao vao.
if ($cliente['cod_postal'] !== '' && $cliente['morada'] !== '' && $cliente['cidade'] !== '') {
$dados['cod_postal'] = $cliente['cod_postal'];
$dados['morada'] = $cliente['morada'];
$dados['cidade'] = $cliente['cidade'];
}
return new SoapVar($dados, SOAP_ENC_OBJECT, 'Cliente', $endpoint);
}
// Sem WSDL o SoapClient recebe null no 1o argumento, e o envelope
// precisa ser descrito a mao aqui.
function abrirCliente($endpoint)
{
return new SoapClient(null, array(
'location' => $endpoint,
'uri' => $endpoint,
'connection_timeout' => 30,
'trace' => true,
'exceptions' => true,
'style' => SOAP_RPC, // sem estes dois o servidor
'use' => SOAP_ENCODED, // nao entende o envelope
));
}
function chamar($soap, $endpoint, $funcao, $parametros)
{
// Sem soapaction/uri a resposta e "funcao desconhecida",
// mesmo com o nome absolutamente correto.
$retorno = $soap->__soapCall($funcao, $parametros, array(
'soapaction' => $endpoint . '#' . $funcao,
'uri' => $endpoint,
));
// A resposta volta como objeto aninhado; isto achata tudo em array.
return json_decode(json_encode($retorno), true);
}
function emitirFatura($compra, $cliente)
{
global $ENDPOINT, $ID_ENTIDADE, $CHAVE, $EMAIL, $SERIE;
// A data e gerada UMA vez: ela assina e viaja no envelope.
// Duas chamadas a date() podem cair em segundos diferentes.
$data = date('Y-m-d H:i:s');
$chave_assinada = sha1($CHAVE . $data);
$artigo = montarArtigo(
$ENDPOINT,
$compra['descricao'],
$compra['valor'],
$compra['quantidade'],
23,
'NOR'
);
$documento = array(
'serie' => $SERIE,
'tipo' => 'FT',
'data_emissao' => date('Y-m-d'), // so a data, sem hora
// O numero da compra vira o numero do documento: o proprio Igest
// recusa um segundo documento com o mesmo numero na serie.
'numero_seguranca' => (string) $compra['numero'],
'numero_fatura' => (string) $compra['numero'],
'tipo_pagamento_code' => '0',
'cliente' => montarCliente($ENDPOINT, $cliente),
'artigos' => new SoapVar(array($artigo), SOAP_ENC_ARRAY, 'ArrayOfArtigo', $ENDPOINT),
'email_send' => $cliente['email'],
);
$parametros = array(
new SoapParam(new SoapVar($ID_ENTIDADE, XSD_INT), 'idEntidade'),
new SoapParam(new SoapVar($chave_assinada, XSD_STRING), 'keyCliente'),
new SoapParam(new SoapVar($data, XSD_STRING), 'data'),
new SoapParam(new SoapVar($EMAIL, XSD_STRING), 'email'),
new SoapParam(new SoapVar($documento, SOAP_ENC_OBJECT, 'Documento', $ENDPOINT), 'documento'),
new SoapParam(new SoapVar('1.0', XSD_STRING), 'versao'),
);
try {
$soap = abrirCliente($ENDPOINT);
$resposta = chamar($soap, $ENDPOINT, 'novo_documento', $parametros);
} catch (Exception $e) {
// Nao sabemos o que aconteceu do outro lado: a fatura pode
// ter sido criada e so a resposta ter se perdido.
return array(
'ok' => false,
'reenviar' => false, // conferir a mao, NUNCA repetir sozinho
'mensagem' => 'Falha de comunicacao: ' . $e->getMessage(),
);
}
if (empty($resposta['bSucesso'])) {
// Aqui ha resposta dizendo que nada foi criado: pode corrigir e reenviar.
return array(
'ok' => false,
'reenviar' => true,
'mensagem' => 'O Igest recusou o documento: ' . $resposta['descricaoErro'],
);
}
return array(
'ok' => true,
'documento' => $resposta['numeroDocumentoCompleto'],
'sem_iva' => $resposta['valorIncidencia'],
'iva' => $resposta['valorIva'],
'total' => $resposta['valorTotal'],
'link' => $resposta['linkDocumento'], // caminho em base64
);
}
// -------------------------------------------------------------------
// Uso
$compra = array(
'numero' => 4021,
'descricao' => 'COMPRA #4021 - PLANO MENSAL',
'valor' => 20.00,
'quantidade' => 1,
);
$cliente = array(
'codigo' => 77,
'nome' => 'Maria Silva',
'nif' => 'PT123456789',
'email' => '[email protected]',
'cod_postal' => '1000-001',
'morada' => 'Rua das Flores, 10',
'cidade' => 'Lisboa',
);
$resultado = emitirFatura($compra, $cliente);
if ($resultado['ok']) {
echo "Fatura emitida: " . $resultado['documento'] . "\n";
echo "Sem IVA: " . $resultado['sem_iva'] . " EUR\n";
echo "IVA: " . $resultado['iva'] . " EUR\n";
echo "Total: " . $resultado['total'] . " EUR\n";
echo "PDF: " . base64_decode($resultado['link']) . "\n";
} else {
echo "Erro: " . $resultado['mensagem'] . "\n";
echo "Pode reenviar: " . ($resultado['reenviar'] ? 'sim' : 'nao') . "\n";
}
🎁 O que eu levo desta integração
Faturação certificada é um daqueles temas em que o código é fácil e as regras é que são difíceis. O SOAP sem WSDL assusta na primeira meia hora e depois nunca mais; o que continua exigindo atenção é o resto: a data que precisa ser a mesma nos dois lugares, o valor que vai limpo com quatro casas, o código de IVA que é de tabela, o NIF que é nove dígitos ou nada.
E, acima de tudo, aquela distinção entre "foi recusado" e "não sei o que aconteceu". Num sistema comum, repetir a requisição é a coisa mais natural do mundo. Aqui, repetir é criar um documento fiscal a mais. 🦄
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Leia também
Node.js: consultando CPF e CNPJ no SPC
Como consultar CPF e CNPJ no SPC Brasil com Node.js puro: envelope SOAP na mao, sem biblioteca, e as armadilhas do caminho.
Tomadas Tuya pela API da nuvem, com Node.js
Monitorar e controlar tomadas Tuya pela API da nuvem em Node puro: a assinatura HMAC, as escalas e a leitura velha que parece atual.
Node.js: enviando SMS pela ez4uteam
Como enviar SMS pela API da ez4uteam com Node.js puro, e as armadilhas do telefone, do limite de 160 caracteres e do HTTP 200 que mente.