Pular para o conteúdo

Exemplos em Ruby

Setup

Requer Ruby 3+. Sem dependências externas (stdlib).

Terminal window
export MYSE_API_BASE="https://api.mysebr.com.br/nfemyse-v3/rest"
export MYSE_API_TOKEN="seu_token_aqui"

Cliente HTTP centralizado

myse_api.rb
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
end
end

Cená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}"
end

Retry 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
end
end
# Uso
nfce = 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'
end
end

Emitir 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-xml
end

Cancelar 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'))
end

Lanç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'))
end

Cancelar 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'))
end

Emitir 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']
end

Cancelar NFS-e

def cancelar_nfse(client, chave, motivo)
validar_motivo!(motivo)
client.call('POST', '/nfse/cancelamento', { chave: chave, motivo: motivo })
end

Reconhecer 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 realizada
CODIGOS_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'))
end

Listar 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
todos
end

Tratamento 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)
raise
end

Health 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/CNPJ
cliente = client.call('GET', '/clientes/12345678000199')
# Buscar por nome
encontrados = client.call('GET', '/clientes/busca/exemplo')
top = client.call('GET', '/clientes/busca/top_clientes')
# Atualizar
client.call('PUT', '/clientes/12345678000199', {
tipo: 'J',
'razao-social' => 'Empresa Exemplo (Novo Nome) Ltda',
})
# Excluir
client.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
# Consultar
produto = 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')
# Atualizar
client.call('PUT', '/produtos/SKU-001', {
referencia: 'SKU-001',
nome: 'Camiseta Polo M (Novo)',
valor: '99.90',
})
# Excluir
client.call('DELETE', '/produtos/SKU-001')