Tutorial8 min readAlberto García

VAT API Ruby on Rails: Tax ID Validation for E-commerce

Ruby on Rails VAT validation using Net::HTTP — no additional gems required. Includes a service object, controller concern, and RSpec tests with WebMock stubs.

rubyrailsvatapitutorial

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

rubyapp/services/vat_validation_service.rb
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
end

Controller Concern

rubyapp/controllers/concerns/vat_validatable.rb
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
end

Controller Usage

rubyapp/controllers/checkouts_controller.rb
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
end

RSpec Tests with WebMock

rubyspec/services/vat_validation_service_spec.rb
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
end

Start validating EU VAT numbers

Free plan — 100 validations/month. No credit card required.

AG
Alberto García

Founder, TaxID

Building EU VAT validation tools for developers. Obsessed with compliance automation and developer experience.