Todas as mudanças relevantes deste projeto serão documentadas neste arquivo.
O formato é baseado em Keep a Changelog, e este projeto segue Versionamento Semântico.
- Campos tipados de alto valor no DTO
ServiceInvoice(NFS-e):number(número fiscal),checkCode(código de verificação),description,cityServiceCode,baseTaxAmount,issRate,issTaxAmount,amountNet— anexados ao fim do construtor (aditivo) (expand-service-invoice-dto). - DTO aninhado
Nfe\Resource\Dto\ServiceInvoices\Borrower, exposto emServiceInvoice::$borrower.federalTaxNumberéint|string|null(tolerante a CPF/CNPJ; desvio deliberado vs. ointegerdo spec, pinado por teste de alinhamento). Oprovider(objeto pesado) permanece acessível viaraw. AbstractResource::hydrate()passa a popular o camporawde qualquer DTO que o declare, com o payload decodificado completo — o escape-hatch de forward-compat agora é consistente em todo o SDK (antes, sóWebhooksResourcee os lookups o preenchiam à mão). Efeito: todo campo que a API retorna fica acessível via$dto->raw['campo'], mesmo sem tipo. Também passa a hidratar DTOs aninhados (Nfe\...) recursivamente.- Teste de alinhamento YAML↔DTO para a resposta de NFS-e
(
ServiceInvoiceSpecAlignmentTest), ancorado pelo path/serviceinvoices/{id}(ooperationIdServiceInvoices_idGetcolide com a rota/external/{id}).
ServiceInvoice::$totalAmount— a API nunca o retorna (semprenull; confirmado ao vivo). UseservicesAmount/amountNet, ourawpara outros valores. Comportamento inalterado; remoção na próxima major.
- O transporte não retenta mais requisições
POSTem respostas HTTP 5xx nem em falhas de rede ambíguas (timeout após o envio). Reexecutar umPOSTpode duplicar o efeito — na emissão de NFS-e, isso significa nota fiscal duplicada. Sonda ao vivo (2026-07-06) reproduziu o risco: umcreate()retornouHTTP 500e ainda assim emitiu a nota.POSTsó é retentado agora em429, em falha comprovada de conexão (a requisição nunca chegou ao servidor) ou quando carrega um headerIdempotency-Key. Métodos idempotentes (GET/PUT/DELETE/HEAD/OPTIONS) seguem retentando como antes. - Rotas de escape: para emissão retry-safe, envie um
externalIde reconcilie comfindByExternalId()após uma falha ambígua (veja Adicionado); para restaurar o retry por chamada, passeRequestOptions(retry: ...).
- Retry ciente de método/idempotência em
Nfe\Http\RetryingTransport(tabela de decisão por método × status × fase da falha × headerIdempotency-Key) (add-safe-retry-idempotency). Nfe\Http\FailurePhase(ConnectionNotEstablished/RequestMaybeSent) e os camposfailurePhase/curlErrnoemNfe\Exception\ApiConnectionException:CurlTransportclassifica por allowlist de errnos (5/6/7/35 → conexão não estabelecida) ePsr18TransportporNetworkExceptionInterface.- Override de retry por requisição:
Nfe\Http\RequestOptionsganha o camporetry(?RetryPolicy), permitindo ligar/desligar o retry de uma única chamada sem manter dois clients. O decorator de retry passa a envolver sempre o transporte base. ServiceInvoicesResource::findByExternalId($companyId, $externalId)— recupera a NFS-e pela chave do integrador via rota dedicadaGET /v1/companies/{id}/serviceinvoices/external/{externalId}(hit = envelope de coleção; miss =null; confirmado ao vivo em 2026-07-06).ServiceInvoicesResource::isDuplicateExternalId(ApiErrorException $e): bool— reconhece a rejeição400 "service invoice with external id (…) already exists".- Campo
externalIdno DTONfe\Resource\Dto\ServiceInvoices\ServiceInvoice.
- Métodos account-scoped em
WebhooksResource, alinhados ao contrato real da API (/v2/webhooks, confirmado por sondas ao vivo em 3 contas, 2026-07-02/03):listAccountWebhooks(),createAccountWebhook(),retrieveAccountWebhook(),updateAccountWebhook(),deleteAccountWebhook(),deleteAllAccountWebhooks()(destrutivo — remove TODOS os webhooks da conta),pingAccountWebhook()efetchEventTypes()(lista viva de event types). Create/update enviam o envelope obrigatório{"webHook": {...}}e as respostas envelopadas são desembrulhadas (fix-account-webhooks-contract). - Novo DTO
Nfe\Resource\Dto\Webhooks\AccountWebhookcom o shape real do recurso:id,uri,contentType,secret(32–64 chars; ecoado no create, omitido nas leituras),filters,insecureSsl,headers,properties,status,createdOn,modifiedOn,raw. Nota de contrato: o spec OpenAPI declaracontentType/statuscomo enum int, mas a API serializa strings ("json","Active") — o DTO segue o fio, e um teste de alinhamento YAML↔DTO pina o desvio.
- Os métodos company-scoped de
WebhooksResource(list,create,retrieve,update,delete,test): a rota/v1/companies/{id}/webhooksretorna 404 incondicional na API atual (o contrato havia sido herdado de alucinação do SDK Node). Comportamento inalterado; use os equivalentes*AccountWebhook*. Remoção na próxima major. WebhooksResource::getAvailableEvents(): os literaisinvoice.*não existem na API — os ids reais seguemservice_invoice.*/product_invoice.*/consumer_invoice.*. UsefetchEventTypes().- DTO
Nfe\Resource\Dto\Webhooks\Webhook(url/events): shape rejeitado pela API (400 "The Uri field is required"). UseAccountWebhook.
- Gotchas do contrato real documentados no phpdoc, README e
docs/(recursos/)webhooks.md: a NFE.io pinga aurina criação e exige resposta 2xx;PUTde update é substituição integral (update semstatusdesativa o webhook — monte o corpo a partir do retrieve).
- BREAKING:
AddressesResource::search()eAddressesResource::lookupByTerm()foram removidos. O hostaddress.api.nfe.io/v2suporta apenas consulta por CEP; os endpointsGET /addresses(busca) eGET /addresses/{termo}não existem (retornam 404), então os métodos nunca funcionaram. Paridade com o SDK Node (fix-address-lookup-api-mismatch). UselookupByPostalCode().
AddressesResource::lookupByPostalCode()agora desembrulha o envelope real{ "address": { … } }retornado pela API. Antes,extractAddresses()procurava a chaveaddresses(plural) — que a API nunca retorna — e caía num fallback que expunha o envelope inteiro, deixando->addresses[0]['street']comonull(quebra silenciosa). Agora os campos do endereço ficam legíveis diretamente em->addresses[0].openapi/consulta-endereco-v3.yamlenxugado para conter só a operação de CEP, alinhando o spec à API real (as operações de busca/termo eram fantasmas).
composer.jsonnamevoltou a sernfe/nfe(av3.0.0-rc.1shipou comnfe/client-phppor engano). A v3 mantém o mesmo slug Packagist da v2 — Composer resolve^2.0e^3.0para a major correta. Não há pacotenfe/client-phpno Packagist; av3.0.0-rc.1foi indexada corretamente sobnfe/nfe. README e MIGRATION alinhados.
TaxCodesResourceapontava para/tax-rules/...(plural); a API expõe esses endpoints em/tax-codes/...(singular, paridade com o SDK Node). Todos os quatro métodos (listOperationCodes,listAcquisitionPurposes,listIssuerTaxProfiles,listRecipientTaxProfiles) retornavam 404. Descoberto via smoke real e corrigido em 2026-06-27.AbstractResource::download()agora segue HTTP 302/303/307 com headerLocationem uma segunda requisição cURL plain, sem enviarAuthorizationao CDN. A API NFE.io responde 302 + Location para o S3 pré-assinado em todos os downloads de PDF/XML; antes, o SDK tratava o 3xx como falha e lançavaInvalidRequestException.AbstractResource::send()agora só auto-throwa em>= 400. 3xx flui para o caller, permitindo quedownload()siga o redirect manualmente. 2xx (incluindo 202) continua fluindo como antes.CompaniesResource::listAll()agora começa empageIndex = 1. A API da NFE.io usa paginação 1-based (pageIndex >= 1); o valor antigo0causava HTTP 400 ("pageIndex must be greater or equal to 1") na primeira chamada. Descoberto via smoke test contra sandbox real.CompaniesResource::listAll()usapageSize = 50(era 100). A API rejeitapageCount > 50com HTTP 400 ("pageCount must be between 1 and 50").- Exemplos do README e
samples/service-invoice-list.phpatualizados para refletir a paginação 1-based. Docblocks delist()emCompaniesResourceeServiceInvoicesResourceagora documentam explicitamente o valor mínimo.
- Baseline em PHP 8.2+. Encerra suporte às versões 5.4 até 8.1.
- Autoload PSR-4 com namespace raiz
Nfe\emsrc/. - Mesmo pacote Composer da v2 (
nfe/nfe), agora servindo a major 3. A v2 permanece disponível via constraint^2.0; a v3 via^3.0. declare(strict_types=1)em todos os arquivos-fonte.- Pest 3, PHPStan nível 8 e PHP-CS-Fixer (PER-CS 2.0 + PHP 8.2 migration)
configurados em
require-deve impostos pelo CI. - Matriz de CI do GitHub Actions em PHP 8.2 / 8.3 / 8.4.
Nfe\Clientno estilo Stripe: ponto de entrada único com propriedades tipadas para cada recurso. Sem estado global, sem chamadas estáticas.Nfe\Configimutável (final readonly) eNfe\Environment(Production / Sandbox).Config::baseUrlForApi($familia)roteia recursos para seis hosts distintos (api.nfe.io, address.api.nfe.io/v2, nfe.api.nfe.io, legalentity.api.nfe.io, naturalperson.api.nfe.io, api.nfse.io) — paritário com o mapeamento canônico do SDK Node.dataApiKeyopcional emConfigeClientpara a API de serviços de dados (consultas de CEP/CNPJ/CPF, query de NF-e/NFC-e). Quando definida, o SDK roteia as famílias de recurso correspondentes para a chave de dados; quandonull, faz fallback paraapiKey— espelha a cadeiaresolveDataApiKey()do SDK Node.- Pilha HTTP zero-dependência:
Nfe\Http\CurlTransport(transporte cURL nativo) +Nfe\Http\RetryingTransport(retry com backoff exponencial e jitter simétrico, defaults alinhados ao Stripe-PHP). Nfe\Http\RequestOptionspermite override por-chamada deapiKey,baseUrletimeout(útil em integrações multi-tenant).- Slot opcional para
Psr\Log\LoggerInterface(PSR-3) e adaptador opcional paraPsr\Http\Client\ClientInterface(PSR-18) — nenhum dos dois é dependência em runtime.
- Emissão de notas (5):
serviceInvoices(NFS-e),productInvoices(NF-e),consumerInvoices(NFC-e — paridade-plus, além do Node SDK),transportationInvoices(CT-e),inboundProductInvoices(NF-e de entrada). - Consulta de notas (2):
productInvoiceQuery,consumerInvoiceQuery. - Entidades (4):
companies,legalPeople,naturalPeople,webhooks. - Consultas de dados (3):
addresses(CEP),legalEntityLookup(CNPJ),naturalPersonLookup(CPF). - Tributação (3):
taxCalculation,taxCodes,stateTaxes.
- Interfaces
Nfe\Response\PendingeNfe\Response\Issued<T>modelam a resposta assíncrona da NFE.io. Cada família de emissão (Service, Product, Consumer) tem suas implementações concretas (ServiceInvoicePending/ServiceInvoiceIssued, etc.) comresource(): Ttipado. Nfe\Util\FlowStatus::TERMINALlista os quatro estados terminais (Issued,IssueFailed,Cancelled,CancelFailed) — espelhaTERMINAL_FLOW_STATESdo SDK Node.Nfe\Util\ListResponse<T>+ListPage— wrapper de listagem (page-style ou cursor-style).
Nfe\Exception\ApiErrorExceptioncomo base, com subclasses:InvalidRequestException(400),AuthenticationException(401),AuthorizationException(403),NotFoundException(404),RateLimitException(429),ServerException(5xx).Nfe\Exception\ApiConnectionExceptionpara falhas de rede/DNS/TLS.Nfe\Exception\SignatureVerificationExceptionpara assinaturas de webhook inválidas.- Toda resposta não-2xx é mapeada automaticamente para a subclasse tipada
correspondente na camada de recurso. Antes, respostas 5xx podiam
emergir como DTOs com tudo
null; agora geram exceção.
Nfe\Webhook::constructEvent(payload, sigHeader, secret): WebhookEvent— preferido; valida HMAC-SHA1 sobreX-Hub-Signaturecom comparação timing-safe (hash_equals) e retorna o evento parseado.Nfe\Webhook::verifySignature()— variante low-level, paridade comvalidateSignaturedo SDK Node.Nfe\WebhookEventDTO readonly (type,data,id,createdAt).- Esquema confirmado com a API NFE.io em 2026-05-13.
- Specs OpenAPI versionadas em
openapi/(12 specs gerando 624 DTOs; 5 specs Swagger 2.0 e 1 sem schemas ficam fora do codegen e usam DTOs hand-written emsrc/Resource/Dto/). - Geração via
scripts/Generator(symfony/yaml+nikic/php-parser, ambosrequire-dev). DTOs gerados emsrc/Generated/— nunca edite à mão. - Comandos
composer generate(regenera) ecomposer generate:check(verifica drift no CI).
Nfe\Util\IdValidator— validação fail-fast paracompanyId,invoiceId,accessKey(44 dígitos),stateTaxId,eventKey,cnpj,cpf,cep,state(UF 2 letras).Nfe\Util\DateNormalizer— convertestring|DateTimeImmutableemYYYY-MM-DDpara endpoints que aceitam ambos os formatos.Nfe\Util\UserAgent— header padronizadoNfe-PHP/<versão>com sufixo opcional configurável (ex.:WHMCS/8.10).
- O desenvolvimento agora ocorre na branch
v3. Amasterestá congelada na v2.5.
lib/NFe/*(código legado autoloaded por classmap).- Integração com runner
test/simpletest/*. Pest substitui o SimpleTest. composer.pharversionado no repositório..travis.yml(substituído por.github/workflows/ci.yml).
Veja o histórico git na branch master. A linha v2 não recebe mais manutenção.