Pular para o conteúdo
xFlow Payments

Blog

API Pix no Ruby on Rails

Para integrar a API Pix no Ruby on Rails, envie POST /api/v1/charges com Net::HTTP e basic_auth (pk_live_ como usuário, sk_live_ como senha), mostre o pix.qrCodeBase64 e o pix.copyPaste na view e receba o webhook transaction.paid num controller com skip_forgery_protection, validando o HMAC-SHA256 de request.raw_post com ActiveSupport::SecurityUtils.secure_compare. Teste antes com chaves pk_test_.

Atualizado em · Equipe xFlow Payments · 6 min de leitura

No Ruby on Rails, a API Pix se integra sem gem extra: o Net::HTTP da biblioteca padrão cria a cobrança, uma view ERB mostra o QR Code e o copia e cola, e um controller recebe o webhook que confirma o pagamento. O código segue a documentação da API, usa Rails 7 ou superior e a base https://app.xflowpayments.com/api/v1. O módulo do cliente não depende do Rails e roda num script Ruby comum. A visão geral está no guia da API Pix.

O que você precisa

  • Uma chave gerada no painel, em Desenvolvedores > Chaves de API. A chave pública (pk_live_...) fica sempre visível. O segredo (sk_live_...) aparece uma única vez.
  • Para testar, uma chave do ambiente Teste (pk_test_... e sk_test_...), que pode ser criada antes da aprovação do cadastro.
  • As variáveis de ambiente XFLOW_PUBLIC_KEY, XFLOW_SECRET_KEY e XFLOW_WEBHOOK_SECRET. Se preferir as credentials do Rails, troque ENV.fetch por Rails.application.credentials.dig(:xflow, ...).

O segredo não vai para o repositório nem para o JavaScript da página.

Um cliente com Net::HTTP

A autenticação é HTTP Basic: usuário é a chave pública, senha é o segredo. Valores vão em centavos, como inteiros.

ruby
# app/services/xflow.rb
require "net/http"
require "json"

module Xflow
  BASE = "https://app.xflowpayments.com/api/v1"
  METODOS = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.freeze

  class Erro < StandardError
    attr_reader :status, :codigo, :request_id, :retry_after

    def initialize(resposta)
      corpo = resposta.body.to_s.empty? ? {} : JSON.parse(resposta.body)
      @status = resposta.code.to_i
      @codigo = corpo["error"]
      @request_id = corpo["request_id"]
      @retry_after = resposta["retry-after"].to_i
      super("#{@status} #{@codigo} (#{@request_id})")
    end
  end

  def self.chamar(metodo, caminho, corpo: nil, idempotency_key: nil)
    uri = URI(BASE + caminho)
    req = METODOS.fetch(metodo).new(uri)
    req.basic_auth(ENV.fetch("XFLOW_PUBLIC_KEY"), ENV.fetch("XFLOW_SECRET_KEY"))
    req["Idempotency-Key"] = idempotency_key if idempotency_key
    if corpo
      req["Content-Type"] = "application/json"
      req.body = JSON.generate(corpo)
    end

    resposta = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30) do |http|
      http.request(req)
    end
    raise Erro, resposta unless resposta.is_a?(Net::HTTPSuccess)

    resposta.body.to_s.empty? ? {} : JSON.parse(resposta.body)
  end
end

Toda resposta traz um request_id (req_...), e a mensagem do Xflow::Erro já o inclui. Com ele você acha a chamada em Integrações > Logs da API, que guarda cada requisição autenticada por 30 dias.

Criar a cobrança

ruby
# config/routes.rb
post "pedidos/:pedido_id/pix", to: "pagamentos#create"
post "webhooks/xflow", to: "xflow_webhooks#create"
ruby
# app/controllers/pagamentos_controller.rb
class PagamentosController < ApplicationController
  def create
    pedido = current_user.pedidos.find(params[:pedido_id]) # só pedidos do usuário logado
    @cobranca = Xflow.chamar(
      "POST", "/charges",
      corpo: {
        amountCents: pedido.total_centavos, # inteiro: R$ 19,90 = 1990
        customer: { name: pedido.cliente_nome, email: pedido.cliente_email, document: pedido.cliente_documento },
        description: "Pedido ##{pedido.id}",
        external_reference: "pedido-#{pedido.id}"
      },
      idempotency_key: "pedido-#{pedido.id}"
    )
    pedido.update!(xflow_charge_id: @cobranca["id"])
    render :pix
  end
end

Ajuste current_user e os campos ao seu app. O valor vem do banco, nunca de params. A Idempotency-Key por pedido faz uma nova tentativa devolver a mesma cobrança em vez de gerar outro Pix. A mesma chave com outro corpo dá 409 idempotency_conflict. O porquê está em idempotência em API de pagamento. O external_reference (até 120 caracteres) volta em toda resposta e webhook.

