# Validate a phone number in C# with libphonenumber-csharp

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


You want to validate a phone number in C#, and you want more than a regex that accepts anything with a `+` and a run of digits. Good news: you can do this locally, for free, with a library that already knows the numbering plan of every country. Better news: if your stack is all .NET, you probably don't need a paid API to start. Here's the honest version — what the library does, what it can't, and the one place an API earns a line in your budget.

## Start local: libphonenumber-csharp

[libphonenumber-csharp](https://www.nuget.org/packages/libphonenumber-csharp) is a direct port of Google's libphonenumber — the same metadata that powers Android's dialer, packaged for .NET under the `PhoneNumbers` namespace. It runs offline. No network call, no per-lookup cost.

```bash
dotnet add package libphonenumber-csharp
```

The entry point is `PhoneNumberUtil.GetInstance()`. It loads bundled metadata, so grab it once and reuse it — don't call it per request.

```csharp
using PhoneNumbers;

var util = PhoneNumberUtil.GetInstance();

try
{
    PhoneNumber number = util.Parse("650-447-2983", "US");

    bool isValid = util.IsValidNumber(number);
    string e164 = util.Format(number, PhoneNumberFormat.E164);
    PhoneNumberType type = util.GetNumberType(number);

    Console.WriteLine($"Valid:     {isValid}");   // True
    Console.WriteLine($"E.164:     {e164}");      // +16504472983
    Console.WriteLine($"Line type: {type}");      // FIXED_LINE_OR_MOBILE
}
catch (NumberParseException ex)
{
    Console.WriteLine($"Could not parse: {ex.Message}");
}
```

`Parse` throws `NumberParseException` on input it can't make sense of, so it belongs in a `try/catch` — a bare regex would just silently return false and lose the reason. `IsValidNumber` then checks the parsed number against the assigned ranges for its region. There's also a looser, cheaper `IsPossibleNumber` if you only care whether the length could plausibly work.

## Region matters: Parse needs to know where a number is from

`Parse` takes a default region as its second argument. For a number already in E.164 form (`+16504472983`), the `+` carries the country code and you can pass `null`. For a national-format string like `(650) 447-2983`, the library has no way to guess the country — you must tell it.

```csharp
util.Parse("+61 2 8123 4567", null);   // country code is in the number
util.Parse("02 8123 4567", "AU");      // needs the region hint
```

Once parsed, formatting is a menu: `PhoneNumberFormat.E164` for storage, `INTERNATIONAL` and `NATIONAL` for display, `RFC3966` for `tel:` links. Store E.164, show whatever your UI wants. (If E.164 itself is new to you, we wrote up [what E.164 is and why you should store it](/blog/what-is-e164).)

## Read the line type — and what it can't promise

`GetNumberType` returns a `PhoneNumberType`: `MOBILE`, `FIXED_LINE`, `TOLL_FREE`, `VOIP`, and friends. It's useful for routing an SMS versus a voice call — but read the fine print. In North America many ranges come back as `FIXED_LINE_OR_MOBILE`, because the metadata genuinely can't tell the two apart from the number alone. That's not the library hedging; that's the honest answer.

The deeper limit is the important one: **valid does not mean reachable.** libphonenumber checks format and assigned range, offline, against its bundled tables. It does not dial the number, does not ask the carrier, and does not know whether the line is disconnected or has been ported to a different network. `IsValidNumber` returns `True` for a well-formed number in a live range even if nobody has answered it in years. That's a different question — and worth being clear about before you treat a green check as "this person can receive my text."

## A small reusable check

```csharp
using PhoneNumbers;

public record PhoneCheck(bool Valid, string? E164, PhoneNumberType Type);

public static class PhoneValidation
{
    private static readonly PhoneNumberUtil Util = PhoneNumberUtil.GetInstance();

    public static PhoneCheck Check(string input, string defaultRegion)
    {
        try
        {
            PhoneNumber number = Util.Parse(input, defaultRegion);
            if (!Util.IsValidNumber(number))
                return new PhoneCheck(false, null, PhoneNumberType.UNKNOWN);

            return new PhoneCheck(
                Valid: true,
                E164: Util.Format(number, PhoneNumberFormat.E164),
                Type: Util.GetNumberType(number));
        }
        catch (NumberParseException)
        {
            return new PhoneCheck(false, null, PhoneNumberType.UNKNOWN);
        }
    }
}
```

Drop that into a form handler or a CSV import and you've covered the honest, unglamorous majority of the work: reject malformed input, normalize the rest to E.164, keep the line type around.

## Where an API fits — and where it doesn't yet

If your whole stack is C#, libphonenumber-csharp is genuinely most of what you need today, and it's free. Say that plainly to yourself before you reach for a subscription. There are two honest reasons to reach anyway:

**One contract across languages.** If your Node, Python, and Go services each bundle their own libphonenumber version, they drift — a number that's valid in one service is stale in another because the metadata versions differ. Boundstone's `POST /v1/verify/phone` gives every client the same answer over HTTP, no per-language upgrade dance.

**The layer past metadata.** Carrier lookup, ported status, and HLR liveness are exactly what a local library cannot do. Boundstone does not run them *by default* either — they are a paid opt-in, `hlr:true` at 5 credits, refunded when the network cannot answer. Left off, they are reported as `not_performed` rather than faked.

```bash
curl -s https://api.boundstone.io/v1/verify/phone \
  -H "Authorization: Bearer bs_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+16504472983"}'
```

```json
{
  "valid": true,
  "e164": "+16504472983",
  "country": "US",
  "line_type": "fixed_line_or_mobile",
  "national_format": "(650) 447-2983",
  "checks": {
    "performed": ["format", "region", "line_type_metadata"],
    "not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
  }
}
```

That `checks` block is the whole point. A `"valid": true` from Boundstone today means the same class of thing `IsValidNumber` means — format and range, not liveness — and we print `not_performed` so you never mistake the two. The list you can't yet rely on is precisely what makes the `valid` you *can* rely on trustworthy. Want to poke at it without a key? Try the [free phone validator tool](/tools/phone-validator). Writing the JVM side of this too? Here's the [Java version](/blog/validate-phone-java) — same contract, different language.

## The short version

- For C#, use **libphonenumber-csharp** locally: `Parse` → `IsValidNumber` → `Format(…, PhoneNumberFormat.E164)` → `GetNumberType`. It's free and offline, and often all you need.
- Always wrap `Parse` in a `try/catch` for `NumberParseException`, and pass a default region for national-format input.
- **Valid is not reachable.** The library checks format and range, never the live network. Expect `FIXED_LINE_OR_MOBILE` on many ranges.
- Reach for the API when you need one consistent contract across languages, or the carrier/HLR liveness layer — which isn't shipped yet, and we say so.
- Boundstone's free tier is 250 credits/month, no card, credits never expire — enough to wire the contract in and see the honesty block for yourself.
