← Blog

Validate a phone number in Ruby with phonelib

The phonelib gem validates and formats phone numbers to E.164 locally for free — here's how, and where an API earns its place.

Contents

You want to validate a phone number in Ruby: confirm it could be real, and normalize it before it lands in your database. For most Ruby apps you do not need an API for this. You need the phonelib gem — a Ruby port of Google's libphonenumber — and it runs on your own machine, for free.

Add it to your Gemfile:

gem "phonelib"

Then bundle install, and you have Phonelib.valid?:

require "phonelib"

Phonelib.valid?("+16504472983")   # => true
Phonelib.valid?("+1415555")       # => false
Phonelib.valid?("not a phone")    # => false

valid? returns true only when the number matches the published numbering plan for its region — right length, right prefix, a real number range. That already rejects most of the junk a signup form collects, and it does it without a network call.

Parse once, read the fields

Calling Phonelib.parse gives you a phone object you can interrogate:

phone = Phonelib.parse("+16504472983")

phone.valid?    # => true
phone.e164      # => "+16504472983"
phone.country   # => "US"
phone.type      # => :fixed_or_mobile
phone.national  # => "(650) 447-2983"

Store phone.e164. It is the one unambiguous international format — country code, no spaces, no punctuation — and it is what every downstream system (your SMS provider, your CRM, the next validator) expects. If you are fuzzy on why, we wrote up what E.164 is and why you should store it.

If your input arrives in national format, set a default region so phonelib knows how to read it:

Phonelib.default_country = "US"

Phonelib.parse("(650) 447-2983").e164   # => "+16504472983"

What the line type does and does not tell you

phone.type returns a symbol from libphonenumber's metadata: :mobile, :fixed_line, :premium_rate, :toll_free, and so on. Useful — until you hit North America, where the metadata cannot separate the first two:

Phonelib.parse("+16504472983").type   # => :fixed_or_mobile

That :fixed_or_mobile is not a bug. It is the honest answer: the North American numbering plan does not encode line type into the number, so no offline library — phonelib, libphonenumber, or anything built on them — can tell you whether that number is a mobile or a landline. It can only tell you the range allows both. For the full picture of what line type can and cannot promise, see what phone line type actually means.

This matters because "mobile or landline?" is a question people expect a real answer to, and a truthful "we cannot tell from the number alone" beats a confident guess.

When you need one contract across languages

phonelib is the right tool inside a Ruby process. Where it stops helping is when the same rule has to hold across a stack that is not all Ruby — a Rails API, a Go worker, a no-code form, three services that must agree on what "valid" means. Now you are maintaining libphonenumber ports in several runtimes and hoping they stay in step.

That is the layer Boundstone sits on: one HTTP endpoint, one response shape, every language.

require "net/http"
require "json"

uri  = URI("https://api.boundstone.io/v1/verify/phone")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer bs_live_YOUR_KEY"
request["Content-Type"]  = "application/json"
request.body = JSON.generate(phone: "+16504472983")

response = JSON.parse(http.request(request).body)

response["valid"]      # => true
response["e164"]       # => "+16504472983"
response["line_type"]  # => "fixed_line_or_mobile"

Same format, region, and line-type metadata phonelib gives you — because under the hood it is the same libphonenumber logic, and it reports the same North American ambiguity rather than papering over it. What you buy is not more magic per call; it is one contract your Ruby, Go, and no-code surfaces can all call, plus the honesty contract below.

Want to try it with no key and no signup? Paste a number into the free phone validator.

The part we do not do (yet), stated plainly

Every Boundstone response tells you exactly what it checked and what it did not:

response["checks"]["performed"]
# => ["format", "region", "line_type_metadata"]

response["checks"]["not_performed"]
# => ["carrier_lookup", "ported_status", "hlr_liveness"]

Read the second array. carrier_lookup, ported_status, and hlr_liveness are the things phonelib cannot do either — they require querying live carrier network databases, not parsing the number. Boundstone does not perform them by default. Carrier and HLR liveness (whether the line is currently reachable) run as a paid opt-in with hlr:true, at 5 credits, refunded when the network cannot answer. Until then the response says not_performed, so a "valid": true never pretends to be more than a format-and-region check. That is the point: you can trust the "valid" because you can see the edge of it.

The short version

  • To validate a phone number in Ruby, use the phonelib gem. Phonelib.valid? and Phonelib.parse(...).e164 cover the large majority of what apps need — locally, offline, and free.
  • Expect :fixed_or_mobile for North American numbers. That is the honest limit of any offline library, not a defect.
  • Reach for the API for one contract across languages. It also gives you the carrier and HLR liveness layer no local gem can provide, as a paid opt-in with hlr:true.
  • Whatever you use, store the E.164 string.

Frequently asked questions

How do I validate a phone number in Ruby using the phonelib gem?

Add phonelib to your Gemfile, then parse a number with Phonelib.parse("+16504472983") and check phone.valid? to confirm it is well-formed and possible for its region. You can also read phone.country, phone.type for the line type, and phone.e164 for a normalized format. phonelib is built on Google's libphonenumber, so it validates format and regional plausibility entirely offline with no per-lookup cost. Keep in mind that a valid? result proves the number is correctly formatted and could exist, not that the line is actually active or reachable.

Does phonelib tell me whether a phone number is real and can receive calls or texts?

No. phonelib checks a number's format, region, and line-type metadata against Google's libphonenumber dataset, but it never contacts a carrier, so it cannot confirm the line is active, whether it has been ported, or that it can actually receive a call or SMS. Confirming liveness requires a separate HLR network lookup. Boundstone's phone endpoint is honest about this same boundary: it performs format, region, and line-type metadata checks and explicitly reports carrier_lookup, ported_status, and hlr_liveness as not_performed by default, while offering a live HLR dip as a paid opt-in (hlr:true) for callers who need real carrier and reachability data.

Should I use phonelib locally or a phone validation API like Boundstone?

Use phonelib when you want fast, offline, zero-cost format and line-type validation directly inside your Ruby process and are comfortable keeping the gem's bundled metadata updated yourself. A hosted API like Boundstone runs the same metadata-grade checks (format, region, line type) over a simple HTTP call, ships every response with an explicit checks.performed and checks.not_performed list, and offers a free tier of 250 credits a month with no card and credits that never expire. Both approaches give you metadata-grade validation rather than proof that a line is live, so choose based on whether you would rather keep the logic in-process or offload it and get the honesty contract in every response.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist