Resumo
ServiceInvoicesResource::isDuplicateExternalId() identifica a rejeição de externalId duplicado por match em texto livre da mensagem de erro. Isso funciona hoje, mas quebra silenciosamente se a API mudar a redação, a capitalização ou o idioma da mensagem — e o modo de falha é perigoso, porque o método passa a devolver false e o integrador conclui que nenhuma nota foi criada.
Verificado na v3.5.0.
Onde
src/Resource/ServiceInvoicesResource.php:
public static function isDuplicateExternalId(ApiErrorException $e): bool
{
if ($e->statusCode !== 400) {
return false;
}
$haystack = strtolower(($e->responseBody ?? '') . ' ' . $e->getMessage());
return str_contains($haystack, 'external id') && str_contains($haystack, 'already exists');
}
O próprio docblock já reconhece a fragilidade ("A mensagem pode chegar no corpo cru ou (futuramente) no campo message").
O comportamento confirmado ao vivo
Vale registrar porque diverge da documentação da API. Testado em 2026-09-02 contra conta de desenvolvimento, empresa com environment = Development, rota POST /v1/companies/{id}/serviceinvoices:
- Emitida uma NFS-e com
externalId: "WOO-NFE-101" → 202, nota criada e emitida.
- Nota cancelada via
cancel() → flowStatus: "Cancelled".
- Reenviada a criação com o mesmo
externalId: "WOO-NFE-101".
Resultado: 400, Nfe\Exception\InvalidRequestException, e isDuplicateExternalId() retornou true.
A documentação da NFe.io sobre idempotência descreve o comportamento oposto — "em vez de criarmos uma nova nota, retornaremos os dados da nota fiscal original". Nesta rota o que ocorre é rejeição, não replay.
Isso não é um defeito do SDK — o SDK está certo e a documentação é que está desatualizada — mas reforça que integradores vão depender desse método, e que ele precisa ser confiável.
Pedido
Um código de erro estável para essa rejeição, exposto em ApiErrorException::$errorCode (o campo já existe), de modo que isDuplicateExternalId() possa comparar um identificador em vez do texto. Algo como external_id_already_exists.
Enquanto o código não existir, uma melhoria barata no SDK seria tornar o match mais tolerante — hoje str_contains($haystack, 'external id') não casa com externalId, external_id nem com a mensagem traduzida.
Sugestão adicional para a documentação
Alinhar a página de idempotência com o comportamento real da rota, ou explicitar em quais rotas/layouts vale replay e em quais vale rejeição. A diferença é relevante: quem assume replay conclui que reemitir é seguro e idempotente; na prática a segunda emissão falha.
Contexto
Encontrado ao migrar o plugin nfe/woo-nfe para o nfe/nfe 3.5. O plugin enviava externalId fixo por pedido, o que tornava impossível reemitir após cancelamento — corrigido para externalId por tentativa.
Resumo
ServiceInvoicesResource::isDuplicateExternalId()identifica a rejeição deexternalIdduplicado por match em texto livre da mensagem de erro. Isso funciona hoje, mas quebra silenciosamente se a API mudar a redação, a capitalização ou o idioma da mensagem — e o modo de falha é perigoso, porque o método passa a devolverfalsee o integrador conclui que nenhuma nota foi criada.Verificado na v3.5.0.
Onde
src/Resource/ServiceInvoicesResource.php:O próprio docblock já reconhece a fragilidade ("A mensagem pode chegar no corpo cru ou (futuramente) no campo message").
O comportamento confirmado ao vivo
Vale registrar porque diverge da documentação da API. Testado em 2026-09-02 contra conta de desenvolvimento, empresa com
environment = Development, rotaPOST /v1/companies/{id}/serviceinvoices:externalId: "WOO-NFE-101"→202, nota criada e emitida.cancel()→flowStatus: "Cancelled".externalId: "WOO-NFE-101".Resultado:
400,Nfe\Exception\InvalidRequestException, eisDuplicateExternalId()retornoutrue.A documentação da NFe.io sobre idempotência descreve o comportamento oposto — "em vez de criarmos uma nova nota, retornaremos os dados da nota fiscal original". Nesta rota o que ocorre é rejeição, não replay.
Isso não é um defeito do SDK — o SDK está certo e a documentação é que está desatualizada — mas reforça que integradores vão depender desse método, e que ele precisa ser confiável.
Pedido
Um código de erro estável para essa rejeição, exposto em
ApiErrorException::$errorCode(o campo já existe), de modo queisDuplicateExternalId()possa comparar um identificador em vez do texto. Algo comoexternal_id_already_exists.Enquanto o código não existir, uma melhoria barata no SDK seria tornar o match mais tolerante — hoje
str_contains($haystack, 'external id')não casa comexternalId,external_idnem com a mensagem traduzida.Sugestão adicional para a documentação
Alinhar a página de idempotência com o comportamento real da rota, ou explicitar em quais rotas/layouts vale replay e em quais vale rejeição. A diferença é relevante: quem assume replay conclui que reemitir é seguro e idempotente; na prática a segunda emissão falha.
Contexto
Encontrado ao migrar o plugin nfe/woo-nfe para o
nfe/nfe3.5. O plugin enviavaexternalIdfixo por pedido, o que tornava impossível reemitir após cancelamento — corrigido paraexternalIdpor tentativa.