← Blog

How to validate a phone number in Go

Validate and normalize phone numbers in Go for free with one library — and see exactly where a live carrier lookup is the only thing it can't do.

Contents

You have a string that claims to be a phone number, and you want Go to tell you two things: is it real, and what does it look like once it's cleaned up. You can validate a phone number in Go entirely in-process — no network call, no API key, no cost — using one well-maintained library. This post covers exactly that, then draws a clean line around the one question the library cannot answer.

Use nyaruka/phonenumbers

The library is github.com/nyaruka/phonenumbers, a Go port of Google's libphonenumber. It carries the same metadata Google ships, so it knows the numbering plans for every country.

go get github.com/nyaruka/phonenumbers

Parsing is one call. Pass the raw string and a default region — an ISO country code like "US" — that tells the parser how to read numbers written without a + country code. If the number is already in E.164 form, the region is ignored, so you can pass "".

package main

import (
	"fmt"

	"github.com/nyaruka/phonenumbers"
)

func main() {
	num, err := phonenumbers.Parse("(650) 447-2983", "US")
	if err != nil {
		fmt.Println("could not parse:", err)
		return
	}
	fmt.Println(num.GetCountryCode(), num.GetNationalNumber())
}

Parse returns an error only when the input can't be read as a number at all. A string that parses is not necessarily valid — that's the next step.

Validate and normalize to E.164

phonenumbers.IsValidNumber checks the parsed number against the numbering plan for its region: right length, valid prefix, a real range. phonenumbers.Format with phonenumbers.E164 gives you the canonical +<country><number> string you should store.

num, err := phonenumbers.Parse("(650) 447-2983", "US")
if err != nil {
	fmt.Println("could not parse:", err)
	return
}

if !phonenumbers.IsValidNumber(num) {
	fmt.Println("not a valid number")
	return
}

fmt.Println(phonenumbers.Format(num, phonenumbers.E164)) // +16504472983

If you want a cheaper pre-check, phonenumbers.IsPossibleNumber only asks whether the digit count is plausible for the region. It's faster, but it passes numbers that IsValidNumber rejects, so treat possibility as a filter and validity as the gate — for anything you store, use IsValidNumber.

Store the E.164 string, not the raw input. It's unambiguous, comparable, and what every downstream API expects. For the background on why that format exists, see what is E.164.

Read the line type

phonenumbers.GetNumberType tells you whether the metadata marks a number as mobile, fixed line, toll-free, and so on.

switch phonenumbers.GetNumberType(num) {
case phonenumbers.MOBILE:
	fmt.Println("mobile")
case phonenumbers.FIXED_LINE:
	fmt.Println("fixed line")
case phonenumbers.FIXED_LINE_OR_MOBILE:
	fmt.Println("could be either — the metadata can't tell")
default:
	fmt.Println("other")
}

Note the FIXED_LINE_OR_MOBILE case. In several countries — the US among them — mobile and landline ranges overlap, so the honest answer is "either." The library returns that instead of guessing, which is the right call. +16504472983 resolves to exactly that.

What the library cannot tell you

Everything above is structural. It answers "is this a well-formed, assignable number, and what type of line is that range" — all from static metadata compiled into your binary. It does not, and cannot, tell you:

  • whether the number is connected right now,
  • which carrier actually holds it today, or
  • whether it's been ported to a different network.

Those facts don't live in metadata. They live on the carrier network, and reading them means a live HLR lookup — a different data source with a real per-query cost. No in-process library gives you that, in Go or any other language. Be suspicious of one that claims to. Even the carrier a range was originally allocated to is not necessarily the carrier holding a specific number today; portability is exactly the gap static metadata can't close.

Where an API fits, honestly

If your stack is Go end to end, nyaruka/phonenumbers is very likely all you need. Ship it and move on.

An API earns its place in two situations. First, a polyglot backend: the same validation running in a Go service, a Node worker, and a Python job drifts unless it comes from one source. Boundstone returns one identical JSON contract to every client — the JavaScript version of this post hits the same endpoint and gets the same shape.

Second — eventually — the network layer the library can't reach. Here's the honest state of it. Boundstone's phone endpoint runs the same libphonenumber checks and reports them plainly:

"checks": {
  "performed":     ["format", "region", "line_type_metadata"],
  "not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
}

Carrier lookup, ported status, and HLR liveness are not_performed by default — not faked, and not hidden. They run as a paid opt-in, hlr:true at 5 credits, priced for what a real dip costs and refunded when the network cannot answer. Until then the endpoint gives you the same answer the library does, plus an explicit list of what it did not check. You can try it on any number, no signup, with the free phone validator.

A quick Go call, for when you want it in a service:

body, _ := json.Marshal(map[string]string{"phone": "+16504472983"})
req, _ := http.NewRequest("POST",
	"https://api.boundstone.io/v1/verify/phone", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer bs_live_YOUR_KEY")
req.Header.Set("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)
// decode resp.Body into your struct; read out.Checks.NotPerformed

The short version

  • To validate a phone number in Go, use github.com/nyaruka/phonenumbers: Parse, then IsValidNumber, then Format(num, phonenumbers.E164). Free, in-process, correct.
  • IsPossibleNumber is a fast pre-filter, not a substitute — possibility is not validity.
  • Store the E.164 string, not what the user typed.
  • GetNumberType gives line type from metadata; trust FIXED_LINE_OR_MOBILE when it appears — it's honest, not lazy.
  • No library — Boundstone included, today — knows whether a number is live, ported, or which carrier holds it now. That needs an HLR lookup.
  • Reach for the API when you need one contract across several languages, or later for the carrier/HLR layer once it ships. Otherwise the library is enough, and saying so is the point.

Frequently asked questions

How do I validate a phone number in Go?

In Go you can validate a phone number's format and region with a libphonenumber-based library (such as the nyaruka/phonenumbers port) or by calling a validation API. Boundstone's POST /v1/verify/phone endpoint takes a number and returns whether it is validly formatted for its region, along with the E.164 and national forms and line-type metadata. Every response also lists exactly which checks ran under checks.performed (format, region, line_type_metadata) and which did not, so you know the limits of the result before you rely on it.

Does a valid phone number mean it can actually receive calls or texts?

No. Format validation only confirms that a number is well-formed and possible for its region; it cannot tell you whether the line is currently active, assigned, or reachable. Boundstone is explicit about this in every response: carrier_lookup, ported_status, and hlr_liveness are listed under checks.not_performed by default, though a live HLR dip (carrier, ported status, reachability) is available as a paid opt-in with hlr:true. So a valid result is a shape-and-region guarantee, not a proof of deliverability.

How can I tell if a phone number is a mobile or a landline in Go?

Libphonenumber-based tooling, and Boundstone's line_type_metadata check, infers line type from the number's published numbering ranges, so you can often distinguish mobile from fixed-line without any live lookup. Keep in mind this is metadata, not a carrier query: Boundstone does not perform carrier_lookup or ported_status by default (both are a paid opt-in via hlr:true), so on a metadata-only check a number ported between a landline and mobile carrier may still report its original range's type. In regions where a range is shared, the metadata can be genuinely ambiguous rather than a definitive mobile-or-landline answer.

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