Block disposable email signups in Rails
Add an ActiveRecord validation on User. The validation runs before save, so disposable emails never become rows in your `users` table. Works with Devise or vanilla Rails auth.
The code
# config/initializers/disposable_email.rb
require "net/http"
require "json"
module DisposableEmail
URL = "https://api.checkdisposable.email/v1/check"
KEY = ENV.fetch("CDE_KEY")
def self.disposable?(email)
uri = URI("#{URL}?email=#{URI.encode_www_form_component(email)}")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{KEY}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true,
open_timeout: 2, read_timeout: 3) do |http|
http.request(req)
end
return false unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body).fetch("is_disposable", false) == true
rescue StandardError
false # fail open
end
end
# app/models/user.rb
class User < ApplicationRecord
validate :no_disposable_email
private
def no_disposable_email
return unless email_changed? && email.present?
if DisposableEmail.disposable?(email)
errors.add(:email, "is not allowed — please use a real address.")
end
end
endNotes
- Why on save, not the controller
- Putting the check in the model means it runs from any code path that creates a User — controller actions, console, rake tasks, OAuth callbacks. One check, full coverage.
- Devise compatibility
- Devise uses the same User model, so the validation runs during registration without any further wiring.
- Background jobs
- For high-volume signup flows, move the check into an ActiveJob and use a soft "pending verification" state. For most apps the inline check is fine — 50ms is well below user-perceived latency.
Get a free API key
500 checks/month, no credit card. No credit card. 30 seconds.
Sign up free →