Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Verify Phone Numbers (Before You Dial or Text)

Four checks, in order of cost: format, line type, person match, live line. Two of them are free.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

TL;DR

  • Verify in four steps: normalise to E.164, check the line type, confirm the number belongs to the person, then test that the line is live. Steps 1 and 2 cost nothing to run on a list you hold.
  • It takes about 30 minutes to set up. The LeadOcean calls below spend 3 records of the 1,000 on the Free plan.
  • LeadOcean returns numbers with a type and tells you who holds a number. It does not ping a line to see if it rings. For that you still need a carrier lookup from a telecom vendor.

Most guides on this topic list tools. This one is the order of operations a technical buyer would put in a script. Each step either throws a number out or earns it a dial.

Prerequisites

  • A LeadOcean API key in $LEADOCEAN_API_KEY (Free plan: 1,000 records for life, no card, 5 requests a second).
  • Python 3 with phonenumbers (pip install phonenumbers) for the local checks.
  • curl and jq.
  • A list of numbers you hold, or a person you want a number for.

Step 1: Normalise to E.164 and drop what cannot be a number

Format validation is free and removes the most junk. Parse every number with a default region, then keep only the ones that are possible and valid.

python
import phonenumbers

def clean(raw, region="US"):
    try:
        n = phonenumbers.parse(raw, region)
    except phonenumbers.NumberParseException:
        return None
    if not phonenumbers.is_possible_number(n):
        return None
    if not phonenumbers.is_valid_number(n):
        return None
    return phonenumbers.format_number(n, phonenumbers.PhoneNumberFormat.E164)

for raw in ["(415) 555-0133", "415-555-013", "+44 20 7946 0000"]:
    print(raw, "->", clean(raw))

is_possible_number checks length. is_valid_number checks that the prefix exists in that country's numbering plan. Neither proves the number is assigned to anyone. Treat a pass as "could exist".

Step 2: Check the line type

Mobile, direct and office numbers behave differently. Texting a switchboard wastes a send. A mobile is the line a rep reaches without a gatekeeper. phonenumbers can guess from the prefix, but it cannot see ported numbers.

python
from phonenumbers import number_type, PhoneNumberType
n = phonenumbers.parse("+14155550133")
print(number_type(n) == PhoneNumberType.MOBILE)  # prefix guess only

When you source numbers from LeadOcean, the type comes with the record. Ask for numbers on one person and read the type field:

bash
curl -s "https://api.leadocean.io/v2/people/phone?person_id=$PERSON_ID" \
  -H "x-api-key: $LEADOCEAN_API_KEY" | jq '.data.phones'

person_id comes from meta.source_ids.person_id on a people-search row. The call returns every number we hold, best first, and the position in the array is the priority. It costs one record per call, including a call that finds nothing.

Step 3: Size the list before you spend anything

To find people who have a number, filter on hasPhone instead of looking each one up. Sizing is free. Here is a count run through the LeadOcean MCP tool leadocean_count_leads on 2026-10-21:

json
{
  "country": ["US"],
  "jobLevel": ["VP"],
  "jobFunction": ["Sales & Business Development"],
  "hasPhone": true
}

Result: 275,259 people, 135,386 distinct employers (employer count is an estimate), 0 records spent. This count carries hasPhone and the tool's default email condition, so it is not a pure phone count. Pass emailStatus or reachable yourself to change that.

The same sizing works over REST. Send count=true as a query parameter and limit 1 in the body:

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "Content-Type: application/json" \
  -d '{"country":["US"],"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"hasPhone":true,"limit":1}'

meta.total is capped at 100,000 on this call, so use the MCP count for a larger number.

Step 4: Confirm the number belongs to the person

A valid mobile number can still belong to the wrong person. If you hold a number and a name, enrich by the number and compare. Enrich takes exactly one of linkedin_url, email, phone or person_id.

bash
curl -s -X POST https://api.leadocean.io/v1/people/enrich \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "Content-Type: application/json" \
  -d '{"phone":"+14155550133"}'

If the record that comes back has a different name or employer than your CRM row, quarantine the row. A 404 means nobody matched and costs nothing. You can also ask the reverse question with /v2/people/reverse.

Step 5: Test that the line is live

This is the check LeadOcean does not do. Format, type and ownership say a number is plausible. They do not say the SIM is active today. A live-line test (an HLR or carrier lookup) queries the network and returns connected, disconnected or ported. Buy that from a telecom lookup vendor. Run it last, on the short list that survived steps 1 to 4, because it costs money per number.

Also check consent. The phone endpoint publishes no do-not-call flag, because the source does not populate one. A missing flag is not clearance. Run your own do-not-call screen before any outbound call or text.

How the four checks fit together

Order them by cost. Each check removes numbers before the next one runs, so the expensive step sees the fewest.

CheckWhere it runsCostWhat it catches
1. E.164 formatYour codeFreeTypos, wrong length, impossible prefixes
2. Line typeYour code, or the type field from LeadOceanFree locally, 1 record per LeadOcean lookupSwitchboards and landlines in a mobile-only campaign
3. Person matchPOST /v1/people/enrich with phone1 record when found, 0 on a 404Right number, wrong person
4. Live lineA carrier lookup vendorPer numberDisconnected and recycled SIMs

A practical rule: run 1 and 2 on the whole list, 3 on anything a rep will actually dial, and 4 only before a large text or call blast. For a handful of high-value accounts, skip 4 and let the rep's first dial be the test.

One more habit helps. Store the check date next to each number. A line verified in March says little about October, and each LeadOcean record also carries its own fetched_at date, so you can see how fresh the source was. The dataset is refreshed monthly.

What you get

Response shape from the API reference, placeholder values, for GET /v2/people/phone:

json
{
  "success": true,
  "data": {
    "found": true,
    "phones": [
      { "phone": "+14155550133", "type": "mobile" },
      { "phone": "+14155550134", "type": "office" }
    ],
    "best": { "phone": "+14155550133", "type": "mobile" }
  }
}

Each number is phone and type, best first. The type is the source's own label and is not a closed list. Treat an unrecognised value as unknown, not as an error. The response has no meta; read GET /v1/usage for what you spent.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerSend the key in x-api-key, read from $LEADOCEAN_API_KEY.
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Back off for the Retry-After header, or move to a plan with a higher limit.
402The 1,000 Free records are spentUpgrade on the pricing page. Solo is $97 a month.
Empty resultsA bad enum value, such as jobLevel: "vp" instead of VPEnum values are exact strings. Check them with leadocean_list_enum_values.
found: falseWe hold no number for that personThis is not a failed lookup. Try another identifier, or move on.
Export refusedOver 50,000 rows a request or 500,000 a daySplit the export. Bulk CSV export is not on Free.

FAQ

What is the difference between phone number validation and verification?

Validation checks the number's shape: length, prefix and country plan. Verification checks the number against something real: its line type, its owner or the carrier network. Run validation first because it is free.

Does LeadOcean tell me if a number is disconnected?

No. It returns the numbers it holds with a type, and lets you look up the person behind a number. It does not query the carrier network. Use a carrier lookup for that.

How many records does verification cost?

Format checks in your own code cost nothing. Sizing with hasPhone is free. Each phone lookup or enrich by phone costs one record. An enrichment that finds nobody costs nothing.

Does LeadOcean screen numbers against do-not-call lists?

No. LeadOcean publishes no do-not-call flag on phone numbers, so treat every number as unscreened. Screen against the national and regional registers yourself.

Where do I find mobile numbers by country?

See best mobile phone numbers by region, mobile number finders and phone number finder tools. The blog lists the rest, and pricing covers what each plan allows.

Verify the numbers you dial. Start with 1,000 free records.

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →