# Validate a phone number in Ruby with phonelib

_2026-09-04 · Boundstone (https://boundstone.io/blog/validate-phone-ruby)_


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:

```ruby
gem "phonelib"
```

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

```ruby
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:

```ruby
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](/blog/what-is-e164).

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

```ruby
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:

```ruby
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](/blog/what-is-phone-line-type).

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](/docs) sits on: one HTTP endpoint, one response shape, every language.

```ruby
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](/tools/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:

```ruby
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.