Mostrar o QR Code na view

pix.qrCodeBase64 é um PNG em data-URL, pronto para o src. O escape do ERB não altera esse texto:

erb
<%# app/views/pagamentos/pix.html.erb %>
<img src="<%= @cobranca.dig("pix", "qrCodeBase64") %>" alt="QR Code Pix" width="240" height="240">
<textarea readonly><%= @cobranca.dig("pix", "copyPaste") %></textarea>

pix.copyPaste é o código BR Code que o pagador cola no app do banco: repasse sem alterar. Os dois campos vêm na criação e em GET /api/v1/charges/{id}, mas não na listagem.

Receber o webhook

Cadastre a URL em Integrações > Webhooks. Ela precisa ser https:// e pública. Cada entrega é um POST JSON com os headers x-webhook-event e x-webhook-signature: sha256=..., o HMAC-SHA256 do corpo cru com o segredo do endpoint.

ruby
# app/controllers/xflow_webhooks_controller.rb
class XflowWebhooksController < ActionController::Base
  skip_forgery_protection

  def create
    corpo = request.raw_post # bytes crus, antes de qualquer parse
    esperada = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("XFLOW_WEBHOOK_SECRET"), corpo)
    recebida = request.headers["X-Webhook-Signature"].to_s
    return head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(esperada, recebida)

    evento = JSON.parse(corpo)
    if evento["event"] == "transaction.paid" && evento.dig("data", "livemode") == true
      ProcessarPagamentoJob.perform_later(evento["data"])
    end
    head :ok
  end
end

Por que cada parte está assim:

  • Herda de ActionController::Base, e não do ApplicationController, para não carregar o before_action de login do app. O skip_forgery_protection desliga o CSRF só aqui.
  • request.raw_post, e não params. O Rails converte o JSON em params, mas o HMAC precisa dos bytes exatos.
  • secure_compare compara em tempo constante. Não use == para assinatura.
  • head :ok confirma a entrega. Sem 2xx, a xFlow reenvia até 3 vezes. livemode: false marca evento de teste.

ActiveJob opcional

O job deixa a resposta rápida:

ruby
# app/jobs/processar_pagamento_job.rb
class ProcessarPagamentoJob < ApplicationJob
  queue_as :default
  retry_on StandardError, wait: 30.seconds, attempts: 5

  def perform(data)
    # só muda pedido ainda não pago: o mesmo evento pode chegar duas vezes
    Pedido.where(referencia: data["external_reference"])
          .where.not(status: "pago")
          .update_all(status: "pago", end_to_end: data.dig("pix", "endToEnd"), updated_at: Time.current)
  end
end

Depois do head :ok, a xFlow considera o evento entregue. Se o job falhar, quem tenta de novo é a sua fila, com o retry_on. Apps novos do Rails 8 já vêm com o Solid Queue. Sem fila, chame o mesmo código direto no controller. Os outros eventos estão em webhook Pix.

Testar no sandbox

Com as chaves de teste nas variáveis, rode no rails console:

ruby
teste = Xflow.chamar("POST", "/charges", corpo: {
  amountCents: 1990,
  customer: { name: "Cliente", email: "c@exemplo.com", document: "00000000000" }
})
Xflow.chamar("POST", "/test/charges/#{teste["id"]}/pay")
Xflow.chamar("GET", "/charges/#{teste["id"]}").values_at("status", "livemode") # => ["paid", false]

A cobrança vira paid, a taxa sai das taxas reais da sua conta e o webhook chega com livemode: false. O pix.copyPaste de teste não pode ser pago, e a simulação com chave de produção responde 403 test_mode_only. Para receber o evento no ambiente de desenvolvimento por um túnel HTTPS, inclua o domínio do túnel em config.hosts, senão o Rails bloqueia a requisição. O passo a passo está em como testar webhook localmente e em sandbox da API Pix.

A rota do webhook se testa com um corpo assinado por você:

ruby
# test/integration/xflow_webhook_test.rb
require "test_helper"

class XflowWebhookTest < ActionDispatch::IntegrationTest
  test "aceita webhook assinado" do
    ENV["XFLOW_WEBHOOK_SECRET"] = "segredo-de-teste"
    corpo = { event: "transaction.pending", data: { livemode: false } }.to_json
    assinatura = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", "segredo-de-teste", corpo)

    post "/webhooks/xflow", params: corpo,
                            headers: { "CONTENT_TYPE" => "application/json", "X-Webhook-Signature" => assinatura }

    assert_response :ok
  end
