Rails makes custom validation easy with Active Model validators. This guide shows how to integrate Mailchk email validation using a service object, a custom validator, and controller-level checks.
Step 1: Service Object
# app/services/email_validator.rb
class EmailValidator
API_URL = "https://api.mailchk.io/v1/check"
def self.validate(email)
new(email).validate
end
def initialize(email)
@email = email
end
def validate
cached = Rails.cache.read(cache_key)
return cached if cached
result = call_api
Rails.cache.write(cache_key, result, expires_in: 24.hours)
result
end
private
def call_api
uri = URI(API_URL)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request["X-API-Key"] = Rails.application.credentials.mailchk_api_key
request.body = { email: @email }.to_json
response = http.request(request)
JSON.parse(response.body)
rescue StandardError
{ "valid" => true, "disposable" => false }
end
def cache_key
"email_validation:#{Digest::MD5.hexdigest(@email)}"
end
end
Step 2: Custom Active Model Validator
# app/validators/real_email_validator.rb
class RealEmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
result = EmailValidator.validate(value)
unless result["valid"]
record.errors.add(attribute, "does not appear to be valid")
return
end
if result["disposable"]
record.errors.add(attribute, "cannot be a disposable address")
end
if options[:block_free] && result["free_email"]
record.errors.add(attribute, "must be a work email address")
end
end
end
Step 3: Use in Your Model
# app/models/user.rb
class User < ApplicationRecord
validates :email,
presence: true,
uniqueness: true,
real_email: true
# B2B: also block free providers
# validates :email, real_email: { block_free: true }
end
Controller-Level Validation
class RegistrationsController < ApplicationController
def create
result = EmailValidator.validate(params[:email])
if result["did_you_mean"]
render json: { suggestion: result["did_you_mean"] }, status: 422
return
end
if result["disposable"]
render json: { error: "Disposable emails not allowed" }, status: 422
return
end
@user = User.new(user_params)
if @user.save
render json: @user, status: :created
else
render json: { errors: @user.errors }, status: 422
end
end
end
Background Validation with Active Job
# app/jobs/validate_email_job.rb
class ValidateEmailJob < ApplicationJob
queue_as :default
def perform(user_id)
user = User.find(user_id)
result = EmailValidator.validate(user.email)
if result["disposable"]
user.update(email_status: "disposable")
elsif result["valid"]
user.update(email_status: "verified")
else
user.update(email_status: "invalid")
end
end
end
Best Practices
- Cache with Rails.cache — use Redis in production.
- Fail open — never block a signup because the API is temporarily unreachable.
- Use Rails credentials — more secure than plain ENV vars for API keys.
- Show typo suggestions —
did_you_meanconverts better than a hard rejection.
Getting Started
Sign up for a free Mailchk account. 200 validations/month free, sub-50ms response time.



