Para consumir a API de CPF da CPFHub em aplicações Rails com Hotwire, crie um controller que faz uma requisição GET para https://api.cpfhub.io/cpf/{CPF} com o header x-api-key e responda com um template Turbo Frame ou Turbo Stream. Turbo Frames atualizam um fragmento isolado da página, enquanto Turbo Streams permitem atualizar múltiplas regiões simultaneamente — ambos sem recarregar a página inteira. A latência média da API é de aproximadamente 900ms, o que torna o uso de um indicador de carregamento importante para uma boa experiência do usuário. Consulte a documentação oficial do Turbo para entender as diferenças entre frames e streams antes de escolher a abordagem.
Turbo Frames vs Turbo Streams
Turbo Frames
Turbo Frames encapsulam uma seção da página que pode ser atualizada independentemente. Quando um formulário dentro de um frame é submetido, apenas o conteúdo do frame é substituído pela resposta do servidor.
Turbo Streams
Turbo Streams permitem atualizações granulares do DOM usando ações como append, replace, update e remove. Podem ser enviados via resposta HTTP ou via WebSocket com Action Cable.
Qual usar para validação de CPF
Para a maioria dos casos, Turbo Frames são suficientes. Use Turbo Streams quando precisar atualizar múltiplas regiões da página simultaneamente -- por exemplo, preencher nome, data de nascimento e status de validação em áreas diferentes do formulário.
Configurando o controller Rails
Primeiro, crie o controller que receberá as requisições de validação e consumirá a API da CPFHub:
# app/controllers/cpf_lookups_controller.rb
class CpfLookupsController < ApplicationController
require "net/http"
require "json"
def create
cpf = params[:cpf].to_s.gsub(/\D/, "")
if cpf.length != 11
@error = "CPF deve conter 11 dígitos."
respond_to do |format|
format.turbo_stream
format.html { render :new }
end
return
end
@result = fetch_cpf_data(cpf)
respond_to do |format|
format.turbo_stream
format.html { render :new }
end
end
private
def fetch_cpf_data(cpf)
uri = URI("https://api.cpfhub.io/cpf/#{cpf}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 10
http.read_timeout = 10
request = Net::HTTP::Get.new(uri)
request["x-api-key"] = Rails.application.credentials.cpfhub_api_key
request["Accept"] = "application/json"
response = http.request(request)
JSON.parse(response.body, symbolize_names: true)
rescue Net::OpenTimeout, Net::ReadTimeout
{ success: false, error: "Tempo de resposta excedido" }
rescue StandardError => e
{ success: false, error: "Erro na consulta: #{e.message}" }
end
end
Implementando com Turbo Frames
A view do formulário
Crie o formulário envolvido por um Turbo Frame:
<%# app/views/cpf_lookups/new.html.erb %>
<div class="cpf-lookup-container">
<h2>Consulta de CPF</h2>
<%= form_with url: cpf_lookups_path, method: :post, data: { turbo_frame: "cpf_result" } do |f| %>
<div class="form-group">
<%= f.label :cpf, "Digite o CPF" %>
<%= f.text_field :cpf,
placeholder: "000.000.000-00",
inputmode: "numeric",
autofocus: true %>
</div>
<%= f.submit "Consultar", class: "btn btn-primary" %>
<% end %>
<%= turbo_frame_tag "cpf_result" do %>
<div class="cpf-result-placeholder">
<p>Os dados do CPF aparecerão aqui após a consulta.</p>
</div>
<% end %>
</div>
A resposta em Turbo Frame
<%# app/views/cpf_lookups/create.html.erb %>
<%= turbo_frame_tag "cpf_result" do %>
<% if @error %>
<div class="alert alert-danger">
<p><%= @error %></p>
</div>
<% elsif @result && @result[:success] %>
<div class="cpf-result cpf-result--success">
<h3>Dados encontrados</h3>
<dl>
<dt>CPF</dt>
<dd><%= @result[:data][:cpf] %></dd>
<dt>Nome</dt>
<dd><%= @result[:data][:name] %></dd>
<dt>Data de Nascimento</dt>
<dd><%= @result[:data][:birthDate] %></dd>
<dt>Gênero</dt>
<dd><%= @result[:data][:gender] %></dd>
</dl>
</div>
<% else %>
<div class="alert alert-warning">
<p>CPF não encontrado ou erro na consulta.</p>
</div>
<% end %>
<% end %>
Implementando com Turbo Streams
Para cenários onde você precisa atualizar múltiplas partes da página, use Turbo Streams:
A resposta em Turbo Stream
<%# app/views/cpf_lookups/create.turbo_stream.erb %>
<% if @error %>
<%= turbo_stream.update "cpf_feedback" do %>
<div class="alert alert-danger"><%= @error %></div>
<% end %>
<%= turbo_stream.update "cpf_name_field" do %>
<%= text_field_tag :name, "", readonly: true, class: "form-control" %>
<% end %>
<% elsif @result && @result[:success] %>
<%= turbo_stream.update "cpf_feedback" do %>
<div class="alert alert-success">CPF válido!</div>
<% end %>
<%= turbo_stream.update "cpf_name_field" do %>
<%= text_field_tag :name, @result[:data][:name],
readonly: true, class: "form-control" %>
<% end %>
<%= turbo_stream.update "cpf_birthdate_field" do %>
<%= text_field_tag :birth_date, @result[:data][:birthDate],
readonly: true, class: "form-control" %>
<% end %>
<%= turbo_stream.update "cpf_gender_field" do %>
<%= text_field_tag :gender, @result[:data][:gender],
readonly: true, class: "form-control" %>
<% end %>
<% else %>
<%= turbo_stream.update "cpf_feedback" do %>
<div class="alert alert-warning">CPF não encontrado.</div>
<% end %>
<% end %>
O formulário com targets para Turbo Streams
<%# app/views/registrations/new.html.erb %>
<%= form_with url: registrations_path, method: :post do |f| %>
<div class="form-group">
<%= f.label :cpf %>
<div class="input-group">
<%= f.text_field :cpf, id: "cpf_input", inputmode: "numeric" %>
<%= button_tag "Validar",
type: "button",
data: { action: "click->cpf-turbo#submit" },
class: "btn btn-secondary" %>
</div>
<div id="cpf_feedback"></div>
</div>
<div class="form-group">
<%= f.label :name, "Nome" %>
<div id="cpf_name_field">
<%= f.text_field :name, readonly: true %>
</div>
</div>
<div class="form-group">
<%= f.label :birth_date, "Data de Nascimento" %>
<div id="cpf_birthdate_field">
<%= f.text_field :birth_date, readonly: true %>
</div>
</div>
<div class="form-group">
<%= f.label :gender, "Gênero" %>
<div id="cpf_gender_field">
<%= f.text_field :gender, readonly: true %>
</div>
</div>
<%= f.submit "Cadastrar" %>
<% end %>
Turbo Streams via Action Cable em tempo real
Para cenários avançados -- como validações em lote ou dashboards --, use Turbo Streams com WebSocket:
# app/models/cpf_lookup.rb
class CpfLookup < ApplicationRecord
after_create_commit -> {
broadcast_append_to "cpf_lookups",
partial: "cpf_lookups/lookup",
locals: { lookup: self },
target: "cpf_lookups_list"
}
end
<%# app/views/cpf_lookups/index.html.erb %>
<%= turbo_stream_from "cpf_lookups" %>
<div id="cpf_lookups_list">
<%= render @cpf_lookups %>
</div>
<%# app/views/cpf_lookups/_lookup.html.erb %>
<div class="lookup-item" id="<%= dom_id(lookup) %>">
<span class="lookup-cpf"><%= lookup.cpf %></span>
<span class="lookup-name"><%= lookup.name %></span>
<span class="lookup-status <%= lookup.valid? ? 'success' : 'error' %>">
<%= lookup.valid? ? "Válido" : "Inválido" %>
</span>
</div>
Adicionando loading state com Stimulus auxiliar
Para mostrar um indicador de carregamento durante a requisição Turbo, crie um controller Stimulus complementar:
// app/javascript/controllers/cpf_turbo_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form", "spinner", "button"]
submit() {
const cpfInput = document.getElementById("cpf_input")
const cpf = cpfInput.value.replace(/\D/g, "")
if (cpf.length !== 11) {
document.getElementById("cpf_feedback").innerHTML =
'<div class="alert alert-warning">CPF deve ter 11 dígitos.</div>'
return
}
this.showLoading()
const form = document.createElement("form")
form.method = "POST"
form.action = "/cpf_lookups"
form.dataset.turbo = "true"
const csrfToken = document.querySelector("meta[name='csrf-token']").content
const csrfInput = document.createElement("input")
csrfInput.type = "hidden"
csrfInput.name = "authenticity_token"
csrfInput.value = csrfToken
const cpfField = document.createElement("input")
cpfField.type = "hidden"
cpfField.name = "cpf"
cpfField.value = cpf
form.appendChild(csrfInput)
form.appendChild(cpfField)
document.body.appendChild(form)
form.requestSubmit()
setTimeout(() => {
document.body.removeChild(form)
this.hideLoading()
}, 5000)
}
showLoading() {
if (this.hasSpinnerTarget) {
this.spinnerTarget.classList.remove("hidden")
}
if (this.hasButtonTarget) {
this.buttonTarget.disabled = true
}
}
hideLoading() {
if (this.hasSpinnerTarget) {
this.spinnerTarget.classList.add("hidden")
}
if (this.hasButtonTarget) {
this.buttonTarget.disabled = false
}
}
}
Configurando as rotas
Não esqueça de configurar as rotas:
# config/routes.rb
Rails.application.routes.draw do
resources :cpf_lookups, only: [:new, :create, :index]
resources :registrations, only: [:new, :create]
end
Testando a integração
Utilize testes de sistema para verificar o fluxo completo:
# test/system/cpf_lookups_test.rb
require "application_system_test_case"
class CpfLookupsTest < ApplicationSystemTestCase
test "validating a CPF via Turbo Frame" do
visit new_cpf_lookup_path
fill_in "CPF", with: "123.456.789-09"
click_button "Consultar"
assert_selector "#cpf_result .cpf-result--success", wait: 15
assert_text "Dados encontrados"
end
end
Perguntas frequentes
Como funciona a autenticação na API CPFHub.io dentro de uma aplicação Rails?
A autenticação é feita pelo header x-api-key em cada requisição GET para https://api.cpfhub.io/cpf/{CPF}. Em Rails, o valor da chave deve ser armazenado em credentials.yml.enc e acessado via Rails.application.credentials.cpfhub_api_key, nunca em texto puro no código-fonte. Isso impede que a chave seja exposta em repositórios Git.
Turbo Frames ou Turbo Streams: qual escolher para validação de CPF?
Use Turbo Frames quando precisar atualizar apenas uma região isolada da página — como um bloco de resultado abaixo do campo de CPF. Use Turbo Streams quando a resposta da API precisar preencher múltiplos campos simultâneos, como nome, data de nascimento e status em partes diferentes do formulário. Para a maioria dos cadastros simples, Turbo Frames são suficientes e mais simples de implementar.
A API CPFHub.io bloqueia requisições quando o limite do plano é atingido?
Não. Quando o limite mensal é ultrapassado, a API continua respondendo normalmente e cobra R$0,15 por consulta adicional — sem retornar erro 429 nem bloquear o serviço. O plano gratuito inclui 50 consultas por mês; o plano Pro oferece 1.000 consultas mensais por R$149.
Qual o tempo de resposta esperado da API e como isso afeta a experiência no Turbo?
A latência média da API CPFHub.io é de aproximadamente 900ms. Por isso, exibir um indicador de carregamento durante a requisição Turbo é importante para evitar que o usuário pense que a página travou. O controller Stimulus auxiliar descrito neste artigo implementa exatamente esse comportamento com showLoading() e hideLoading().
Conclusão
Turbo Frames e Turbo Streams oferecem uma maneira poderosa e elegante de integrar validações de CPF em aplicações Rails sem escrever JavaScript complexo. Com Turbo Frames, atualize seções isoladas do formulário após a consulta. Com Turbo Streams, atualize múltiplas áreas da página simultaneamente. E com Action Cable, receba atualizações em tempo real para dashboards e validações em lote.
A API do CPFHub.io
Cadastre-se em cpfhub.io
CPFHub.io
Pronto para integrar a API?
50 consultas gratuitas para testar agora. Sem cartão de crédito. Acesso imediato à documentação.
Sobre a redação
Redação CPFHub.io
Time editorial especializado em APIs de CPF, identidade digital e compliance no mercado brasileiro. Produzimos guias técnicos, análises regulatórias e tutoriais sobre LGPD e KYC para desenvolvedores e líderes de produto.



