Ruby on Rails handles the TaxID API call cleanly with either Net::HTTP from the standard library or the Faraday gem. This guide uses Net::HTTP so there are no additional dependencies. The pattern is a service object that is called from a controller concern — keeping the validation logic separate from your controller code and reusable across multiple controllers. For the use-case guide with step-by-step integration steps, see the Ruby on Rails VAT validation use case.
VatValidationService
require 'net/http'
require 'json'
class VatValidationService
BASE_URL = 'https://taxid.dev/api/v1'
Result = Struct.new(:valid, :status, :company_name, :address, :request_id, :error, keyword_init: true)
def self.validate(vat_number)
normalised = vat_number.gsub(/\s/, '').upcase
country = normalised[0..1]
uri = URI("#{BASE_URL}/validate/#{country}/#{normalised}")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{ENV['TAXID_API_KEY']}"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 5) do |http|
http.request(req)
end
data = JSON.parse(response.body, symbolize_names: true)
Result.new(
valid: data[:valid],
status: data[:status],
company_name: data[:company_name],
address: data[:address],
request_id: data[:request_id]
)
rescue => e
Rails.logger.warn "TaxID API error: #{e.message}"
Result.new(valid: false, status: 'service_unavailable', error: e.message)
end
endController Concern
module VatValidatable
extend ActiveSupport::Concern
private
def validate_vat_number(vat_number)
return nil if vat_number.blank?
result = VatValidationService.validate(vat_number)
case result.status
when 'active'
result
when 'inactive'
errors.add(:vat_number, 'is not currently registered in the EU VAT system')
nil
when 'format_invalid'
errors.add(:vat_number, 'format is invalid — check your country prefix and digit count')
nil
else # service_unavailable or error
# Don't block checkout — charge VAT and revalidate later
result
end
end
endController Usage
class CheckoutsController < ApplicationController
include VatValidatable
def create
@order = Order.new(order_params)
if (vat = params.dig(:order, :vat_number)).present?
result = validate_vat_number(vat)
if result.nil?
render :new, status: :unprocessable_entity and return
end
@order.company_name = result.company_name
@order.vat_request_id = result.request_id
@order.vat_status = result.status
end
if @order.save
redirect_to order_confirmation_path(@order)
else
render :new, status: :unprocessable_entity
end
end
endRSpec Tests with WebMock
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe VatValidationService do
let(:api_url) { /taxid.dev/ }
describe '.validate' do
context 'when VAT number is active' do
before do
stub_request(:get, api_url).to_return(
status: 200,
body: { valid: true, status: 'active', company_name: 'Acme GmbH',
address: 'Berlin', request_id: 'r1' }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it 'returns a valid result with company name' do
result = described_class.validate('DE123456789')
expect(result.valid).to be true
expect(result.status).to eq('active')
expect(result.company_name).to eq('Acme GmbH')
end
end
context 'when VAT number is inactive' do
before do
stub_request(:get, api_url).to_return(
status: 200,
body: { valid: false, status: 'inactive' }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it 'returns invalid result' do
result = described_class.validate('DE000000000')
expect(result.valid).to be false
expect(result.status).to eq('inactive')
end
end
context 'on network error' do
before { stub_request(:get, api_url).to_raise(Timeout::Error) }
it 'returns service_unavailable without raising' do
result = described_class.validate('DE123456789')
expect(result.status).to eq('service_unavailable')
end
end
end
endRelated guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.