# How to validate a phone number in Go

_2026-08-28 · Boundstone (https://boundstone.io/blog/validate-phone-go)_


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.

```bash
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 `""`.

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

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

## Read the line type

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

```go
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](/blog/validate-phone-javascript) 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:

```json
"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](/tools/phone-validator).

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

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