IBAN Validation in Ruby — Gems, Code Examples & Best Practices
How to validate IBANs in Ruby using ibanizator and banktools-eu gems, a pure-Ruby MOD-97 implementation, Rails model validator, and calling the ibanchecker.cash REST API for bank metadata.
IBAN validation in Ruby is a common requirement for fintech Rails applications, payment processing gems, and data import pipelines. This guide covers three approaches: using the ibanizator gem, using banktools-eu, and calling the ibanchecker.cash REST API when you need bank metadata alongside the validity result.
What Are the Validation Layers Every Ruby IBAN Checker Must Cover?
IBAN validation is a three-step process. A regex alone is not sufficient because it cannot verify the MOD-97 check digit embedded in positions 3–4 of every IBAN.
- Format check: Two uppercase letters (country code), two digits (check digits), alphanumeric BBAN. Strip spaces and dashes before checking.
- Length check: Each country has a fixed length. German IBANs are always 22 characters; UK IBANs are always 22; French IBANs are always 27. Reject anything that deviates.
- MOD-97 check: Move the first four characters to the end, replace each letter A–Z with its numeric equivalent (A=10 … Z=35), interpret the result as a large integer, and compute modulo 97. Valid IBANs produce a remainder of 1.
Which Ruby Gems Should You Use for IBAN Validation?
Two well-maintained gems cover all production use cases:
- ibanizator — pure Ruby, zero dependencies, covers all 84 IBAN countries, returns structured objects with country and BBAN.
- banktools-eu — focuses on European IBANs, also parses BIC and branch codes from the BBAN.
For most Rails applications, ibanizator is the correct default.
Using ibanizator
# Gemfile
gem "ibanizator"
# In your code
require "ibanizator"
iban = Ibanizator.iban_from_string("DE89 3704 0044 0532 0130 00")
puts iban.valid?
# => true
puts iban.country_code
# => :de
puts iban.bban
# => "370400440532013000"
invalid = Ibanizator.iban_from_string("DE00370400440532013000")
puts invalid.valid?
# => falseThe gem normalizes whitespace automatically — you can pass either the electronic format (DE89370400440532013000) or the printed format with spaces.
Using banktools-eu
# Gemfile
gem "banktools-eu"
require "banktools-eu"
iban = BankTools::EU::IBAN.new("GB29NWBK60161331926819")
puts iban.valid?
# => true
puts iban.errors
# => []
invalid = BankTools::EU::IBAN.new("GB29NWBK6016133192681X")
puts invalid.valid?
# => false
puts invalid.errors
# => [:invalid_characters]How Do You Implement MOD-97 Validation in Pure Ruby?
If you prefer no gem dependencies — useful in data migration scripts or lambdas where bundle size matters — implement the algorithm directly. Ruby handles arbitrarily large integers natively, so there is no BigInt equivalent needed.
IBAN_LENGTHS = {
"AD" => 24, "AE" => 23, "AL" => 28, "AT" => 20, "AZ" => 28,
"BA" => 20, "BE" => 16, "BG" => 22, "BH" => 22, "BR" => 29,
"BY" => 28, "CH" => 21, "CR" => 22, "CY" => 28, "CZ" => 24,
"DE" => 22, "DK" => 18, "DO" => 28, "EE" => 20, "EG" => 29,
"ES" => 24, "FI" => 18, "FO" => 18, "FR" => 27, "GB" => 22,
"GE" => 22, "GI" => 23, "GL" => 18, "GR" => 27, "GT" => 28,
"HR" => 21, "HU" => 28, "IE" => 22, "IL" => 23, "IQ" => 23,
"IS" => 26, "IT" => 27, "JO" => 30, "KW" => 30, "KZ" => 20,
"LB" => 28, "LC" => 32, "LI" => 21, "LT" => 20, "LU" => 20,
"LV" => 21, "LY" => 25, "MC" => 27, "MD" => 24, "ME" => 22,
"MK" => 19, "MR" => 27, "MT" => 31, "MU" => 30, "NL" => 18,
"NO" => 15, "PK" => 24, "PL" => 28, "PS" => 29, "PT" => 25,
"QA" => 29, "RO" => 24, "RS" => 22, "SA" => 24, "SC" => 31,
"SE" => 24, "SI" => 19, "SK" => 24, "SM" => 27, "ST" => 25,
"SV" => 28, "TL" => 23, "TN" => 24, "TR" => 26, "UA" => 29,
"VA" => 22, "VG" => 24, "XK" => 20
}.freeze
def normalize_iban(raw)
raw.strip.gsub(/[s-]/, "").upcase
end
def validate_iban(raw)
iban = normalize_iban(raw)
return { valid: false, error: "Invalid format" } unless iban.match?(/A[A-Z]{2}[0-9]{2}[A-Z0-9]+z/)
country = iban[0, 2]
expected = IBAN_LENGTHS[country]
return { valid: false, error: "Unknown country: #{country}" } unless expected
return { valid: false, error: "Wrong length: expected #{expected}, got #{iban.length}" } if iban.length != expected
rearranged = iban[4..] + iban[0, 4]
numeric = rearranged.chars.map { |c| c =~ /[A-Z]/ ? (c.ord - 55).to_s : c }.join
if numeric.to_i % 97 != 1
return { valid: false, error: "Check digit validation failed (MOD-97)" }
end
{ valid: true }
end
p validate_iban("DE89 3704 0044 0532 0130 00")
# => {:valid=>true}
p validate_iban("DE89370400440532013001")
# => {:valid=>false, :error=>"Check digit validation failed (MOD-97)"}How Do You Integrate IBAN Validation in a Rails Model?
Add a custom validator that uses the gem and optionally calls the API for bank metadata enrichment. The local check runs synchronously in the model; the API call is best deferred to a background job to avoid blocking web requests.
# app/validators/iban_validator.rb
require "ibanizator"
class IbanValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
iban = Ibanizator.iban_from_string(value.to_s)
unless iban.valid?
record.errors.add(attribute, options[:message] || "is not a valid IBAN")
end
end
end
# app/models/bank_account.rb
class BankAccount < ApplicationRecord
validates :iban, presence: true, iban: true
before_save :normalize_iban
private
def normalize_iban
self.iban = iban.to_s.strip.gsub(/[s-]/, "").upcase if iban.present?
end
endHow Do You Call the ibanchecker.cash API from Ruby?
Use Net::HTTP for stdlib-only environments, or Faraday/HTTParty in Rails applications. The API returns bank name, BIC, and country alongside the validity flag — useful for autofilling bank name fields in payment forms.
require "net/http"
require "json"
def validate_iban_api(iban)
uri = URI("https://ibanchecker.cash/api/v1/validate")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = { iban: iban }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
JSON.parse(res.body, symbolize_names: true)
end
result = validate_iban_api("DE89370400440532013000")
puts result
# {valid: true, iban: "DE89370400440532013000", country: "Germany",
# bankName: "Deutsche Bank", bic: "DEUTDEDB", formatted: "DE89 3704 0044 0532 0130 00"}For bulk validation of up to 100 IBANs in one request:
def validate_ibans_bulk(ibans)
uri = URI("https://ibanchecker.cash/api/v1/validate/bulk")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = { ibans: ibans }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
JSON.parse(res.body, symbolize_names: true)[:results]
end
results = validate_ibans_bulk([
"DE89370400440532013000",
"GB29NWBK60161331926819",
"INVALID",
])
results.each { |r| puts "#{r[:iban]}: #{r[:valid]}" }How Should You Test IBAN Validation in RSpec?
# spec/validators/iban_validator_spec.rb
RSpec.describe IbanValidator do
let(:record) { BankAccount.new }
VALID_IBANS = %w[
DE89370400440532013000
GB29NWBK60161331926819
FR7630006000011234567890189
NL91ABNA0417164300
BE68539007547034
].freeze
INVALID_IBANS = %w[
DE89370400440532013001
GB29NWBK6016133192681
NOTANIBAN
DE00000000000000000000
].freeze
VALID_IBANS.each do |iban|
it "accepts #{iban}" do
record.iban = iban
record.valid?
expect(record.errors[:iban]).to be_empty
end
end
INVALID_IBANS.each do |iban|
it "rejects #{iban}" do
record.iban = iban
record.valid?
expect(record.errors[:iban]).not_to be_empty
end
end
it "normalizes spaces before validation" do
record.iban = "DE89 3704 0044 0532 0130 00"
record.valid?
expect(record.errors[:iban]).to be_empty
expect(record.iban).to eq("DE89370400440532013000")
end
endSee the API documentation for authentication details and the pricing page for rate limits on each plan.
Last updated: June 2026
Validate an IBAN instantly
Free IBAN checker — MOD-97 verification, bank lookup, and SEPA status across 84 countries.
Open IBAN Checker →Related Articles