---
title: "Getting started with OneContact"
url: https://recursiveai.org/onecontact/docs/getting-started/
description: "Install OneContact 1.0.0 for .NET 10, then validate and format a phone number, check an email address, parse a web address, read a postal address and pull…"
version: "1.0.0"
---

# Getting started with OneContact

Install OneContact 1.0.0 for .NET 10, then validate and format a phone number, check an email address, parse a web address, read a postal address and pull the contacts out of a small web page. Every sample on this page was run on the 1.0.0 packages from NuGet and shows its real output. The inputs are made up: phone numbers from ranges set aside for fiction, and `example.com` addresses.

## Install

The packages target `net10.0`, so you need the .NET 10 SDK.

```shell
dotnet new console -o OneContactStart
cd OneContactStart
dotnet add package RecursiveAI.OneContact --version 1.0.0
dotnet add package RecursiveAI.OneContact.Extraction --version 1.0.0
```

Three packages are on NuGet at 1.0.0, all on one version:

- **`RecursiveAI.OneContact`**: phones, email, web addresses, postal addresses, names and companies. It depends on the .NET base class library only.
- **`RecursiveAI.OneContact.Extraction`**: `ContactExtractor`, which finds people, organizations and their contact fields in HTML and plain text. It brings in the core and OneHtml 1.6.0.
- **`RecursiveAI.OneContact.Phones.Geo`**: the geocoder, carrier and time-zone mappers for phone numbers.

None of them uses native code, and in the README’s words, “Nothing in the packages opens a socket, resolves a name or downloads a file.”

Version 1.0 has no trained models. In the README’s words: “Version 1.0 runs on rules and markup alone. The trained model packages (`RecursiveAI.OneContact.Models.Tier1`, `.Models` and `.Models.Accurate`) follow in 1.1, after OneNLP 1.0.” Text the rules can’t read is refused with a reason, never guessed.

The samples below are one `Program.cs`, run top to bottom. It starts with these lines:

```csharp
using RecursiveAI.OneContact;
using RecursiveAI.OneContact.Extraction;
using RecursiveAI.OneContact.Extraction.Records;
```

## Validate and format a phone number

A national number needs a region; one written with `+` and its country code doesn’t. A parsed number has its E.164 form, every format, a number type and a validity check against the numbering plan.

```csharp
// A national number needs a region; an international one does not.
PhoneNumber phone = PhoneNumber.Parse("(202) 555-0145", Region.US);
Console.WriteLine(phone.E164);
Console.WriteLine(phone.Format(PhoneFormat.International));
Console.WriteLine(phone.Format(PhoneFormat.National));
Console.WriteLine(phone.Format(PhoneFormat.Rfc3966));
Console.WriteLine($"{phone.Type} | valid={phone.IsValid} | region {phone.Region}");

PhoneNumber london = PhoneNumber.Parse("+44 20 7946 0958");
Console.WriteLine($"{london.Format(PhoneFormat.National)} | {london.Type} | valid={london.IsValid} | region {london.Region}");

Console.WriteLine(PhoneNumber.TryParse("2025550145", out _) ? "parsed" : "refused: a national number needs a region");
Console.WriteLine(Contacts.SameKey("(202) 555-0145", "+1 202.555.0145", Region.US));
```

```text
+12025550145
+1 202-555-0145
(202) 555-0145
tel:+1-202-555-0145
FixedLineOrMobile | valid=True | region US
020 7946 0958 | FixedLine | valid=True | region GB
refused: a national number needs a region
True
```

A national number with no region is refused rather than guessed. `Contacts.SameKey` compares two values by their comparison key, so differently written forms of one number match. The project checks these results against libphonenumber. For parsing, validation, number types, every format, region, possibility and example numbers, across every region and non-geographic code, the README counts mismatches on generated cases against libphonenumber 9.0.43: “0 of 2,819,630 results (141,206 cases, 2 seeds)”. [Phone numbers](https://recursiveai.org/onecontact/docs/phones/) covers the as-you-type formatter, the matcher that finds numbers in text, and short numbers.

## Check an email address

`EmailAddress.Parse` reads one mailbox by RFC 5322 and RFC 6531, with or without a display name, and throws `ContactParseException` with a reason when the address isn’t one. `TryParse` does the same without the exception.

```csharp
foreach (string text in new[] { "Jo Example <Jo.Example+news@Example.com>", "sales@example.com", "jo@example..com" })
{
    try
    {
        EmailAddress email = EmailAddress.Parse(text);
        Console.WriteLine($"{email.Canonical} | {email.DisplayName} | {email.Flags}");
    }
    catch (ContactParseException e)
    {
        Console.WriteLine($"refused: {e.Reason.Id}");
    }
}
```

```text
Jo.Example+news@example.com | Jo Example | HasDisplayName
sales@example.com |  | RoleAccount
refused: email.invalid-domain
```

The canonical form keeps the local part as written and lowercases the domain. `RoleAccount` marks a role address such as `sales@`. The empty label in `example..com` makes the domain invalid. Flags such as `FreeProvider`, `Disposable` and `PossibleTypo` only report: in the docs’ words on typo suggestions, “nothing is ever changed”. [Email addresses](https://recursiveai.org/onecontact/docs/email/) has every rule, flag and provider key.

## Parse a web address

`WebAddress` follows the WHATWG URL Standard and reads the registrable domain from the Public Suffix List. The canonical form drops tracking parameters and says so in its flags.

```csharp
WebAddress web = WebAddress.Parse("HTTPS://Shop.Example.co.uk:443/a/../Sale?utm_campaign=fall&id=7");
Console.WriteLine(web.Canonical);
Console.WriteLine($"{web.Host} | {web.RegistrableDomain} | {web.Flags}");
Console.WriteLine($"key {web.Key}");
```

```text
https://shop.example.co.uk/Sale?id=7
shop.example.co.uk | example.co.uk | TrackingParametersRemoved
key example.co.uk/sale?id=7
```

The scheme and host are lowercased, the default port and the `..` segment are gone, and `utm_campaign` is removed while `id` stays. `co.uk` is a public suffix, so the registrable domain is `example.co.uk`. [Web addresses](https://recursiveai.org/onecontact/docs/web/) lists the tracking parameters that are removed and the social-profile flags.

## Read a postal address

`PostalAddress.Parse` reads free text, writes it back in the postal authority’s style, and validates it against the country’s structure. Full free-text parsing covers the United States, Canada, Mexico, Australia, the United Kingdom and Crown dependencies, and the Latin-script countries of Europe; every other country has the structural layer (fields, layout, required fields, postcode patterns).

```csharp
PostalAddress address = PostalAddress.Parse("Jo Example\n123 north main street suite 400\nspringfield, il 62704");
Console.WriteLine($"{address.Country.Code} | {address.Locality} | {address.AdminArea} | {address.PostalCode} | {address.Coverage}");
Console.WriteLine(address.ToString(AddressStyle.Postal, AddressLayout.Label));
Console.WriteLine(address.ToString(AddressStyle.Display, AddressLayout.SingleLine));
Console.WriteLine($"{address.Validate().Count} structural issues");
```

```text
US | springfield | IL | 62704 | Full
JO EXAMPLE
123 N MAIN ST STE 400
SPRINGFIELD IL  62704
Jo Example, 123 N Main St Ste 400, Springfield, IL 62704
0 structural issues
```

The country came from the text: a state and a ZIP code. The fields keep what was written (`springfield`), and the two styles standardize it: `Postal` is USPS Publication 28 (capitals, `N`, `ST`, `STE`), and `Display` is the same words in title case.

When the text shows its country, the country is read from it and flagged `CountryInferred`. When it doesn’t, `PostalAddress` refuses it, and you give the country:

```csharp
// The country is read from the text when the text shows it ...
PostalAddress inferred = PostalAddress.Parse("Musterstraße 12, 10115 Berlin");
Console.WriteLine($"{inferred.Country.Code} | {inferred.Flags} | {inferred.ToString(AddressStyle.Postal, AddressLayout.SingleLine)}");

// ... and text that doesn't say which country it is in is refused: give the country.
string market = "Am Markt 5, 10117 Berlin";
Console.WriteLine(PostalAddress.TryParse(market, out _) ? "parsed" : "refused: the country can't be told");
Console.WriteLine(PostalAddress.Parse(market, Region.Parse("DE")).ToString(AddressStyle.Postal, AddressLayout.SingleLine));
```

```text
DE | CountryInferred | Musterstraße 12, 10115 BERLIN
refused: the country can't be told
Am Markt 5, 10117 BERLIN
```

In the README’s words, the rules “never read a text in the wrong country and never print a value the text does not contain.” [When the country can’t be told](https://recursiveai.org/onecontact/docs/address-country-undetermined/) explains the inference, and each country group has its own page under Postal addresses.

## Extract contacts from a web page

`ContactExtractor` reads a page you give it: structured markup (JSON-LD, microdata, RDFa Lite, h-card, `mailto:` and `tel:` links, `<address>`) and the visible text. It groups what it finds into person and organization records. It never fetches anything.

```csharp
string html = """
<!doctype html>
<html lang="en-US">
<head>
<title>Contact · Example Bakery</title>
<script type="application/ld+json">
{"@context": "https://schema.org", "@type": "Bakery", "name": "Example Bakery",
 "telephone": "+1 217 555 0123", "url": "https://bakery.example.com/",
 "address": {"@type": "PostalAddress", "streetAddress": "10 Elm Street",
   "addressLocality": "Springfield", "addressRegion": "IL", "postalCode": "62704", "addressCountry": "US"}}
</script>
</head>
<body>
  <h1>Contact us</h1>
  <section>
    <h2>Wholesale orders</h2>
    <p>Jo Example, Head Baker</p>
    <p>Phone: (217) 555-0188</p>
    <p>Email: jo [at] example [dot] com</p>
  </section>
</body>
</html>
""";

ExtractResult result = new ContactExtractor().ExtractHtml(html);
foreach (ContactRecord r in result.Records)
{
    Console.WriteLine($"{r.Kind}: {r.Name?.Text}");
    foreach (ExtractedField f in r.Fields)
        Console.WriteLine($"  {f.Kind,-8} {f.Text.Replace("\n", ", "),-42} {f.Source} {f.Flags}");
}
```

```text
Organization: Example Bakery
  Name     Example Bakery                             JsonLd None
  Phone    +1 217 555 0123                            JsonLd None
  Website  https://bakery.example.com/                JsonLd None
  Address  10 Elm Street, Springfield, IL, 62704, US  JsonLd Composed
Person: Jo Example
  Name     Jo Example                                 Text None
  Phone    (217) 555-0188                             Text RegionInferred
  Email    jo@example.com                             Text Deobfuscated
```

The bakery comes from its JSON-LD, and its address is `Composed` from the structured parts. Jo Example comes from the visible text. The national phone number took its region from the page’s `lang="en-US"` (`RegionInferred`), and the `[at]`/`[dot]` email was decoded (`Deobfuscated`). In the docs’ words, “Job titles are evidence only and are never returned.” Every field also carries its offsets into the page, a confidence and its normalized value. `ExtractText` does the same for plain text such as a CRM note.

Names, emails, phones and addresses are personal data. In the docs’ words, extraction “runs offline on exactly what the caller passes in, and the library keeps no copy; responsibility for a lawful basis and for compliance with data-protection law (for example the GDPR) for what is extracted and what is done with it sits with the caller.” See [extraction](https://recursiveai.org/onecontact/docs/extraction/).

## Next

- [Overview](https://recursiveai.org/onecontact/docs/overview/): the packages, the quick start and every result with what it was measured against.
- [Coming from another library](https://recursiveai.org/onecontact/docs/migrating/): libphonenumber, libpostal, python-nameparser and probablepeople, call by call.
- [Person names](https://recursiveai.org/onecontact/docs/names/) and [company names](https://recursiveai.org/onecontact/docs/company-names/): titles, particles, name order, several people in one string, and ISO 20275 legal forms.
- [Geocoder, carrier and time zones](https://recursiveai.org/onecontact/docs/phones-geo/): `RecursiveAI.OneContact.Phones.Geo`, with the original-carrier caveat.
