Exemplos em Ruby
Setup
Requer Ruby 3+. Sem dependências externas (stdlib).
export MYSE_API_BASE="https://api.mysebr.com.br/nfemyse-v3/rest"export MYSE_API_TOKEN="seu_token_aqui"Cliente HTTP centralizado
require 'net/http'require 'json'require 'uri'
module Myse class ApiError < StandardError attr_reader :http_status, :payload, :request_id
def initialize(http_status, message, payload = nil, request_id = nil) @http_status = http_status @payload = payload @request_id = request_id super(message) end end
class Client BASE = ENV.fetch('MYSE_API_BASE') TOKEN = ENV.fetch('MYSE_API_TOKEN')
def call(method, path, body = nil, timeout: 30) uri = URI.parse("#{BASE}#{path}") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = (uri.scheme == 'https') http.read_timeout = timeout
req_class = { 'GET' => Net::HTTP::Get, 'POST' => Net::HTTP::Post, 'PUT' => Net::HTTP::Put, 'DELETE' => Net::HTTP::Delete, }.fetch(method.upcase)
req = req_class.new(uri.request_uri) req['Authorization'] = "Token #{TOKEN}" req['Content-Type'] = 'application/json' req.body = body.to_json if body
res = http.request(req) request_id = res['x-request-id'] json = JSON.parse(res.body) rescue {}
if res.code.to_i >= 400 msg = json.dig('error', 'message') || "HTTP #{res.code}" raise ApiError.new(res.code.to_i, msg, json, request_id) end
if json['status'] == '999' msg = json.dig('data', 'erro') || json['descricao'] || 'erro' rid = json.dig('meta', 'request_id') || request_id raise ApiError.new(200, msg, json, rid) end
json end endendCenário: emitir NFCe com polling
require_relative 'myse_api'
TERMINAIS = %w[004 010 900 999].freeze
def emitir_nfce(client, pedido) body = { serie: '65', cfop: '5102', 'numero-origem' => "PED-#{pedido[:id]}", tp_pagamento: pedido[:forma_pagamento], cliente: pedido[:cpf_cnpj_cliente], itens: pedido[:itens].map do |i| { produto: i[:sku], quantidade: i[:qtd].to_s, 'valor-unitario' => i[:preco].to_s, } end, }
emissao = client.call('POST', '/nfce/emissao', body)
unless %w[001 050].include?(emissao['status']) raise "Emissão falhou: #{emissao['descricao']}" end
recibo = emissao.dig('data', 'recibo') puts "[emissao] recibo=#{recibo} request_id=#{emissao.dig('meta', 'request_id')}"
wait = 5.0 inicio = Time.now
while Time.now - inicio < 5 * 60 sleep(wait) consulta = client.call('GET', "/nfce/cupom/#{recibo}") puts "[polling] status=#{consulta['status']}"
if TERMINAIS.include?(consulta['status']) return consulta['data'] if consulta['status'] == '004' raise "Status terminal não-sucesso: #{consulta['status']}" end
wait = [wait * 1.5, 30].min end
raise "Timeout aguardando recibo #{recibo}"endRetry com backoff
def com_retry(max_tentativas: 3) (1..max_tentativas).each do |i| begin return yield rescue Myse::ApiError => e raise if e.payload&.dig('status') == '999' raise if i == max_tentativas sleep(2 ** (i - 1)) # 1s, 2s, 4s end endend
# Usonfce = com_retry { emitir_nfce(client, pedido) }Helper de polling reutilizável
TERMINAIS = %w[004 010 900 999].freeze
def aguardar_recibo(client, path_base, recibo, timeout_sec: 300) wait = 5.0 inicio = Time.now
while Time.now - inicio < timeout_sec sleep(wait) resp = client.call('GET', "#{path_base}/#{recibo}") return resp if TERMINAIS.include?(resp['status']) wait = [wait * 1.5, 30].min end raise "Timeout aguardando recibo #{recibo}"end
def validar_motivo!(motivo) unless (15..255).include?(motivo.length) raise ArgumentError, 'motivo deve ter entre 15 e 255 caracteres' endendEmitir NF-e (com transporte completo)
def emitir_nfe(client, pedido) body = { 'serie' => '1', 'cfop' => '5102', 'numero-origem' => "PED-#{pedido[:id]}", 'tipo' => 'S', 'finalidade' => 'N', 'valor-despesas' => '0.00', 'cliente' => pedido[:cpf_cnpj_cliente], 'transporte' => { 'modalidade' => '0', 'transportadora' => pedido[:transportadora], 'placa-veiculo' => pedido[:placa], 'valor-frete' => pedido[:frete] || '0.00', 'valor-seguro' => '0.00', 'peso-bruto' => pedido[:peso_bruto], 'peso-liquido' => pedido[:peso_liquido], 'quantidade-volume' => '1', 'especie-volume' => 'Caixa', 'marca-volume' => 'Embalagem', }, 'itens' => pedido[:itens].map do |i| { 'produto' => i[:sku], 'quantidade' => i[:qtd].to_s, 'valor-unitario' => i[:preco].to_s, } end, 'faturas' => (pedido[:faturas] || []).map do |f| { 'valor' => f[:valor].to_s, 'data' => f[:data] } end, }
emissao = client.call('POST', '/nfe/emissao', body) unless %w[001 050].include?(emissao['status']) raise "Emissão falhou: #{emissao['descricao']}" end
resultado = aguardar_recibo(client, '/nfe/nota', emissao.dig('data', 'recibo')) unless resultado['status'] == '004' raise "Status terminal não-sucesso: #{resultado['status']}" end resultado['data'] # inclui sefaz.chave, url-danfe, url-xmlendCancelar NF-e
def cancelar_nfe(client, chave, motivo) validar_motivo!(motivo) cancel = client.call('POST', '/nfe/cancelamento', { chave: chave, motivo: motivo }) raise cancel['descricao'] unless cancel['status'] == '001' aguardar_recibo(client, '/nfe/cancelamento', cancel.dig('data', 'recibo'))endLançar carta de correção (CC-e)
# O campo 'motivo' na CC-e é o TEXTO da correção (xCorrecao).def carta_correcao_nfe(client, chave, texto_correcao) validar_motivo!(texto_correcao) cce = client.call('POST', '/nfe/correcao', { chave: chave, motivo: texto_correcao }) raise cce['descricao'] unless cce['status'] == '001' aguardar_recibo(client, '/nfe/correcao', cce.dig('data', 'recibo'))endCancelar NFC-e
def cancelar_nfce(client, chave, motivo) validar_motivo!(motivo) cancel = client.call('POST', '/nfce/cancelamento', { chave: chave, motivo: motivo }) raise cancel['descricao'] unless cancel['status'] == '001' aguardar_recibo(client, '/nfce/cancelamento', cancel.dig('data', 'recibo'))endEmitir NFS-e (Nota Fiscal de Serviço)
def emitir_nfse(client, servico) body = { 'numero-origem' => "OS-#{servico[:id]}", 'observacao' => servico[:observacao], 'nome-contato' => servico[:nome_contato], 'telefone-contato' => servico[:telefone_contato], 'data-fim' => servico[:data_fim], 'cliente' => servico[:cpf_cnpj_cliente], 'itens' => servico[:itens].map do |i| { 'produto' => i[:codigo_servico], 'quantidade' => i[:qtd].to_s, 'valor-unitario' => i[:preco].to_s, } end, }
emissao = client.call('POST', '/nfse/emissao', body) unless %w[001 050].include?(emissao['status']) raise "Emissão NFSe falhou: #{emissao['descricao']}" end
resultado = aguardar_recibo(client, '/nfse/nota', emissao.dig('data', 'recibo')) unless resultado['status'] == '004' raise "Status terminal não-sucesso: #{resultado['status']}" end resultado['data']endCancelar NFS-e
def cancelar_nfse(client, chave, motivo) validar_motivo!(motivo) client.call('POST', '/nfse/cancelamento', { chave: chave, motivo: motivo })endReconhecer nota de fornecedor (MD-e)
# Códigos de reconhecimento:# '210200' — Confirmação da operação (terminal, irreversível)# '210210' — Ciência da operação (provisório, 15 dias)# '210220' — Desconhecimento da operação# '210240' — Operação não realizadaCODIGOS_RECONHECIMENTO = %w[210200 210210 210220 210240].freeze
def reconhecer_nota_fornecedor(client, chave, codigo) unless CODIGOS_RECONHECIMENTO.include?(codigo) raise ArgumentError, "Código inválido: #{codigo}" end
resp = client.call('POST', '/nffornecedor/reconhecimento', { chave: chave, 'codigo-reconhecimento' => codigo, }) raise resp['descricao'] unless resp['status'] == '001' aguardar_recibo(client, '/nffornecedor', resp.dig('data', 'recibo'))endListar com paginação
def listar_todos_clientes(client) todos = [] page = 1 page_size = 100
loop do resp = client.call('GET', "/clientes?page=#{page}&page_size=#{page_size}") break unless resp['status'] == '005' && resp['data'].is_a?(Array) todos.concat(resp['data']) break if resp['data'].size < page_size page += 1 sleep(0.2) end
todosendTratamento de erros
require 'logger'logger = Logger.new($stdout)client = Myse::Client.new
begin nfce = emitir_nfce(client, pedido) logger.info("NFCe emitida: #{nfce.dig('sefaz', 'chave')}")rescue Myse::ApiError => e logger.error({ event: 'api_error', http_status: e.http_status, request_id: e.request_id, message: e.message, }.to_json) raiseendHealth check (sem autenticação)
uri = URI.parse("#{Myse::Client::BASE}/health")res = Net::HTTP.get_response(uri)health = JSON.parse(res.body)# {"status" => "ok", "database" => "ok", "timestamp" => "..."}Alterar nota antes da emissão (PUT)
Disponível em NF-e (/nfe/{id}), NFC-e (/nfce/{id}) e NFS-e (/nfse/{id}).
Só funciona enquanto a nota ainda não foi submetida para emissão.
def alterar_nota(client, tipo, id, dados) # tipo: 'nfe' | 'nfce' | 'nfse' client.call('PUT', "/#{tipo}/#{id}", dados)end
# Uso:alterar_nota(client, 'nfe', '12345', { serie: '1', cfop: '5102', cliente: '12345678000199', itens: [{ produto: 'SKU-001', quantidade: '3', 'valor-unitario' => '89.90' }],})Consulta detalhada por recibo
nfe_completa = client.call('GET', "/nfe/nota/#{recibo}/completa")nfce_completa = client.call('GET', "/nfce/cupom/#{recibo}/completa")Listagens
nfes = client.call('GET', '/nfe?page=1&page_size=20')nfces = client.call('GET', '/nfce?page=1&page_size=50')nfses = client.call('GET', '/nfse?dataInicial=2026-05-01&page=1&page_size=20')fornecedor = client.call('GET', '/nffornecedor?page=1&page_size=20')
# Forma legada (path-based)nfes_legado = client.call('GET', '/nfe/paginacao/1/20')Clientes (CRUD completo)
def cadastrar_pf(client, dados) client.call('POST', '/clientes', { tipo: 'F', nome: dados[:nome], cpf: dados[:cpf], rg: dados[:rg], 'consumidor-final' => '1', contato: { email: dados[:email], telefone: dados[:telefone] }, endereco: dados[:endereco], })end
def cadastrar_pj(client, dados) client.call('POST', '/clientes', { tipo: 'J', 'razao-social' => dados[:razao_social], 'nome-fantasia' => dados[:nome_fantasia], cnpj: dados[:cnpj], 'inscricao-municipal' => dados[:inscricao_municipal], 'tipo-inscricao-estadual' => dados[:contribuinte] || '9', 'inscricao-estadual' => dados[:inscricao_estadual] || 'ISENTO', 'consumidor-final' => '0', contato: { email: dados[:email], telefone: dados[:telefone] }, endereco: dados[:endereco], })end
# Consultar por CPF/CNPJcliente = client.call('GET', '/clientes/12345678000199')
# Buscar por nomeencontrados = client.call('GET', '/clientes/busca/exemplo')top = client.call('GET', '/clientes/busca/top_clientes')
# Atualizarclient.call('PUT', '/clientes/12345678000199', { tipo: 'J', 'razao-social' => 'Empresa Exemplo (Novo Nome) Ltda',})
# Excluirclient.call('DELETE', '/clientes/12345678000199')Produtos (CRUD completo)
def cadastrar_produto(client, produto) client.call('POST', '/produtos', { referencia: produto[:sku], nome: produto[:nome], valor: produto[:valor].to_s, medida: produto[:medida] || 'UN', categoria: produto[:categoria], ncm: produto[:ncm], origem: produto[:origem] || '0', 'cfop-preferencial' => produto[:cfop] || '5102', 'tipo-produto' => 'P', 'configuracoes-fiscais' => produto[:config_fiscal] || [], })end
def cadastrar_servico(client, servico) client.call('POST', '/produtos', { referencia: servico[:sku], nome: servico[:nome], valor: servico[:valor].to_s, medida: 'H', 'tipo-produto' => 'S', 'codigo-servico' => servico[:codigo_servico], })end
# Consultarproduto = client.call('GET', '/produtos/SKU-001')por_nome = client.call('GET', '/produtos/nome/Camiseta')
# Listar paginado com busca (use _todos para não filtrar)todos = client.call('GET', '/produtos/paginacao/1/20/_todos')filtrados = client.call('GET', '/produtos/paginacao/1/20/Camiseta')
# Atualizarclient.call('PUT', '/produtos/SKU-001', { referencia: 'SKU-001', nome: 'Camiseta Polo M (Novo)', valor: '99.90',})
# Excluirclient.call('DELETE', '/produtos/SKU-001')