FocusNfe
Cliente Ruby não-oficial para a API da Focus NFe — serviço brasileiro de emissão de documentos fiscais eletrônicos (NFe, NFCe, NFSe, CTe, MDFe, NFCom, DCe e outros).
⚠️ Não-oficial. Esta gem não tem vínculo com a Focus NFe. A autoridade final sobre campos, regras e validações fiscais é sempre a API da Focus/SEFAZ.
A gem é uma camada fina sobre HTTP: transporta JSON, autentica, trata os status HTTP em erros tipados e devolve objetos Ruby úteis. Não reimplementa regras fiscais. Não tem dependências de runtime (usa apenas a stdlib).
Cobre:
- Documentos emitidos —
nfe,nfce,nfse,nfse_nacional,cte,cte_os,mdfe,nfcom,dce,nfgas. - Documentos recebidos —
nfes_recebidas,ctes_recebidas,nfses_nacionais_recebidas(listagem com sincronização incremental, consulta, downloads, manifestação e eventos). - APIs auxiliares (somente leitura, autenticadas pelo token da conta) —
ceps,municipios,cfops,cnaes,ncms,cnpjs. - APIs de gestão —
empresas(token da conta);webhooks,emails_bloqueados,backups(token da empresa).
Instalação
Adicione a gem ao Gemfile da aplicação:
bundle add focus_nfe
Ou instale diretamente:
gem install focus_nfe
Configuração
Os dois tokens da Focus NFe
A API usa dois tokens distintos, e a gem os separa:
token_empresa— identifica a empresa que emite/consulta o documento. Autentica todos os documentos (nfe,nfce, …, e as recebidas) e as APIs de gestão por empresa (webhooks,emails_bloqueados,backups).token_conta— token da conta. Autentica as consultas auxiliares (ceps,municipios,cfops,cnaes,ncms,cnpjs) e a gestão de empresas (empresas).
Configure só o que for usar: um cliente só com token_empresa emite documentos;
acessar um recurso de conta sem token_conta levanta ConfigurationError (e
vice-versa), antes de qualquer ida à rede.
Há dois modos de uso, que coexistem.
Global — para aplicações de uma empresa só
FocusNfe.configure do |config|
config.token_empresa = ENV["FOCUS_NFE_TOKEN_EMPRESA"]
config.token_conta = ENV["FOCUS_NFE_TOKEN_CONTA"] # opcional (consultas auxiliares/empresas)
config.environment = :producao # ou :homologacao (padrão)
config.timeout = 30
config.logger = Rails.logger
end
client = FocusNfe.client # usa a config global
Explícito — várias empresas no mesmo processo
O token_empresa é por empresa; cada Client carrega seus próprios tokens e
ambiente, sem estado compartilhado. O token_conta, quando usado, é o mesmo da
conta que agrupa as empresas.
loja = FocusNfe::Client.new(token_empresa: "TOKEN_LOJA", environment: :producao)
filial = FocusNfe::Client.new(token_empresa: "TOKEN_FILIAL", environment: :homologacao)
# Consultas auxiliares e gestão de empresas usam o token da conta:
conta = FocusNfe::Client.new(token_conta: "TOKEN_CONTA", environment: :producao)
conta.cnpjs.consultar("12345678000123")
conta.empresas.criar(dados: dados_empresa, dry_run: true)
O ambiente resolve a URL base (o prefixo /v2 é interno):
:producao→https://api.focusnfe.com.br:homologacao→https://homologacao.focusnfe.com.br
Logger
Logging é opt-in: por padrão config.logger é nil e nada é emitido. Plugue qualquer
logger compatível com o Logger da stdlib (responde a debug/info/warn/error),
como Rails.logger ou Logger.new($stdout):
config.logger = Logger.new($stdout)
A gem registra cada requisição (debug), resposta (info/warn) e falha (error). O
Authorization é sempre redigido ([FILTERED]) e o corpo da requisição nunca é logado —
dados sensíveis não vazam.
Uso
Emissão e ciclo assíncrono
A emissão é assíncrona na maioria dos documentos. A ref é a referência única do
documento na sua aplicação (validada client-side como alfanumérica antes do
envio). As respostas de emissão e consulta são encapsuladas em
FocusNfe::Modelos::Documento.
doc = client.nfe.emitir(ref: "pedido-42", dados: payload_nfe)
doc.status # => "processando_autorizacao"
doc.processando? # => true
doc.ref # => "pedido-42"
# Acompanhamento por polling (ou via webhooks — ver Gestão).
doc = client.nfe.consultar("pedido-42")
if doc.autorizado?
doc.chave_nfe
doc.caminho_xml_nota_fiscal
doc.caminho_danfe
elsif doc.erro?
doc.status_sefaz
doc.mensagem_sefaz
end
Predicados de status disponíveis: autorizado?, cancelado?, processando?,
erro?, denegado?. Campos não mapeados continuam acessíveis via doc["campo"]
ou doc.dados.
A NFC-e é síncrona — o resultado já vem na própria chamada de emissão:
nota = client.nfce.emitir(ref: "venda-1001", dados: payload_nfce)
nota.autorizado? # => true/false na mesma chamada
Cancelamento
client.nfe.cancelar("pedido-42", justificativa: "Cliente desistiu da compra.")
Documentos recebidos e sincronização incremental
listar devolve uma FocusNfe::Modelos::Pagina (enumerável). O cabeçalho
X-Max-Version é exposto em versao_maxima, para retomar a sincronização do
ponto onde parou.
pagina = client.nfes_recebidas.listar(cnpj: "12345678000123", versao: ultima_versao)
pagina.cada { |nfe| processar(nfe) }
proxima_versao = pagina.versao_maxima
# Consulta, downloads e manifestação do destinatário:
client.nfes_recebidas.consultar(chave, completa: true)
xml = client.nfes_recebidas.download_xml(chave)
pdf = client.nfes_recebidas.download_pdf(chave)
client.nfes_recebidas.manifestar(chave, tipo: "confirmacao")
APIs auxiliares
Autenticadas pelo token_conta (ver Configuração):
client.ceps.consultar("69909032")
client.cnpjs.consultar("12345678000123")
client.ncms.consultar("01012100")
APIs de gestão
# Cadastro de empresa (apenas produção); dry_run valida sem persistir.
client.empresas.criar(dados: dados_empresa, dry_run: true)
# Webhooks (a gem registra o gatilho e processa a chamada de volta).
client.webhooks.criar(dados: {
event: "nfe",
url: "https://meu.app/hooks/nfe",
cnpj: "12345678000123",
authorization_header: "X-Focus-Authorization", # header que a Focus enviará no callback
authorization: "um-segredo-forte" # valor esperado nesse header
})
Recebendo webhooks (inbound)
Quando a Focus muda o status de um documento, ela chama a URL cadastrada. A gem
converte o corpo recebido no mesmo Modelos::Documento de emissão/consulta e
autentica a chamada comparando o header com o authorization do gatilho:
# Em um controller Rails:
def focus_callback
autenticado = FocusNfe::Webhook.autenticado?(
headers: request.headers,
authorization: ENV.fetch("FOCUS_WEBHOOK_AUTH"),
authorization_header: "X-Focus-Authorization"
)
return head(:unauthorized) unless autenticado
documento = FocusNfe::Webhook.parse(request.raw_post)
AtualizaNota.call(ref: documento.ref) if documento.autorizado?
head :ok
end
parse aceita a String crua ou um Hash já parseado e levanta
FocusNfe::Errors::WebhookError se o corpo não for JSON válido.
Erros tipados
Cada faixa de status HTTP vira uma exceção específica, todas descendentes de
FocusNfe::Error. Cada exceção carrega status, body (mensagens da API) e a
response original.
| Status | Exceção | Significado |
|---|---|---|
| 400 | FocusNfe::Errors::BadRequest |
Requisição malformada |
| 401 | FocusNfe::Errors::Unauthorized |
Token ausente ou inválido |
| 403 | FocusNfe::Errors::Forbidden |
Sem permissão |
| 404 | FocusNfe::Errors::NotFound |
Recurso inexistente |
| 409 | FocusNfe::Errors::Conflict |
Conflito de estado (ex.: ref em uso) |
| 422 | FocusNfe::Errors::ValidationError |
Erro de validação dos campos |
| 429 | FocusNfe::Errors::RateLimited |
Limite de requisições excedido |
| 5xx | FocusNfe::Errors::ServerError |
Falha no servidor da Focus/SEFAZ |
begin
client.nfe.emitir(ref: "pedido-42", dados: payload)
rescue FocusNfe::Errors::ValidationError => e
e.status # => 422
e.body # => mensagens de erro da API
rescue FocusNfe::Error => e
# captura qualquer falha da gem
end
Há ainda ConfigurationError (token/ambiente inválidos, client-side) e
ConnectionError (timeout, conexão recusada, excesso de redirects).
Validação opt-in por schemas
Os campos de emissão derivam dos schemas documentados em
campos.focusnfe.com.br (empacotados em data/schemas/). A validação
client-side é opcional e desligada por padrão — a Focus é a autoridade final
e os campos mudam (ex.: Reforma Tributária em transição).
client.nfe.emitir(ref: "pedido-42", dados: payload, validar: true)
# => levanta FocusNfe::Esquemas::ErroDeValidacao se faltar obrigatório
# ou o tipo/tamanho de um campo escalar não bater.
A validação é recursiva: campos de coleção (Coleção[...], como itens) têm
cada item validado contra o schema da coleção, em qualquer profundidade. Os erros
vêm com o caminho até o campo — a posição do item é base 1:
payload = {
natureza_operacao: "Venda",
itens: [
{ numero_item: 1, descricao: "Produto A" },
{ numero_item: 2 } # falta a descrição obrigatória
]
}
begin
client.nfe.emitir(ref: "pedido-42", dados: payload, validar: true)
rescue FocusNfe::Esquemas::ErroDeValidacao => e
e.erros # => ["itens[2].descricao: campo obrigatório ausente", ...]
end
Documentos sem schema próprio são emitidos sem validar (pulam silenciosamente).
Introspecção dos schemas
Os mesmos schemas empacotados ficam acessíveis como dado, para você (ou uma ferramenta automatizada) descobrir quais campos e tipos um documento aceita — sem token nem conexão:
FocusNfe::Esquemas.disponiveis
# => ["cte", "cte_os", "dce", "mdfe", "nfcom", "nfe", "nfe_item", "nfgas", ...]
FocusNfe::Esquemas.descrever("nfe")
# => [
# { nome: "natureza_operacao", descricao: "Descrição da natureza de operação.",
# tipo: :string, tipo_bruto: "String[1-60]", obrigatorio: true,
# tamanho_minimo: 1, tamanho_maximo: 60, enum: nil, tag: "natOp", colecao: nil },
# ...
# ]
# => nil para documento sem schema
Cada campo vira um Hash serializável. Campos de coleção (Coleção[...]) aninham
a descrição dos subcampos em :colecao, em qualquer profundidade; enums trazem os
valores aceitos em :enum. disponiveis também lista os sub-schemas auxiliares
(nfe_item, cte_transporte_aereo, …), que igualmente podem ser descritos.
Desenvolvimento
Após clonar o repositório, rode bin/setup: ele instala as dependências,
configura os git hooks (overcommit —
pre-commit/pre-push/commit-msg) e roda o rake default como verificação de
ambiente. bin/console abre um IRB com a gem carregada.
O projeto é desenvolvido test-first (TDD) com RSpec + WebMock — nenhuma
classe/método/branch nasce sem um spec falhando que o exija. O rake default
roda RSpec + RuboCop e precisa estar verde antes de cada commit:
bundle exec rake # RSpec + RuboCop
bin/rspec # apenas a suíte
bin/rubocop -a # estilo, com auto-correção
bundle exec rake pull_fields # regenera data/schemas/ a partir de campos.focusnfe.com.br
Os arquivos em data/schemas/ são gerados automaticamente por
rake pull_fields — não os edite à mão. Para atualizá-los, rode o script e
faça commit do resultado. O CI verifica em cada PR se os schemas estão em dia.
Para instalar a gem localmente, rode bundle exec rake install. Para publicar
uma nova versão, atualize o número em version.rb e rode bundle exec rake release, que cria a tag git, sobe os commits + tag e publica o .gem no
rubygems.org.
Contribuindo
Bug reports e pull requests são bem-vindos no GitHub em https://github.com/wilfison/focus_nfe. Espera-se que os participantes sigam o código de conduta.
Licença
Disponível como código aberto sob os termos da licença MIT.