⚠️ Av1.0.0é uma reescrita greenfield, sem camada de compatibilidade. Nenhum símbolo da série0.xfoi mantido. Não existe modo de transição, alias ou shim: o código0.xnão compila contra a1.0. Migre de forma deliberada, recurso a recurso.
A 0.x está congelada no branch 0.x-legacy (sem backports — correções e
novos recursos só na 1.0+). O branch master passa a ser a v1.
A 1.0 substitui o estado global por instância (Nfe.api_key,
Nfe::ServiceInvoice.company_id) por um cliente único (Nfe::Client),
remove a dependência rest-client (agora zero dependências de runtime,
apenas a stdlib), exige Ruby 3.2+, troca os objetos dinâmicos por
value objects imutáveis (Data.define, snake_case) e introduz uma
hierarquia de erros tipada sob Nfe::Error. A emissão de notas passa a ter
um contrato assíncrono explícito (resposta 202 → Pending/Issued).
| Tema | 0.x (legado) |
1.0 |
|---|---|---|
| Entrada | API global: Nfe.api_key("...") + estado por classe |
Cliente único: Nfe::Client.new(api_key: "...") |
| Configuração | Nfe.configure { |c| c.url = ...; c.user_agent = ... } |
argumentos do construtor de Nfe::Client / Nfe::Configuration |
| Escopo de empresa | Nfe::ServiceInvoice.company_id("...") (estado mutável por classe) |
company_id: por chamada |
| Roteamento de host | uma única configuration.url global |
host roteado por recurso (multi-host automático) |
| Segunda chave de API | inexistente | data_api_key: com fallback para api_key |
| HTTP | dependência rest-client |
zero deps — Net::HTTP da stdlib |
| Modelos | NfeObject dinâmico (method_missing, reflesh_object) |
value objects imutáveis (Data.define), snake_case |
| Ruby | 2.x | 3.2 / 3.3 / 3.4 |
| Erros | Nfe::NfeError único (http_status, json_message) |
hierarquia tipada sob Nfe::Error |
| Emissão | create retornava o objeto (sem distinção de 202) |
create retorna *Pending (202) ou *Issued (201) |
| Downloads | download(id, :pdf).body (objeto RestClient) |
String binária (ASCII-8BIT) — exceto product_invoices (→ NfeFileResource) |
| Webhooks | esquema legado documentado (X-NFe-Signature / SHA-256) — errado |
X-Hub-Signature + HMAC-SHA1 sobre os bytes crus |
| Thread-safety | estado global compartilhado (não seguro) | um Nfe::Client é seguro para compartilhar entre threads |
Mudança de major — quebra intencional.
# 0.x — Gemfile
gem "nfe-io", "~> 0.3"
# 1.0 — Gemfile
gem "nfe-io", "~> 1.0"# require permanece igual
require "nfe-io" # ou require "nfe"A 1.0 não declara nenhuma dependência de runtime. As bibliotecas
rest-client e json (gem) deixam de ser exigidas; a 1.0 usa apenas a
biblioteca padrão: net/http, json, openssl, uri, securerandom,
stringio, time, zlib, cgi, date, base64.
0.x |
1.0 |
|
|---|---|---|
| Ruby mínimo | 2.x | 3.2 (required_ruby_version = ">= 3.2") |
| Testado em CI | — | 3.2, 3.3, 3.4 |
A 1.0 usa Data.define (Ruby 3.2+), métodos de endpoint de uma linha
(def x = ...) e argumentos nomeados de forma generalizada. Não há
compatibilidade com Ruby 2.x.
Na 0.x, a chave era um estado global de processo e a empresa um estado
mutável por classe. Na 1.0, ambos são explícitos: a chave vai no
construtor do Nfe::Client e a empresa em cada chamada.
# 0.x — estado global + estado por classe
Nfe.api_key("c73d49f9649046eeba36dcf69f6334fd")
Nfe::ServiceInvoice.company_id("55df4dc6b6cd9007e4f13ee8")
# 1.0 — cliente único; nenhum estado global
client = Nfe::Client.new(api_key: "c73d49f9649046eeba36dcf69f6334fd")Construtor completo (todos os argumentos são nomeados, com os defaults reais):
client = Nfe::Client.new(
api_key: "...", # chave principal; fallback ENV NFE_API_KEY
data_api_key: nil, # chave de dados; fallback ENV NFE_DATA_API_KEY
environment: :production, # SÍMBOLO :production|:development (reservado p/ uso futuro)
timeout: 30, # read timeout (segundos)
max_retries: 3, # tentativas após a inicial
logger: nil, # objeto com #info/#warn/#error
user_agent_suffix: nil # sufixo anexado ao User-Agent do SDK
)
environment:está reservado para uso futuro. Hoje é validado mas não altera endpoints, chaves ou comportamento. A separação produção/teste (homologação) é definida na configuração da sua conta em https://app.nfe.io (lado servidor) — não pela chave de API nem pelo SDK; não existe "URL de sandbox". É um símbolo (:production), não uma string.
A chave pode vir do argumento ou do ambiente — o argumento explícito sempre vence; um valor vazio é tratado como ausente.
ENV["NFE_API_KEY"] # usado por api_key quando o argumento é nil/vazio
ENV["NFE_DATA_API_KEY"] # usado por data_api_key quando o argumento é nil/vazioArgumentos que não estão no atalho do Client (como open_timeout,
base_url_overrides, ca_file, ca_path, proxy) vivem em
Nfe::Configuration. Monte a configuração e injete-a:
config = Nfe::Configuration.new(
api_key: "...",
open_timeout: 10, # connect timeout (s)
base_url_overrides: { main: "https://api.exemplo" }, # escape hatch por família
ca_file: "/caminho/ca-bundle.crt", # ADICIONA CAs ao trust store
proxy: "http://proxy.interno:3128"
)
client = Nfe::Client.new(configuration: config)
# quando configuration: é passado, os demais atalhos do Client são ignorados.TLS:
ca_file/ca_pathsó adicionam/substituem o bundle de CAs usado para verificar o servidor. Não existe API para desativar a verificação do peer (semVERIFY_NONE, seminsecure_ssl).
0.x |
1.0 |
|---|---|
Nfe.api_key("...") |
Nfe::Client.new(api_key: "...") |
Nfe.configure { |c| c.url = "..." } |
Nfe::Configuration.new(base_url_overrides: { ... }) |
Nfe.configure { |c| c.user_agent = "..." } |
Nfe::Client.new(user_agent_suffix: "...") |
Nfe.configuration / Nfe.access_keys |
(removidos — sem estado global) |
Nfe::Configuration#url (uma URL única) |
host roteado por recurso (ver §4.4) |
# 0.x — estado mutável por classe (não seguro entre threads)
Nfe::ServiceInvoice.company_id("55df4dc6b6cd9007e4f13ee8")
Nfe::ServiceInvoice.create(params)
# 1.0 — company_id é argumento de cada chamada
client.service_invoices.create(company_id: "55df4dc6b6cd9007e4f13ee8", data: { ... })Na 0.x havia uma única configuration.url (https://api.nfe.io). Na 1.0
cada recurso declara sua família e o host é resolvido automaticamente — você
não monta mais URLs.
| Família / host | Recursos roteados |
|---|---|
https://api.nfe.io (/v1) |
service_invoices, companies, legal_people, natural_people, webhooks, service_invoices_rtc |
https://api.nfse.io (/v2) |
product_invoices, consumer_invoices, transportation_invoices, inbound_product_invoices, tax_calculation, tax_codes, state_taxes, product_invoices_rtc |
https://address.api.nfe.io/v2 |
addresses |
https://legalentity.api.nfe.io |
legal_entity_lookup |
https://naturalperson.api.nfe.io |
natural_person_lookup |
https://nfe.api.nfe.io |
product_invoice_query, consumer_invoice_query |
Para sobrescrever um host (ambiente de testes, proxy reverso), use
base_url_overrides: em Nfe::Configuration (chave = símbolo da família).
Novidade da 1.0. As famílias de dados preferem data_api_key e caem para
api_key quando ela não é informada; todas as outras usam api_key.
client = Nfe::Client.new(api_key: "EMISSAO", data_api_key: "CONSULTA")Famílias que usam data_api_key (com fallback para api_key):
addresses(consulta de endereços)legal_entity_lookup(consulta de pessoa jurídica)natural_person_lookup(consulta de pessoa física)product_invoice_queryeconsumer_invoice_query(consulta de NF-e/NFC-e)
⚠️ A família:cte(api.nfse.io— emissão de NF-e/NFC-e/CT-e + tax-rules / tax-codes / state-taxes) usa aapi_keyprincipal e não é uma família de dados. Isso é uma divergência deliberada do SDK Node (que roteiaapi.nfse.iopela cadeia de fallback da chave de dados) — documentada emlib/nfe/configuration.rb.
Na 0.x você chamava métodos de classe diretamente (Nfe::ServiceInvoice,
Nfe::Company, ...). Na 1.0 tudo passa pelos acessores snake_case,
preguiçosos e memoizados, do Nfe::Client.
Classe 0.x |
Acessor 1.0 |
|---|---|
Nfe::ServiceInvoice |
client.service_invoices |
Nfe::Company |
client.companies |
Nfe::NaturalPeople |
client.natural_people |
Nfe::LegalPeople |
client.legal_people |
Nfe::NfeObject (base dinâmica) |
value objects imutáveis (Nfe::ServiceInvoice, Nfe::Company, ... — apenas leitura) |
Nfe::NfeError |
hierarquia Nfe::Error (ver §10) |
A 1.0 expõe 19 acessores (17 canônicos + 2 addons RTC), muito além das
classes da 0.x:
service_invoices product_invoice_query legal_entity_lookup
product_invoices consumer_invoice_query natural_person_lookup
consumer_invoices companies tax_calculation
transportation_invoices legal_people tax_codes
inbound_product_invoices natural_people state_taxes
webhooks addresses
service_invoices_rtc product_invoices_rtc (addons RTC)
⚠️ Note a colisão de nomes: na0.x,Nfe::ServiceInvoiceeNfe::Companyeram classes ativas (com métodos de criação). Na1.0,Nfe::ServiceInvoiceeNfe::Companysão apenas value objects de leitura; o comportamento mora nos acessoresclient.service_invoices/client.companies.
As notações abaixo refletem as assinaturas reais implementadas. Na 0.x, os
recursos compartilhavam operações genéricas (ApiOperations::Create, List,
Retrieve, Cancel, Update, Download). Na 1.0, cada método é explícito.
0.x |
1.0 |
|---|---|
Nfe::ServiceInvoice.create(params) |
client.service_invoices.create(company_id:, data:, idempotency_key: nil, request_options: nil) |
Nfe::ServiceInvoice.list_all(params) |
client.service_invoices.list(company_id:, **options) |
Nfe::ServiceInvoice.retrieve(id) |
client.service_invoices.retrieve(company_id:, invoice_id:) |
Nfe::ServiceInvoice.cancel(id) |
client.service_invoices.cancel(company_id:, invoice_id:) |
Nfe::ServiceInvoice.update(...) |
client.service_invoices.send_email(company_id:, invoice_id:) (e demais ações dedicadas) |
Nfe::ServiceInvoice.download(id, :pdf) |
client.service_invoices.download_pdf(company_id:, invoice_id: nil) |
Nfe::ServiceInvoice.download(id, :xml) |
client.service_invoices.download_xml(company_id:, invoice_id: nil) |
| — (sem equivalente) | client.service_invoices.get_status(company_id:, invoice_id:) → StatusResult |
Opções de list: page_index, page_count, issued_begin, issued_end,
created_begin, created_end, has_totals (paginação page-style).
download_pdf/download_xml com invoice_id: nil baixam o ZIP da empresa.
0.x |
1.0 |
|---|---|
Nfe::Company.create(params) |
client.companies.create(data) |
Nfe::Company.list_all(params) |
client.companies.list(page_index: 0, page_count: 100) |
| — | client.companies.list_all / client.companies.list_each (auto-paginação) |
Nfe::Company.retrieve(id) |
client.companies.retrieve(company_id) |
Nfe::Company.update(...) |
client.companies.update(company_id, data) |
Nfe::Company + ApiOperations::Delete (delete) |
client.companies.remove(company_id) → { deleted: true, id: } |
| — | client.companies.find_by_tax_number(tax_number) |
| — | client.companies.find_by_name(name) |
Certificado digital (novo na 1.0):
client.companies.validate_certificate(file:, password:) # local, sem HTTP
client.companies.upload_certificate(company_id, file:, password:, filename: nil)
client.companies.replace_certificate(company_id, file:, password:, filename: nil) # alias de upload
client.companies.get_certificate_status(company_id)
client.companies.check_certificate_expiration(company_id, threshold_days: 30)
client.companies.get_companies_with_certificates
client.companies.get_companies_with_expiring_certificates(threshold_days: 30)
⚠️ deletevirouremove. Emcompanies, a remoção chama-se#remove(paridade com os SDKs Node/PHP), não#delete.
0.x |
1.0 |
|---|---|
Nfe::LegalPeople.company_id(id) + list_all |
client.legal_people.list(company_id) |
Nfe::LegalPeople.retrieve(id) |
client.legal_people.retrieve(company_id, legal_person_id) |
— (não existia na 0.x) |
client.legal_people.create(company_id, data) |
| — | client.legal_people.update(company_id, legal_person_id, data) |
| — | client.legal_people.delete(company_id, legal_person_id) |
| — | client.legal_people.create_batch(company_id, list) (sequencial) |
| — | client.legal_people.find_by_tax_number(company_id, federal_tax_number) |
0.x |
1.0 |
|---|---|
Nfe::NaturalPeople.company_id(id) + list_all |
client.natural_people.list(company_id) |
Nfe::NaturalPeople.retrieve(id) |
client.natural_people.retrieve(company_id, natural_person_id) |
| — | client.natural_people.create(company_id, data) |
| — | client.natural_people.update(company_id, natural_person_id, data) |
| — | client.natural_people.delete(company_id, natural_person_id) |
| — | client.natural_people.create_batch(company_id, list) |
| — | client.natural_people.find_by_tax_number(company_id, federal_tax_number) |
Product invoices (NF-e) — client.product_invoices:
create(company_id:, data:, idempotency_key: nil, request_options: nil)
create_with_state_tax(company_id:, state_tax_id:, data:, idempotency_key: nil, request_options: nil)
list(company_id:, environment:, **options) # environment: "Production"/"Test" OBRIGATÓRIO
retrieve(company_id:, invoice_id:)
cancel(company_id:, invoice_id:, reason: nil)
list_items(company_id:, invoice_id:, limit: nil, starting_after: nil)
list_events(company_id:, invoice_id:, limit: nil, starting_after: nil)
send_correction_letter(company_id:, invoice_id:, reason:) # reason 15..1000 chars
disable(company_id:, invoice_id:, reason: nil)
disable_range(company_id:, data:)
download_pdf / download_xml / download_rejection_xml / download_epec_xml # → NfeFileResource (URI)
download_correction_letter_pdf / download_correction_letter_xml # → NfeFileResource (URI)Consumer invoices (NFC-e) — client.consumer_invoices: create,
create_with_state_tax, list, retrieve, cancel, list_items,
list_events, disable_range, download_pdf, download_xml,
download_rejection_xml. (Sem send_correction_letter, download_epec_xml ou
disable por documento — restrições da legislação para NFC-e.)
Transportation invoices (CT-e recebidos) — client.transportation_invoices:
enable, disable, get_settings, retrieve(company_id:, access_key:),
download_xml, get_event, download_event_xml.
Inbound product invoices (NF-e recebidas) — client.inbound_product_invoices:
enable_auto_fetch, disable_auto_fetch, get_settings, get_details,
get_product_invoice_details, get_event_details,
get_product_invoice_event_details, get_xml, get_event_xml, get_pdf,
get_json, manifest(company_id:, access_key:, tp_event: 210210),
reprocess_webhook.
Consultas — client.product_invoice_query: retrieve(access_key),
download_pdf(access_key), download_xml(access_key),
list_events(access_key). client.consumer_invoice_query:
retrieve(access_key), download_xml(access_key).
Lookups (famílias de dados):
client.addresses.lookup_by_postal_code(cep)
client.addresses.lookup_by_term(term)
client.addresses.search(filter: nil)
client.legal_entity_lookup.get_basic_info(cnpj, update_address: nil, update_city_code: nil)
client.legal_entity_lookup.get_state_tax_info(state, cnpj)
client.legal_entity_lookup.get_state_tax_for_invoice(state, cnpj)
client.legal_entity_lookup.get_suggested_state_tax_for_invoice(state, cnpj)
client.natural_person_lookup.get_status(cpf, birth_date)Fiscais (família :cte):
client.tax_calculation.calculate(tenant_id, request) # request: Hash
client.tax_codes.list_operation_codes(page_index: nil, page_count: nil)
client.tax_codes.list_acquisition_purposes(...)
client.tax_codes.list_issuer_tax_profiles(...)
client.tax_codes.list_recipient_tax_profiles(...)
client.state_taxes.list(company_id, starting_after: nil, ending_before: nil, limit: nil)
client.state_taxes.create(company_id, data)
client.state_taxes.retrieve(company_id, state_tax_id)
client.state_taxes.update(company_id, state_tax_id, data)
client.state_taxes.delete(company_id, state_tax_id)RTC (Reforma Tributária do Consumo) — addons da 1.0:
client.service_invoices_rtc (create, retrieve, cancel,
download_cancellation_xml) e client.product_invoices_rtc (mesmo conjunto do
product_invoices). O layout RTC é selecionado pela presença do grupo
ibsCbs (NFS-e) ou items[].tax.IBSCBS (NF-e/NFC-e) no payload — mesmo
endpoint dos recursos clássicos, sem header/param discriminador.
⚠️ listde product invoices exigeenvironment:. Emclient.product_invoices.listeclient.product_invoices_rtc.list,environment:é uma String separada ("Production"/"Test"), distinta doenvironment:doClient(que é um símbolo e seleciona a chave). Omiti-la levantaNfe::InvalidRequestError.
A 0.x dependia da gem rest-client; a 1.0 usa Net::HTTP da stdlib. Toda
exceção do rest-client que vazava do SDK desaparece.
0.x (rest-client) |
1.0 |
|---|---|
RestClient::Exception (rede/conexão) |
Nfe::ApiConnectionError |
| timeout de conexão/leitura | Nfe::TimeoutError (subclasse de ApiConnectionError) |
RestClient::ExceptionWithResponse (HTTP 4xx/5xx) |
subclasse de Nfe::Error por status (ver §10) |
request.execute retornando o objeto-resposta |
objetos hidratados (Data.define) ou bytes (downloads) |
# 0.x
begin
Nfe::ServiceInvoice.create(params)
rescue RestClient::Exception => e
# tratar erro de rede/HTTP
end
# 1.0
begin
client.service_invoices.create(company_id:, data:)
rescue Nfe::ApiConnectionError => e # rede (inclui TimeoutError)
# ...
rescue Nfe::Error => e # qualquer erro do SDK
# ...
endA 0.x tinha um único Nfe::NfeError(http_status, json_message, http_message, message). A 1.0 traz uma hierarquia tipada sob Nfe::Error — capture a
base para pegar toda a família, ou subclasses específicas para tratar casos.
begin
client.service_invoices.create(company_id:, data:)
rescue Nfe::RateLimitError => e
sleep(e.retry_after || 5); retry
rescue Nfe::InvalidRequestError => e
logger.warn(e.to_h) # to_h é seguro para log (sem corpo/headers crus)
rescue Nfe::Error => e
# rede comum: status_code, request_id, error_code, response_body, response_headers
endClasse 1.0 |
Status HTTP | Quando |
|---|---|---|
Nfe::Error |
— | base de toda a hierarquia |
Nfe::AuthenticationError |
401 | chave ausente ou inválida |
Nfe::AuthorizationError |
403 | chave válida, mas sem permissão |
Nfe::InvalidRequestError |
400 / 422 | requisição malformada ou inválida |
Nfe::NotFoundError |
404 | recurso inexistente |
Nfe::ConflictError |
409 | conflito com o estado atual |
Nfe::RateLimitError |
429 | excesso de requisições (#retry_after) |
Nfe::ServerError |
5xx | falha do servidor |
Nfe::ApiConnectionError |
— | falha de rede (DNS, conexão, TLS, reset) |
Nfe::TimeoutError |
— | timeout (subclasse de ApiConnectionError) |
Nfe::SignatureVerificationError |
— | assinatura de webhook inválida ou payload não-JSON |
Nfe::ConfigurationError |
— | SDK mal configurado (chave ausente, environment inválido) — antes de qualquer HTTP |
Nfe::InvoiceProcessingError |
— | resposta 202 sem Location utilizável |
Atributos disponíveis em erros derivados de resposta: status_code,
request_id, error_code, response_body, response_headers. O método
#to_h é seguro para log (omite response_body/response_headers, que
podem conter segredos/PII).
⚠️ Nomes que não existem:Nfe::ConnectionError,Nfe::ValidationError,Nfe::PollingTimeoutError. Use os nomes da tabela acima.
Na 0.x, create simplesmente devolvia o objeto, sem distinguir emissão
síncrona de assíncrona. Na 1.0 o contrato é explícito: create retorna um
resultado discriminado.
- HTTP 202 →
*Pending(invoice_id,location;pending? == true,issued? == false). A nota está enfileirada; oinvoice_idvem do headerLocation. - HTTP 201/200 →
*Issued(resource;issued? == true,pending? == false). A nota foi materializada na hora.
(Recursos: ServiceInvoicePending/ServiceInvoiceIssued,
ProductInvoicePending/ProductInvoiceIssued, e as variantes Consumer e RTC.)
result = client.service_invoices.create(company_id:, data:)
case result
in Nfe::Resources::ServiceInvoicePending => pending
# acompanhar via polling (ver abaixo)
in Nfe::Resources::ServiceInvoiceIssued => issued
nota = issued.resource
endNão há create_and_wait/create_batch na 1.0 (ver §12). Faça o polling
manualmente com Nfe::FlowStatus.terminal?:
result = client.service_invoices.create(company_id:, data:)
if result.pending?
invoice = nil
loop do
invoice = client.service_invoices.retrieve(
company_id:, invoice_id: result.invoice_id
)
break if Nfe::FlowStatus.terminal?(invoice.flow_status)
sleep 2
end
# invoice.flow_status ∈ Issued | IssueFailed | Cancelled | CancelFailed
endEstados terminais (param o polling): Issued, IssueFailed, Cancelled,
CancelFailed. Para service_invoices há ainda o atalho
client.service_invoices.get_status(company_id:, invoice_id:), que devolve um
StatusResult com #complete? e #failed? (derivado de retrieve, sem HTTP
extra além do retrieve).
⚠️ A documentação antiga falava em headerX-NFe-Signature, HMAC-SHA256 e Base64. Isso está errado para o SDK1.0(e para o que a NFE.io de fato envia). O esquema correto éX-Hub-Signature+ HMAC-SHA1 sobre os bytes crus do corpo, em hex.
A verificação é estática (não precisa de Nfe::Client, não lê configuração,
não faz rede):
raw = request.body.read # BYTES CRUS — leia ANTES de parsear o JSON
sig = request.get_header("HTTP_X_HUB_SIGNATURE") # ou request.headers["X-Hub-Signature"]
if Nfe::Webhook.verify_signature(payload: raw, signature: sig, secret: ENV["NFE_WEBHOOK_SECRET"])
event = Nfe::Webhook.construct_event(payload: raw, signature: sig, secret: ENV["NFE_WEBHOOK_SECRET"])
# event.type, event.data, event.id, event.created_at
endPontos críticos:
- Bytes crus. A NFE.io assina exatamente os bytes que entregou. Leia
request.body.read(ourequest.raw_post) antes de parsear. Nunca re-serialize um objeto parseado (payload.to_json) — ordem de chaves e espaços diferem dos bytes assinados e a verificação falha de forma imprevisível. - HMAC-SHA1, hex, prefixo
sha1=. Comparação case-insensitive e timing-safe (OpenSSL.secure_compare). Um headersha256=é rejeitado. verify_signatureNUNCA levanta exceção: retornafalsepara qualquer entrada ausente, malformada, algoritmo errado ou não-hex.construct_eventlevantaNfe::SignatureVerificationErrorquando a assinatura não confere ou o payload não é JSON válido.- Validade ≠ frescor. A NFE.io não envia timestamp/nonce anti-replay. Uma
assinatura válida prova autenticidade, não frescor — seus handlers
precisam ser idempotentes e deduplicar por
event.id(ou pelo id da nota).
Há ainda o atalho client.webhooks.verify_signature(payload:, signature:, secret:), mera delegação a Nfe::Webhook (a API canônica é o módulo).
Na 0.x, download(id, :pdf) retornava o objeto de resposta do rest-client
(você acessava .body). Na 1.0, os métodos de download retornam a String
binária diretamente (encoding ASCII-8BIT), pronta para File.binwrite ou
send_data.
# 0.x
bytes = Nfe::ServiceInvoice.download("59443a...", :pdf).body
# 1.0 — String binária (bytes), sem .body
bytes = client.service_invoices.download_pdf(company_id: "55df...", invoice_id: "59443a...")
File.binwrite("nota.pdf", bytes)Retornam String binária (ASCII-8BIT): service_invoices,
consumer_invoices, transportation_invoices, inbound_product_invoices,
product_invoice_query, consumer_invoice_query (e
service_invoices_rtc.download_cancellation_xml).
⚠️ Exceção —product_invoiceseproduct_invoices_rtc. Seusdownload_*não retornam bytes: retornam umNfe::NfeFileResource(value object comuri,name,content_type,size). O hostapi.nfse.io/v2responde com um envelope JSON{ uri }apontando para o arquivo — baixe os bytes a partir deresource.uripor conta própria.
file = client.product_invoices.download_pdf(company_id:, invoice_id:)
file.uri # => "https://.../danfe.pdf" (não são bytes)Funcionalidades que existiam (ou se esperariam) e foram deliberadamente
adiadas na 1.0, com o respectivo contorno:
| Diferido | Contorno na 1.0 |
|---|---|
create_and_wait (emitir e aguardar) |
emitir + loop de polling com Nfe::FlowStatus.terminal? (ver §9) |
create_batch para notas |
iterar create na sua aplicação (há create_batch apenas em legal_people/natural_people, e sequencial) |
getStatus como chamada dedicada |
derivado de retrieve: client.service_invoices.get_status(...) (sem HTTP extra) ou Nfe::FlowStatus.terminal?(invoice.flow_status) |
| upload/replace de certificado via multipart "rico" | upload_certificate / replace_certificate já existem (multipart simples + validação local fail-fast); não há ainda fluxo multipart estendido |
validate de certificado server-side |
validate_certificate(file:, password:) valida localmente (sem HTTP) |
require "nfe-io"
Nfe.api_key("c73d49f9649046eeba36dcf69f6334fd")
Nfe::ServiceInvoice.company_id("55df4dc6b6cd9007e4f13ee8")
invoice = Nfe::ServiceInvoice.create(
borrower: { name: "Cliente Exemplo", federalTaxNumber: 12_345_678_000_199 },
cityServiceCode: "2690",
description: "Serviço de consultoria",
servicesAmount: 100.0
)
# sem distinção de 202; sem contrato de polling padronizado
pdf = Nfe::ServiceInvoice.download(invoice["id"], :pdf).body
File.open("nota.pdf", "wb") { |f| f.write(pdf) }require "nfe-io"
client = Nfe::Client.new(api_key: "c73d49f9649046eeba36dcf69f6334fd")
company_id = "55df4dc6b6cd9007e4f13ee8"
result = client.service_invoices.create(
company_id: company_id,
data: {
borrower: { name: "Cliente Exemplo", federalTaxNumber: "12345678000199" },
cityServiceCode: "2690",
description: "Serviço de consultoria",
servicesAmount: 100.0
}
)
invoice =
if result.pending?
loop do
current = client.service_invoices.retrieve(
company_id: company_id, invoice_id: result.invoice_id
)
break current if Nfe::FlowStatus.terminal?(current.flow_status)
sleep 2
end
else
result.resource
end
unless invoice.flow_status == "Issued"
raise "emissão falhou: #{invoice.flow_status}"
end
bytes = client.service_invoices.download_pdf(
company_id: company_id, invoice_id: invoice.id
)
File.binwrite("nota.pdf", bytes)Um único Nfe::Client é seguro para compartilhar entre threads (cada
acessor de recurso e cada transport é memoizado sob Mutex). Memoize-o:
# config/initializers/nfe.rb
require "nfe-io"
module NfeClient
def self.instance
@instance ||= Nfe::Client.new(
api_key: Rails.application.credentials.dig(:nfe, :api_key),
data_api_key: Rails.application.credentials.dig(:nfe, :data_api_key),
environment: Rails.env.production? ? :production : :development,
logger: Rails.logger
)
end
end# app/controllers/nfe_webhooks_controller.rb
class NfeWebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
raw = request.raw_post # BYTES CRUS — não parsear antes
sig = request.headers["X-Hub-Signature"]
secret = Rails.application.credentials.dig(:nfe, :webhook_secret)
event = Nfe::Webhook.construct_event(payload: raw, signature: sig, secret: secret)
# idempotência: a NFE.io não envia anti-replay — deduplique por event.id
return head(:ok) if WebhookReceipt.exists?(event_id: event.id)
WebhookReceipt.create!(event_id: event.id, event_type: event.type)
ProcessNfeEventJob.perform_later(event.type, event.data)
head :ok
rescue Nfe::SignatureVerificationError
head :bad_request
end
endEntrada e configuração
- Removido o estado global:
Nfe.api_key,Nfe.configure,Nfe.configuration,Nfe.access_keys. UseNfe::Client.new(api_key:). - Removido o estado mutável por classe (
Nfe::ServiceInvoice.company_id(...)).company_id:agora é argumento de cada chamada. - Removida a URL global única; o host é roteado por recurso (multi-host).
- Novo
data_api_key:(com fallback paraapi_key) para famílias de dados. environment:(:production/:development) é um símbolo reservado para uso futuro — hoje sem efeito; produção/teste é definido na conta em app.nfe.io.
Dependências e runtime
- Removida a dependência
rest-client(e a gemjson). Zero deps de runtime. - Ruby mínimo passa de 2.x para 3.2.
Modelos e API
NfeObjectdinâmico → value objects imutáveis (Data.define), atributossnake_case.Nfe::ServiceInvoice/Nfe::Companydeixam de ser classes ativas e viram value objects de leitura; o comportamento mora nos acessores doClient.companiesusa#remove(não#delete).createde notas retorna um resultado discriminado*Pending/*Issued(antes retornava o objeto direto).listdeproduct_invoices/product_invoices_rtcexigeenvironment:(String"Production"/"Test").
Erros
Nfe::NfeErrorúnico → hierarquia tipada sobNfe::Error.- Exceções do
rest-client→Nfe::ApiConnectionError/Nfe::TimeoutError.
Webhooks
- Esquema corrigido para
X-Hub-Signature+ HMAC-SHA1 sobre bytes crus (documentação antiga comX-NFe-Signature/SHA-256/Base64 estava errada). - Handlers precisam ser idempotentes (sem anti-replay no provedor).
Downloads
- Retorno passa a ser
Stringbinária (ASCII-8BIT), sem.body. - Exceção:
product_invoices/product_invoices_rtcretornamNfe::NfeFileResource(uri), não bytes.
Recursos diferidos
- Sem
create_and_wait/create_batchpara notas; faça polling comNfe::FlowStatus.terminal?.