Production-ready Ruby and Rails code examples for BounceZero email verification - from a simple Net::HTTP request to a full Rails custom validator, Devise registration hook, Sidekiq background worker, and batch CSV enrichment pattern.
require "net/http"
require "uri"
require "json"
API_KEY = ENV["BOUNCEZERO_API_KEY"]
BASE_URL = "https://api.bouncezero.io/v1"
def verify_email(email)
uri = URI("#{BASE_URL}/verify")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{API_KEY}"
request["Content-Type"] = "application/json"
request.body = { email: email }.to_json
response = http.request(request)
JSON.parse(response.body, symbolize_names: true)
end
result = verify_email("[email protected]")
puts result
# => {
# email: "[email protected]",
# result: "valid", # "valid" | "invalid" | "unknown" | "catch-all"
# score: 0.97,
# is_disposable: false,
# is_role_address: false,
# mx_valid: true,
# provider: "Google Workspace"
# }
require "faraday"
require "json"
API_KEY = ENV["BOUNCEZERO_API_KEY"]
def bouncezero_client
Faraday.new("https://api.bouncezero.io") do |f|
f.request :json
f.response :json, symbolize_names: true
f.headers["Authorization"] = "Bearer #{API_KEY}"
end
end
def verify_email(email)
response = bouncezero_client.post("/v1/verify", { email: email })
response.body
end
result = verify_email("[email protected]")
puts result[:result] # => "valid"
puts result[:score] # => 0.96
Add real-time verification to any Rails model attribute at validation time.
require "net/http"
require "json"
class BounceZeroEmailValidator < ActiveModel::EachValidator
API_KEY = ENV["BOUNCEZERO_API_KEY"]
ENDPOINT = URI("https://api.bouncezero.io/v1/verify")
def validate_each(record, attribute, value)
return if value.blank?
result = call_api(value)
return unless result
if result[:result] == "invalid"
record.errors.add(attribute, :invalid, message: "doesn't appear to be a valid email address")
elsif result[:is_disposable]
record.errors.add(attribute, :disposable, message: "Please use a work or personal email address")
end
rescue StandardError
# Fail open on API errors - do not block signup
end
private
def call_api(email)
http = Net::HTTP.new(ENDPOINT.host, ENDPOINT.port)
http.use_ssl = true
req = Net::HTTP::Post.new(ENDPOINT)
req["Authorization"] = "Bearer #{API_KEY}"
req["Content-Type"] = "application/json"
req.body = { email: email }.to_json
res = http.request(req)
JSON.parse(res.body, symbolize_names: true)
end
end
# In your User model:
# validates :email, presence: true, bouncezero_email: true
Run verification asynchronously to avoid blocking the signup response. Flag the account for review if the email fails post-creation.
class EmailVerificationWorker
include Sidekiq::Worker
sidekiq_options retry: 2, queue: :verification
def perform(user_id)
user = User.find_by(id: user_id)
return unless user
result = BounceZeroService.verify(user.email)
return unless result
case result[:result]
when "invalid"
user.update!(email_verified_at: nil, email_status: "invalid")
# Optional: flag for manual review or send re-registration prompt
when "valid", "catch-all"
user.update!(email_verified_at: Time.current, email_status: result[:result])
end
user.update!(
email_disposable: result[:is_disposable],
email_score: result[:score]
)
end
end
# Trigger from UsersController#create or Devise registrations_controller:
# EmailVerificationWorker.perform_async(@user.id)
| Field | Type | Values | Use for |
|---|---|---|---|
| result | String | valid / invalid / unknown / catch-all | Primary routing decision |
| score | Float | 0.0 - 1.0 | Threshold-based filtering for catch-all decisions |
| is_disposable | Boolean | true / false | Block throwaway addresses at signup |
| is_role_address | Boolean | true / false | Flag info@ / admin@ / support@ |
| mx_valid | Boolean | true / false | Domain has active MX - basic health check |
| provider | String | “Gmail” / “Outlook” / etc. | Route to provider-specific logic or personalise |
Use Net::HTTP (built-in) or Faraday to POST to https://api.bouncezero.io/v1/verify with your API key in the Authorization header and {email: address} as a JSON body. Parse the response and check the result field: ‘valid’, ‘invalid’, ‘unknown’, or ‘catch-all’.
Create a custom ActiveModel::EachValidator that calls BounceZero during validation. Add validates :email, bouncezero_email: true to your User model. The validator adds errors to the email attribute for invalid addresses and disposable domains. Fail open on API errors to avoid blocking legitimate signups.
Run verification post-signup in a Sidekiq worker triggered from a Devise after_sign_up hook. The worker calls BounceZero, updates the user’s email_status, and flags invalid accounts for review. Async avoids blocking the signup UX on an external API response.
100 free credits on signup. No credit card. Instant access to the verification API. $3/1K thereafter - credits never expire.
Deep-dive guides on how email verification and inbox placement work
272,446-domain census: DMARC gap, provider divide, catch-all rates
10.2M verifications: 12.3% of addresses are dead, and where they hide
826K re-verifications: only 19% of valid addresses survive 90 days
True catch-all is 1.4% - most of what looks catch-all is unprobeable providers
info@ bounces 4.5x more than personal addresses - measured, not guessed
The 3x invalid-rate gap that vanishes when you control for domain size
Explore other topics