ownerpro / nfsen-php-sdk
Pacote Laravel para emissão de NFSe Nacional
Requires
- php: ^8.3
- ext-curl: *
- ext-dom: *
- ext-libxml: *
- ext-mbstring: *
- ext-openssl: *
- ext-simplexml: *
- ext-zlib: *
- bacon/bacon-qr-code: ^3.0
- dompdf/dompdf: ^3.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- nfephp-org/sped-common: ^5.1
Requires (Dev)
- driftingly/rector-laravel: ^2.1
- larastan/larastan: ^3.9
- laravel/pint: ^1.27
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.1
- pestphp/pest-plugin-type-coverage: ^4.0
- rector/rector: ^2.3
- roave/security-advisories: dev-latest
- shipmonk/dead-code-detector: ^1.0
- smalot/pdfparser: ^2.10
- tomasvotruba/cognitive-complexity: ^1.1
- tomasvotruba/unused-public: ^2.2
- vimeo/psalm: ^6.15
README
Pacote PHP para emissão, cancelamento, substituição e consulta de NFSe Padrão Nacional (nfse.gov.br) via API REST. Funciona com Laravel 11/12 ou standalone (sem framework).
Funcionalidades
- Emissão de NFSe (
emitir) e emissão por decisão judicial (emitirDecisaoJudicial) - Cancelamento de NFSe (
cancelar) - Substituição de NFSe (
substituir) - Consulta por chave de acesso, DPS, DANFSE (URL do PDF), eventos e verificação de DPS
- Distribuição de documentos fiscais via ADN — consulta em lote por NSU (
distribuicao) - Assinatura digital XML com certificado A1 (PFX/P12)
- Validação XSD dos documentos
- Eventos Laravel opcionais (
NfseEmitted,NfseCancelled,NfseRejected, etc.) - mTLS sem escrita nomeada em disco
- 100% de cobertura de testes e tipos
Requisitos
- PHP 8.2+
- Extensões:
curl,dom,zlib,openssl,mbstring,libxml - Laravel 11 ou 12 (opcional — funciona standalone)
Instalação
composer require ownerpro/nfsen-php-sdk
Configuração
Laravel
Publique o arquivo de configuração:
php artisan vendor:publish --tag=nfsen-config
Adicione as variáveis de ambiente no .env:
NFSE_AMBIENTE=2 # 1 = Producao, 2 = Homologacao (aceita: 'producao', 'production', 'homologacao', 'homologation') NFSE_PREFEITURA=3550308 # Codigo IBGE do municipio (7 digitos) NFSE_CERT_PATH=/caminho/cert.pfx # Caminho do certificado PFX/P12 NFSE_CERT_SENHA=senha # Senha do certificado NFSE_TIMEOUT=30 NFSE_CONNECT_TIMEOUT=10 NFSE_SIGNING_ALGORITHM=sha1 NFSE_SSL_VERIFY=true NFSE_VALIDATE_IDENTITY=true # Valida CNPJ/CPF do certificado contra o prestador da DPS
A variavel NFSE_VALIDATE_IDENTITY (padrao: true) controla se o SDK verifica que o CNPJ ou CPF do certificado digital corresponde ao prestador informado na DPS antes de enviar a requisicao. Desative (false) apenas quando um representante legal (contador ou procurador com procuracao eletronica) emite notas em nome de terceiros.
Standalone (sem Laravel)
use OwnerPro\Nfsen\Enums\NfseAmbiente; use OwnerPro\Nfsen\NfsenClient; $client = NfsenClient::forStandalone( pfxContent: file_get_contents('/caminho/certificado.pfx'), senha: 'senha_certificado', prefeitura: '3550308', ambiente: NfseAmbiente::HOMOLOGACAO, );
Para desabilitar a validacao de identidade (representante legal / contador):
$client = NfsenClient::forStandalone( pfxContent: file_get_contents('/caminho/certificado.pfx'), senha: 'senha_certificado', prefeitura: '3550308', ambiente: NfseAmbiente::HOMOLOGACAO, validateIdentity: false, );
Uso
Emitir NFSe
$response = $client->emitir([ 'infDPS' => [ 'tpAmb' => '2', // 1 = Producao, 2 = Homologacao 'dhEmi' => date('Y-m-d\TH:i:sP'), // Data/hora emissao 'verAplic' => 'MeuSistema_v1.0', 'serie' => '1', 'nDPS' => '1', 'dCompet' => date('Y-m-d'), // Data de competencia 'tpEmit' => '1', // 1 = Prestador 'cLocEmi' => '3550308', // Codigo IBGE 7 digitos ], 'prest' => [ 'CNPJ' => '00000000000000', 'fone' => '11999999999', 'regTrib' => [ 'opSimpNac' => '2', // 1 = Nao Optante, 2 = MEI, 3 = ME/EPP 'regEspTrib' => '0', // 0 = Nenhum ], ], 'toma' => [ 'xNome' => 'Tomador Exemplo Ltda', 'CPF' => '00000000000', 'end' => [ 'xLgr' => 'Rua Exemplo', 'nro' => '100', 'xBairro' => 'Centro', 'endNac' => [ 'cMun' => '3550308', 'CEP' => '01001000', ], ], ], 'serv' => [ 'cLocPrestacao' => '3550308', 'cServ' => [ 'cTribNac' => '010101', 'xDescServ' => 'Desenvolvimento de software sob encomenda', 'cNBS' => '116030000', 'cIntContrib' => '1234', ], ], 'valores' => [ 'vServPrest' => ['vServ' => '1000.00'], 'trib' => [ 'tribMun' => [ 'tribISSQN' => '1', // 1 = Operacao tributavel 'tpRetISSQN' => '1', // 1 = Nao retido ], 'indTotTrib' => '0', ], ], ]); if ($response->sucesso) { echo "Chave: {$response->chave}\n"; echo "XML: {$response->xml}\n"; } else { foreach ($response->erros as $erro) { echo "[{$erro->codigo}] {$erro->mensagem} - {$erro->descricao}"; } }
Emitir NFSe por decisão judicial
Utiliza endpoint diferente (emit_court_order) para notas emitidas por determinação judicial:
$response = $client->emitirDecisaoJudicial($dps); // Mesma estrutura de DPS e mesmo NfseResponse do emitir()
Cancelar NFSe
use OwnerPro\Nfsen\Enums\CodigoJustificativaCancelamento; $response = $client->cancelar( chave: '00000000000000000000000000000000000000000000000000', codigoMotivo: CodigoJustificativaCancelamento::ErroEmissao, descricao: 'Erro na emissao da nota fiscal', );
Codigos de cancelamento: ErroEmissao, ServicoNaoPrestado, Outros.
Substituir NFSe
O método substituir() emite uma DPS com o grupo subst preenchido automaticamente. O ADN (Ambiente de Dados Nacional) cancela a nota original ao processar a DPS substituta — uma única requisição.
Nota: O registro do evento de cancelamento por substituição (
e105102) via API Eventos (POST /nfse/{chave}/eventos) é restrito a sistemas municipais conveniados com o ADN — o autor desse evento é o município emissor (MEmis), não o contribuinte. O cancelamento da nota original ocorre automaticamente ao emitir a DPS com o gruposubstpreenchido.
use OwnerPro\Nfsen\Enums\CodigoJustificativaSubstituicao; $response = $client->substituir( chave: '00000000000000000000000000000000000000000000000000', dps: $dpsSubstituta, // DPS da nota substituta (mesma estrutura do emitir) codigoMotivo: CodigoJustificativaSubstituicao::Outros, descricao: 'Substituicao por correcao de dados', ); // NfseResponse (mesmo retorno do emitir) if ($response->sucesso) { echo "Chave substituta: {$response->chave}"; } else { foreach ($response->erros as $erro) { echo "[{$erro->codigo}] {$erro->descricao}\n"; } }
Codigos de substituição: DesenquadramentoSimplesNacional, EnquadramentoSimplesNacional, InclusaoRetroativaImunidadeIsencao, ExclusaoRetroativaImunidadeIsencao, RejeicaoTomadorIntermediario, Outros.
Consultas
use OwnerPro\Nfsen\Enums\TipoEvento; // Consultar NFSe por chave de acesso $response = $client->consultar()->nfse($chave); // Consultar DPS por ID $response = $client->consultar()->dps($idDps); // Obter PDF do DANFSE $response = $client->consultar()->danfse($chave); // $response->pdf contém o conteúdo binário do PDF file_put_contents('danfse.pdf', $response->pdf); // Consultar eventos (tipoEvento é obrigatório) $response = $client->consultar()->eventos( chave: $chave, tipoEvento: TipoEvento::CancelamentoPorIniciativaPrestador, // e101101 nSequencial: 1, ); // Tipos disponíveis: CancelamentoPorIniciativaPrestador, CancelamentoPorIniciativaFisco, // CancelamentoPorSubstituicao, AnulacaoCancelamento // Verificar se DPS foi processada $processada = $client->consultar()->verificarDps($idDps); // true ou false
Distribuição (ADN Contribuinte)
Consulta em lote de documentos fiscais via NSU (Número Sequencial Único) através do ADN (Ambiente de Dados Nacional). Útil para importação em massa de NFS-e.
// Buscar lote de documentos a partir do NSU 0 $response = $client->distribuicao()->documentos(0); if ($response->sucesso) { foreach ($response->lote as $doc) { echo "NSU: {$doc->nsu} | Tipo: {$doc->tipoDocumento->value} | Chave: {$doc->chaveAcesso}\n"; // $doc->arquivoXml contém o XML já descomprimido } } // Buscar documento unitário pelo NSU $response = $client->distribuicao()->documento(42); // Buscar todos os eventos de uma NFS-e $response = $client->distribuicao()->eventos($chave); // Usar CNPJ diferente do certificado (procurador/filiais) $response = $client->distribuicao()->documentos(0, '99999999000100');
O fluxo típico de importação:
- Comece com NSU
0 - Chame
documentos($nsu)— receba um lote - Guarde o maior NSU do lote
- Repita com o próximo NSU até
statusProcessamentoserNenhumDocumentoLocalizado
Laravel Facade
use OwnerPro\Nfsen\Facades\Nfsen; // Emitir $response = Nfsen::emitir($dps); // Cancelar $response = Nfsen::cancelar($chave, $motivo, $descricao); // Consultar $response = Nfsen::consultar()->nfse($chave); $danfse = Nfsen::consultar()->danfse($chave); // Usar certificado diferente por requisicao $client = Nfsen::for($pfxContent, $senha, '3550308'); $response = $client->emitir($dps); // Sobrescrever ambiente (ignorar config) use OwnerPro\Nfsen\Enums\NfseAmbiente; $client = Nfsen::for($pfxContent, $senha, '3550308', NfseAmbiente::PRODUCAO); $response = $client->emitir($dps);
Eventos
O pacote dispara eventos Laravel que podem ser escutados na sua aplicação:
| Evento | Propriedades | Descricao |
|---|---|---|
NfseEmitted |
chave |
NFSe emitida com sucesso |
NfseCancelled |
chave |
NFSe cancelada com sucesso |
NfseSubstituted |
chave, chaveSubstituta |
NFSe substituída com sucesso |
NfseQueried |
operacao |
Consulta realizada |
NfseRequested |
operacao, metadata |
Operação iniciada |
NfseRejected |
operacao, codigoErro |
Operação rejeitada pela API |
NfseFailed |
operacao, mensagem |
Falha na operação |
Substituição: como substituir delega ao emitir internamente, a sequência de eventos disparados é:
NfseRequested('emitir') → NfseEmitted → NfseSubstituted
Objetos de Resposta
Cada operação retorna um DTO tipado e imutável:
NfseResponse
Retornado por emitir(), emitirDecisaoJudicial(), cancelar(), substituir(), consultar()->nfse() e consultar()->dps().
| Propriedade | Tipo | Descricao |
|---|---|---|
sucesso |
bool |
Se a operação foi aceita |
chave |
?string |
Chave de acesso da NFSe (50 dígitos) |
xml |
?string |
XML da NFSe processada |
idDps |
?string |
Identificador da DPS |
alertas |
list<ProcessingMessage> |
Alertas não-bloqueantes |
erros |
list<ProcessingMessage> |
Erros de processamento |
tipoAmbiente |
?int |
1 = Produção, 2 = Homologação |
versaoAplicativo |
?string |
Versão do aplicativo da SEFIN |
dataHoraProcessamento |
?string |
Data/hora do processamento |
DanfseResponse
Retornado por consultar()->danfse().
| Propriedade | Tipo | Descricao |
|---|---|---|
sucesso |
bool |
Se o PDF foi obtido |
pdf |
?string |
Conteúdo binário do PDF |
erros |
list<ProcessingMessage> |
Erros de processamento |
EventsResponse
Retornado por consultar()->eventos().
| Propriedade | Tipo | Descricao |
|---|---|---|
sucesso |
bool |
Se a consulta teve sucesso |
xml |
?string |
XML do evento |
erros |
list<ProcessingMessage> |
Erros de processamento |
tipoAmbiente |
?int |
1 = Produção, 2 = Homologação |
versaoAplicativo |
?string |
Versão do aplicativo da SEFIN |
dataHoraProcessamento |
?string |
Data/hora do processamento |
DistribuicaoResponse
Retornado por distribuicao()->documentos(), distribuicao()->documento() e distribuicao()->eventos().
| Propriedade | Tipo | Descricao |
|---|---|---|
sucesso |
bool |
true quando statusProcessamento é DocumentosLocalizados |
statusProcessamento |
StatusDistribuicao |
Status: Rejeicao, NenhumDocumentoLocalizado, DocumentosLocalizados |
lote |
list<DocumentoFiscal> |
Documentos fiscais retornados |
alertas |
list<ProcessingMessage> |
Alertas não-bloqueantes |
erros |
list<ProcessingMessage> |
Erros de processamento |
tipoAmbiente |
?int |
1 = Produção, 2 = Homologação |
versaoAplicativo |
?string |
Versão do aplicativo |
dataHoraProcessamento |
?string |
Data/hora do processamento |
DocumentoFiscal
Cada item do lote na DistribuicaoResponse.
| Propriedade | Tipo | Descricao |
|---|---|---|
nsu |
?int |
Número Sequencial Único |
chaveAcesso |
?string |
Chave de acesso da NFS-e |
tipoDocumento |
TipoDocumentoFiscal |
Tipo: Nfse, Dps, Evento, Cnc, PedidoRegistroEvento, Nenhum |
tipoEvento |
?TipoEventoDistribuicao |
Tipo do evento (quando tipoDocumento é Evento) |
arquivoXml |
?string |
XML do documento (já descomprimido) |
dataHoraGeracao |
?string |
Data/hora de geração |
ProcessingMessage
Representa uma mensagem de erro ou alerta da API:
| Propriedade | Tipo | Descricao |
|---|---|---|
mensagem |
?string |
Mensagem principal |
codigo |
?string |
Código do erro/alerta |
descricao |
?string |
Descrição detalhada |
complemento |
?string |
Informação complementar |
parametros |
list<string> |
Parâmetros adicionais da mensagem |
Exceções
| Exceção | Pai | Quando |
|---|---|---|
NfseException |
RuntimeException |
Erros gerais (XML inválido, falha de compressão, etc.) |
HttpException |
NfseException |
Erros HTTP 5xx sem corpo JSON. Acesse getResponseBody() para detalhes |
CertificateExpiredException |
NfseException |
Certificado PFX/P12 expirado |
InvalidDpsArgument |
InvalidArgumentException |
Campos mutuamente exclusivos ou obrigatórios violados na DPS |
use OwnerPro\Nfsen\Exceptions\CertificateExpiredException; use OwnerPro\Nfsen\Exceptions\HttpException; use OwnerPro\Nfsen\Exceptions\InvalidDpsArgument; use OwnerPro\Nfsen\Exceptions\NfseException; try { $response = $client->emitir($dps); } catch (CertificateExpiredException $e) { // Certificado expirado -- renovar } catch (InvalidDpsArgument $e) { // Dados da DPS inválidos -- corrigir payload } catch (HttpException $e) { // Erro HTTP -- $e->getCode() para status, $e->getResponseBody() para corpo } catch (NfseException $e) { // Outros erros (XML, compressão, etc.) }
Renderização local do DANFSE
O SDK gera o DANFSE (PDF ou HTML) localmente a partir do XML da NFS-e autorizada, útil como alternativa quando o endpoint ADN oficial está indisponível ou quando você precisa renderizar offline.
Uso básico
use OwnerPro\Nfsen\NfsenClient; $client = NfsenClient::for($pfx, $senha, $prefeitura); $response = $client->emitir($dps); $pdf = $client->danfse()->toPdf($response->xml); file_put_contents('danfse.pdf', $pdf->pdf);
Customização via array
$resp = $client->danfse([ 'logo_path' => '/path/to/custom-logo.png', 'municipality' => [ 'name' => 'São Paulo', 'department' => 'SF/SUBTES', 'email' => 'nfse@sp.gov.br', ], ])->toPdf($response->xml); if ($resp->sucesso) { file_put_contents('danfse.pdf', $resp->pdf); }
Customização via DTO (equivalente)
use OwnerPro\Nfsen\Danfse\DanfseConfig; use OwnerPro\Nfsen\Danfse\MunicipalityBranding; $config = new DanfseConfig( logoPath: '/caminho/para/logo.png', municipality: new MunicipalityBranding( name: 'Município de Canela', department: '(54) 3282-5155', email: 'issqn@canela.rs.gov.br', logoPath: '/caminho/para/brasao.png', ), ); $pdf = $client->danfse($config)->toPdf($response->xml);
Debug: obter o HTML intermediário
$html = $client->danfse()->toHtml($response->xml); file_put_contents('danfse.html', $html);
Geração automática do DANFSE
O PDF é anexado ao NfseResponse em emitir(), emitirDecisaoJudicial(), substituir()
e consultar()->nfse() quando a DANFSE é configurada.
Modo Laravel simples — via config/nfsen.php:
NFSE_DANFSE_AUTO=true NFSE_DANFSE_LOGO_PATH=/path/to/logo.png NFSE_DANFSE_MUN_NAME="São Paulo" NFSE_DANFSE_MUN_DEPT="SF/SUBTES" NFSE_DANFSE_MUN_EMAIL=nfse@sp.gov.br
$resp = NfsenClient::for($pfx, $senha, $ibge)->emitir($dps); echo $resp->pdf; // string com o PDF (ou null se render falhou) print_r($resp->pdfErrors); // list<ProcessingMessage> quando render falha
Modo multi-tenant — passe o array por requisição:
$client = NfsenClient::for($pfx, $senha, $tenant->ibge, danfse: [ 'logo_path' => $tenant->logoPath, 'municipality' => [ 'name' => $tenant->municipio, 'email' => $tenant->emailPrefeitura, ], ]); $resp = $client->emitir($dps);
Desligar pontualmente (mesmo com NFSE_DANFSE_AUTO=true):
$client = NfsenClient::for($pfx, $senha, $ibge, danfse: false);
Quando o PDF falha ($resp->sucesso === true mas $resp->pdf === null): a NFS-e
foi emitida com sucesso. Tente regenerar sob demanda com
$client->danfse($config)->toPdf($resp->xml).
Gotcha do enabled: a flag nfsen.danfse.enabled é checada com === true estrito.
O config publicado aplica (bool) cast; se consumir config de outra fonte (ex.: banco de
dados) garanta o tipo bool. 1, 'true', ou 'on' não ativam auto-render.
Atribuição
A renderização do DANFSE foi portada da biblioteca andrevabo/danfse-nacional (MIT) e adaptada à arquitetura deste SDK. A tabela de municípios IBGE vem de kelvins/municipios-brasileiros (MIT).
Exemplos
Exemplos completos de cada operação estão disponíveis no diretório examples/.
Testes
composer test
Para executar todas as verificações de qualidade:
composer quality
Contribuindo
Veja CONTRIBUTING.md para detalhes.
Créditos
Este pacote teve como base o trabalho do projeto original nfse-nacional de Fernando Friedrich, que por sua vez foi construído sobre o NFePHP de Roberto L. Machado.
Agradecimento a todos os contribuidores que ajudaram a evoluir este projeto.
Licença
MIT. Veja LICENSE.