end

Se a assinatura não bater em produção, compare os valores no validador de assinatura de webhook.

Erros e limite de requisições

Um rescue_from registra o request_id e devolve uma mensagem simples:

ruby
class ApplicationController < ActionController::Base
  rescue_from Xflow::Erro do |erro|
    Rails.logger.warn("xFlow: #{erro.message}") # status, código e request_id
    response.set_header("Retry-After", erro.retry_after.to_s) if erro.status == 429
    render plain: "Não foi possível gerar o Pix agora.", status: erro.status == 429 ? :service_unavailable : :bad_gateway
  end
end

Os códigos mais comuns:

  • 401 unauthorized: chave inválida ou revogada, ou IP fora da allowlist.
  • 400 validation: corpo inválido ou valor fora do mínimo ou do máximo da conta.
  • 403 account_inactive: conta em análise ou bloqueada. Siga com a chave de teste.
  • 429 rate_limited: o limite é por conta. Num job, use retry_job(wait: erro.retry_after.seconds) e repita com a mesma Idempotency-Key. Toda resposta autenticada traz x-ratelimit-remaining.
  • 502 charge_failed: falha ao gerar a cobrança no provedor. Repita com backoff.

A tabela completa está em erros da API Pix.

Antes de ir para produção

  1. Troque as chaves de teste por pk_live_/sk_live_ só nas variáveis (ou credentials) de produção.
  2. Cadastre a URL de produção do webhook e guarde o segredo do endpoint.
  3. Confirme que o worker do ActiveJob está rodando.
  4. Mantenha o filtro de livemode e o update_all condicional.
  5. Registre o request_id dos erros e, se quiser, ative a allowlist de IP em Chaves de API.

Prefira o webhook ao polling de GET /api/v1/charges/{id}. A página Desenvolvedores reúne chaves, logs e os guias de outras linguagens.

Perguntas frequentes

Por que o webhook Pix dá erro de CSRF no Rails?

Porque controllers que herdam de ActionController::Base exigem o token de autenticidade em todo POST, e a xFlow não envia esse token. Use skip_forgery_protection no controller do webhook. Em apps só de API, com ActionController::API, essa verificação não existe.

Preciso de gem para usar a API Pix no Rails?

Não. O Net::HTTP e o JSON da biblioteca padrão do Ruby fazem as chamadas, com basic_auth para a chave pública e o segredo. A API da xFlow é REST, com valores em centavos.

Por que a assinatura do webhook não confere no Rails?

Porque o HMAC foi calculado sobre params ou sobre um JSON gerado de novo, e não sobre os bytes recebidos. Use request.raw_post, calcule OpenSSL::HMAC.hexdigest("SHA256", segredo, corpo), prefixe com sha256= e compare com secure_compare.

Como testar a API Pix no Rails sem pagar?

Com chaves de teste (pk_test_/sk_test_), crie a cobrança no rails console e chame POST /api/v1/test/charges/{id}/pay. A cobrança vira paid e o webhook transaction.paid chega com livemode: false.

Continue lendo

SoluçãoAPI PixA API Pix da xFlow Payments é uma API REST para criar cobranças Pix com QR Code e copia e cola, estornar, sacar e receber webhooks assinados. Uma única integração conecta a sua operação a 11 adquirentes, com roteamento por regras e retentativa automática.BlogWebhook Pix: eventos, assinatura HMAC e boas práticasWebhook Pix é o aviso que a API envia para a sua URL quando algo acontece com uma cobrança ou um saque, como o pagamento confirmado. Na xFlow, cada aviso é um POST JSON assinado com HMAC-SHA256, reenviado até 3 vezes até você responder 2xx.BlogAPI Pix sandbox: como testar sem mover dinheiroO sandbox da API Pix da xFlow funciona com chaves pk_test_ e sk_test_: mesmos endpoints, mesmas respostas com livemode: false, nenhum dinheiro real e um endpoint para simular o pagamento que dispara o webhook transaction.paid.BlogComo testar webhook localmentePara testar webhook localmente, exponha a porta do seu servidor com um túnel HTTPS público, cadastre essa URL como endpoint usando a chave de teste e simule o pagamento com POST /api/v1/test/charges/{id}/pay. Para repetir um evento sem chamar a API, assine o corpo com o segredo e envie com curl.BlogIdempotência em API de pagamento: retry sem duplicarIdempotência é a garantia de que repetir a mesma requisição não repete o efeito. Na API da xFlow, você envia o header Idempotency-Key ao criar cobranças e saques: num retry com a mesma chave, a API devolve a mesma cobrança ou o mesmo saque, sem gerar outro Pix nem debitar o saldo de novo.