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'
end

Using 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] == true

Rails 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
end

Queue 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.