FluvPay Ruby
SDK oficial da FluvPay para Ruby. Cobre cobranças PIX, saques, transferências internas e verificação de webhooks, com erros tipados e tratamento idiomático. A interface é estável e previsível, adequada tanto a integrações operadas por pessoas quanto a agentes que consomem a API de forma programática.
- Requer Ruby 3.0 ou superior.
- O cliente HTTP é construído sobre a biblioteca padrão (
net/http). Não há dependências de runtime. - Inclui retentativas automáticas em operações seguras, geração automática de
Idempotency-Keye erros tipados por classe.
Instalação
A publicação no RubyGems está pendente. Por enquanto, instale a partir do repositório, fixando a tag para builds reproduzíveis. No Gemfile:
gem "fluvpay", git: "https://github.com/fluvpay/fluvpay-ruby", tag: "v1.0.0"
E execute:
bundle install
Substituir tag: por branch: "main" acompanha o desenvolvimento em curso, com a ressalva de que a main pode mudar a qualquer momento.
RubyGems (em breve)
Quando a gem for publicada no RubyGems, a instalação passará a ser gem install fluvpay (ou gem "fluvpay" no Gemfile). Até lá, esses comandos não resolvem.
Para construir a gem a partir do código-fonte sem um Gemfile:
git clone --branch v1.0.0 https://github.com/fluvpay/fluvpay-ruby.git
cd fluvpay-ruby
gem build fluvpay.gemspec
gem install ./fluvpay-1.0.0.gem
Início rápido
require "fluvpay"
client = FluvPay::Client.new(api_key: "fluv_test_sua_chave_de_teste")
charge = client.charges.create(
amount_cents: 5000,
description: "Pedido 123",
customer: { name: "Maria", email: "maria@exemplo.com" },
metadata: { pedido_id: "123" }
)
puts charge["id"]
puts charge["pix_copy_paste"]
Autenticação
A autenticação usa a API Key informada no construtor do cliente. O ambiente é determinado pelo prefixo da chave: fluv_live_ seleciona produção e fluv_test_ seleciona o sandbox.
require "fluvpay"
client = FluvPay::Client.new(api_key: "fluv_live_sua_chave_aqui")
A base URL padrão é https://api.fluvpay.com/api/v1. Para sobrescrevê-la, informe base_url: no construtor.
Exemplo completo
O exemplo a seguir cria uma cobrança, recupera o registro, lista com paginação e verifica a assinatura de um webhook recebido.
require "fluvpay"
client = FluvPay::Client.new(api_key: "fluv_test_sua_chave_de_teste")
# Criar uma cobrança PIX. O valor é informado em centavos.
# A Idempotency-Key é gerada automaticamente quando não fornecida.
begin
charge = client.charges.create(
amount_cents: 5000,
description: "Pedido 123",
customer: { name: "Maria", email: "maria@exemplo.com" },
metadata: { pedido_id: "123" }
)
rescue FluvPay::ValidationError => err
puts "Dados inválidos: #{err.code} #{err.}"
err.details.each { |d| puts " - #{d['field']} #{d['message']}" }
raise
end
puts "Cobrança criada: #{charge['id']} #{charge['status']}"
puts "Copia e cola PIX: #{charge['pix_copy_paste']}"
# Recuperar pela ID.
mesma = client.charges.retrieve(charge["id"])
puts "Status atual: #{mesma['status']}"
# Listar cobranças com paginação page/per_page.
pagina = client.charges.list(page: 1, per_page: 20, status: "paid")
puts "Página #{pagina.page} de #{pagina.total} cobranças, há mais? #{pagina.has_next?}"
pagina.each { |item| puts " - #{item['id']} #{item['amount_cents']} #{item['status']}" }
# Verificar a assinatura de um webhook recebido.
# A verificação usa o corpo cru da requisição, nunca o JSON re-serializado.
def handle_webhook(raw_body, headers)
event = FluvPay::Webhooks.verify_signature(
raw_body,
headers["X-FluvPay-Signature"],
headers["X-FluvPay-Timestamp"],
"whsec_seu_segredo_do_webhook",
event_type: headers["X-FluvPay-Event"],
delivery_id: headers["X-FluvPay-Delivery-Id"],
tolerance_seconds: 300
)
puts "Cobrança paga: #{event.data['id']}" if event.type == "charge.paid"
end
Referência de recursos
Charges (cobranças PIX)
client.charges.create(amount_cents:, idempotency_key: nil, **campos) # POST /charges/
client.charges.retrieve(charge_id) # GET /charges/{id}
client.charges.list(page:, per_page:, sort:, status:) # GET /charges/
Transactions (extrato)
client.transactions.list(page:, per_page:, sort:) # GET /transactions/
client.transactions.retrieve(tx_id) # GET /transactions/{id}
Withdrawals (saques PIX, somente produção)
client.withdrawals.create(amount_cents:, pix_key:, pix_key_type:, idempotency_key: nil) # POST /withdrawals/
client.withdrawals.list(limit:, offset:, status:) # GET /withdrawals/
client.withdrawals.retrieve(withdrawal_id) # GET /withdrawals/{id}
Internal Transfers (transferências FluvPay para FluvPay, somente produção)
client.internal_transfers.create(amount_cents:, recipient_email:, idempotency_key: nil) # POST /internal-transfers/
client.internal_transfers.list(direction:, limit:, offset:) # GET /internal-transfers/
client.internal_transfers.retrieve(transfer_id) # GET /internal-transfers/{id}
Sandbox (somente com chave fluv_test_)
client.sandbox.reset # POST /test/reset
client.sandbox.scenarios # GET /test/scenarios
Campos de charges.create
O método aceita exatamente os campos do contrato. Os campos currency e method não são aceitos: a API responde 422 quando enviados.
| Campo | Tipo | Observação |
|---|---|---|
amount_cents |
Integer, obrigatório | 100 a 100000 (R$ 1,00 a R$ 1.000,00) |
description |
String | até 500 caracteres |
customer |
Hash | { name:, email:, document:, phone: } |
expires_in_seconds |
Integer | 60 a 604800 |
affiliate_code |
String | 4 a 24 caracteres |
split_rule_id |
String | 20 a 32 caracteres |
pass_fee_to_payer |
Boolean | padrão true |
metadata |
Hash | objeto livre |
Os status possíveis de uma cobrança são pending, paid, expired, cancelled e refunded.
Paginação
A API expõe dois formatos de envelope, ambos apresentados como objetos de página iteráveis:
charges.listetransactions.listexpõempage,per_page,total,has_next?ehas_prev?.withdrawals.listeinternal_transfers.listexpõemlimit,offsetetotal.
page = client.withdrawals.list(limit: 10, offset: 0)
puts [page.limit, page.offset, page.total].inspect
page.each { |w| puts "#{w['id']} #{w['status']} #{w['net_cents']}" }
Idempotência
Os POSTs de escrita (charges.create, withdrawals.create e internal_transfers.create) enviam o header Idempotency-Key. Quando a chave não é informada, o SDK gera um UUIDv4. Reenviar a mesma chave devolve a resposta original. Reutilizar a chave com um payload diferente resulta em FluvPay::ConflictError com código IDEMPOTENCY_CONFLICT.
chave = FluvPay::Client.new_idempotency_key
client.charges.create(amount_cents: 5000, idempotency_key: chave)
Webhooks
A FluvPay assina cada entrega. O header X-FluvPay-Signature contém v1=<hex>, calculado da seguinte forma:
hex = HMAC_SHA256(secret, "{timestamp}." + corpo_cru)
O secret é o valor whsec_... exibido na criação do webhook, o timestamp vem do header X-FluvPay-Timestamp e corpo_cru é o corpo da requisição exatamente como recebido. A verificação exige o corpo cru, nunca o JSON re-serializado.
begin
event = FluvPay::Webhooks.verify_signature(
raw_body,
request.headers["X-FluvPay-Signature"],
request.headers["X-FluvPay-Timestamp"],
"whsec_...",
tolerance_seconds: 300
)
rescue FluvPay::SignatureVerificationError
halt 400, "assinatura inválida"
end
Os eventos disponíveis são charge.created, charge.paid, charge.expired, charge.cancelled, charge.refunded, payout.created, payout.completed e payout.failed.
Erros
Todos os erros herdam de FluvPay::Error e carregam code, message, details, trace_id e status_code.
| Status | Exceção |
|---|---|
| 400 / 422 | FluvPay::ValidationError |
| 401 | FluvPay::AuthenticationError |
| 403 | FluvPay::PermissionError |
| 404 | FluvPay::NotFoundError |
| 409 | FluvPay::ConflictError |
| 429 | FluvPay::RateLimitError (campo retry_after) |
| 5xx | FluvPay::ServerError |
| rede / timeout | FluvPay::ConnectionError |
begin
client.charges.list
rescue FluvPay::RateLimitError => err
puts "Rate limit. Tente novamente em #{err.retry_after} segundos."
end
Retentativas
O SDK executa por padrão 2 retentativas com backoff exponencial e jitter. As retentativas ocorrem apenas em operações seguras: requisições GET e POSTs que carregam Idempotency-Key, e somente diante de respostas 429, 5xx ou falha de conexão. Em respostas 429, o header Retry-After é respeitado.
client = FluvPay::Client.new(api_key: "fluv_live_...", max_retries: 4) # aumentar
client = FluvPay::Client.new(api_key: "fluv_live_...", max_retries: 0) # desativar
Desenvolvimento
bundle install
rake test
Os testes unitários rodam sem acesso à rede, com net/http mockado via WebMock. O smoke test no sandbox roda somente quando a variável de ambiente FLUVPAY_TEST_KEY (prefixo fluv_test_) está presente; caso contrário, é pulado.
Licença
MIT.