ownerpro/nfsen-php-sdk

Pacote Laravel para emissão de NFSe Nacional

Maintainers

Package info

github.com/OwnerPro-Software/nfsen-php-sdk

pkg:composer/ownerpro/nfsen-php-sdk

Statistics

Installs: 28

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.3.1 2026-04-17 02:13 UTC

This package is auto-updated.

Last update: 2026-05-01 07:39:40 UTC


README

CI PHPStan Level 10 Latest Version on Packagist PHP Version License

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 grupo subst preenchido.

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:

  1. Comece com NSU 0
  2. Chame documentos($nsu) — receba um lote
  3. Guarde o maior NSU do lote
  4. Repita com o próximo NSU até statusProcessamento ser NenhumDocumentoLocalizado

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')NfseEmittedNfseSubstituted

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.