EU VAT Number Validation in Ruby
A tutorial for validating EU VAT numbers via VIES in Ruby using the vatnode REST API. Examples with Net::HTTP, Faraday, and a Rails concern pattern.
Basic validation with Net::HTTP
Ruby's standard library is sufficient — no gems required:
Ruby — Net::HTTP
require 'net/http'
require 'uri'
require 'json'
def validate_vat(vat_id)
api_key = ENV.fetch('VATNODE_API_KEY')
uri = URI("https://api.vatnode.dev/v1/vat/#{URI.encode_uri_component(vat_id)}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.read_timeout = 10
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{api_key}"
response = http.request(request)
case response.code.to_i
when 200
JSON.parse(response.body)
when 503
{ 'error' => 'VIES_UNAVAILABLE' }
else
{ 'error' => "HTTP #{response.code}" }
end
end
result = validate_vat('IE6388047V')
if result['error'] == 'VIES_UNAVAILABLE'
puts 'VIES temporarily unavailable — queue for retry'
elsif result['valid']
puts "Valid: #{result['companyName']}"
puts "Check ID: #{result['checkId']}" # store on invoice
else
puts 'Invalid VAT number'
endUsing Faraday
Ruby — Faraday
require 'faraday'
require 'json'
CONNECTION = Faraday.new('https://api.vatnode.dev') do |f|
f.headers['Authorization'] = "Bearer #{ENV.fetch('VATNODE_API_KEY')}"
f.options.timeout = 10
f.adapter Faraday.default_adapter
end
def validate_vat_faraday(vat_id)
response = CONNECTION.get("/v1/vat/#{URI.encode_uri_component(vat_id)}")
return { error: 'VIES_UNAVAILABLE' } if response.status == 503
JSON.parse(response.body, symbolize_names: true)
rescue Faraday::TimeoutError
{ error: 'TIMEOUT' }
end
result = validate_vat_faraday('FR12345678901')
apply_reverse_charge = result[:valid] == trueRails concern pattern
Encapsulate VAT validation as a Rails concern for use in models or services:
Ruby on Rails — concern
# app/concerns/vat_validatable.rb
module VatValidatable
extend ActiveSupport::Concern
def validate_vat_number(vat_id)
response = Faraday.get(
"https://api.vatnode.dev/v1/vat/#{URI.encode_uri_component(vat_id)}",
{},
{ 'Authorization' => "Bearer #{Rails.application.credentials.vatnode_api_key}" }
)
return :vies_unavailable if response.status == 503
return :invalid if response.status != 200
data = JSON.parse(response.body, symbolize_names: true)
data[:valid] ? :valid : :invalid
rescue Faraday::Error
:vies_unavailable
end
end
# app/models/customer.rb
class Customer < ApplicationRecord
include VatValidatable
before_save :check_vat_status, if: :vat_number_changed?
private
def check_vat_status
status = validate_vat_number(vat_number)
case status
when :valid
self.vat_valid = true
self.vat_checked_at = Time.current
when :invalid
self.vat_valid = false
when :vies_unavailable
# Queue async job — do not block save
VatValidationJob.perform_later(id)
end
end
endQueue on VIES unavailability. When the API returns 503, enqueue a background job (Sidekiq, Delayed::Job) to retry the check. Never block a transaction because VIES is temporarily down.
Add VAT validation to your Ruby application
Free plan: 20 requests/month, no credit card. Works with Net::HTTP, Faraday, and Rails.