---
title: Phone Validator API reference
description: Validate phone numbers and identify their line type, carrier and location.
url: https://www.phonevalidator.com/docs/api
---

# Phone Validator API

> Validate phone numbers and identify their line type, carrier and location.

Version 5.0.0. Base URL: `https://api.phonevalidator.com`. Specification: https://www.phonevalidator.com/openapi.yaml

Look up a phone number with `POST /v5/lookups`. The response states whether
the number is valid and gives its country, region, line type, carrier and
port status.

## Authentication

Send your API key as a bearer token:

```
Authorization: Bearer <api key>
```

Live keys start with `pv_live_` and test keys with `pv_test_` (see
[Test mode](#test-mode)).

## Errors

Errors return a non-2xx status and a body of the form
`{"error": {"code": "...", "message": "..."}}`. Match on `code`; `message`
is for people. Every response has a `Request-Id` header for support.

## Rate limits

Each account may make 25 requests per second across all of its keys.
Higher limits are available on request. `RateLimit-Limit`,
`RateLimit-Remaining` and `RateLimit-Reset` report the current window; a
request over the limit receives `429 rate_limited` with `Retry-After`.

## Versioning

v5 changes only by addition. New fields and new `line_type` and
`invalid_reason` values may appear: ignore unknown fields, treat an unknown
`line_type` as `unknown` and an unknown `invalid_reason` as invalid.
Existing fields keep their name, type and meaning.

## Look up a phone number

`POST https://api.phonevalidator.com/v5/lookups`

Validates a phone number and returns its details. An invalid number
returns `200` with `valid: false` and an `invalid_reason`.

Spaces, `()`, `-` and `.` are ignored, and a leading `+` or `00` marks
an international number. Other numbers are read in `default_country`,
else in the first of `allowed_countries`, else as a North American
number and then as an international one.

With an `Idempotency-Key`, the first successful response is stored for
24 hours for your API key. The same key and body return the stored
response with `Idempotent-Replayed: true`, without a second charge. The
same key returns `409 idempotency_in_progress` while the first request
is running and `422 idempotency_key_reused` with a different body.
Errors are not stored, so a failed request can be retried with the same
key.

### Request headers

| Header | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | yes | `Bearer <api key>`. Your API key. |
| `Content-Type` | `string` | yes | `application/json` |
| `Idempotency-Key` | `string` | no | A unique value per lookup, such as a UUID. Maximum 255 characters. Minimum 1 character. Example `4c6f8e0a-1b7d-4f5e-9a3c-2d8e7b6a5f10`. |

### Request body

Content type: `application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone_number` | `string` | yes | The phone number, in any common format. Maximum 64 characters. |
| `default_country` | `string` | no | The country used to read a number written without `+` or `00`, as an ISO 3166-1 alpha-2 code such as `GB`. Numbers with `+` are read as written. Must be in `allowed_countries` when both are sent. Pattern `^[A-Z]{2}$`. |
| `allowed_countries` | `string[]` | no | The countries to accept, as ISO codes such as `["US","CA"]`. A number from any other country returns `valid: false` with `invalid_reason: "country_not_allowed"`. Without `default_country`, the first country is used to read numbers written without `+`. At least 1 item. |

Examples:

**A number in a common format**

```json
{
  "phone_number": "(702) 927-0000"
}
```

**A number read in a country**

```json
{
  "phone_number": "020 7946 0000",
  "default_country": "GB"
}
```

**Only accept numbers from some countries**

```json
{
  "phone_number": "+12025550100",
  "allowed_countries": [
    "US",
    "CA"
  ]
}
```

**A test number, with a test key**

```json
{
  "phone_number": "+12025550101"
}
```

### Code samples

#### cURL

```bash
curl https://api.phonevalidator.com/v5/lookups \
  -H "Authorization: Bearer $PHONEVALIDATOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone_number":"(702) 927-0000"}'
```

#### Python

```python
import os
import requests

response = requests.post(
    "https://api.phonevalidator.com/v5/lookups",
    headers={"Authorization": f"Bearer {os.environ['PHONEVALIDATOR_API_KEY']}"},
    json={"phone_number": "(702) 927-0000"},
    timeout=30,
)
print(response.status_code, response.json())
```

#### Node

```javascript
const response = await fetch("https://api.phonevalidator.com/v5/lookups", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PHONEVALIDATOR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"phone_number":"(702) 927-0000"}),
});
console.log(response.status, await response.json());
```

#### PHP

```php
<?php
$curl = curl_init("https://api.phonevalidator.com/v5/lookups");
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PHONEVALIDATOR_API_KEY"),
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode(['phone_number' => '(702) 927-0000']),
]);
$response = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
echo $status, " ", $response, "\n";
```

#### Ruby

```ruby
require "json"
require "net/http"

uri = URI("https://api.phonevalidator.com/v5/lookups")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("PHONEVALIDATOR_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = JSON.generate({ "phone_number" => "(702) 927-0000" })

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

#### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	req, err := http.NewRequest("POST", "https://api.phonevalidator.com/v5/lookups", strings.NewReader(`{"phone_number":"(702) 927-0000"}`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHONEVALIDATOR_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(data))
}
```

#### C#

```csharp
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("PHONEVALIDATOR_API_KEY"));

var content = new StringContent("""{"phone_number":"(702) 927-0000"}""", Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.phonevalidator.com/v5/lookups", content);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Responses

#### 200 OK

The lookup result.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `Request-Id` | `string` | Identifies this request. Quote it to support. |
| `RateLimit-Limit` | `integer` | Requests allowed per second. |
| `RateLimit-Remaining` | `integer` | Requests left in the current second. |
| `RateLimit-Reset` | `integer` | Seconds until the limit refills. |
| `Cache-Control` | `string` | Always `no-store`. |
| `Idempotent-Replayed` | `string` | `true` when the response is the stored response for this `Idempotency-Key`. Absent otherwise. |

Fields:

| Field | Type | Description |
| --- | --- | --- |
| `phone_number` | `string \| null` | The number in E.164 format. `null` when the input cannot be read as a phone number. Pattern `^\+[1-9]\d{1,14}$`. |
| `valid` | `boolean` | `true` when the number is in an assigned, in-service range of its numbering plan. |
| `invalid_reason` | `string \| null` | Why the number is invalid; `null` when it is valid. New values may be added; treat an unknown value as invalid. One of `not_a_phone_number`, `too_short`, `too_long`, `invalid_area_code`, `invalid_prefix`, `not_in_service`, `invalid_number`, `country_not_allowed`. |
| `format_national` | `string \| null` | National display format. `null` for an invalid number. |
| `format_international` | `string \| null` | International display format. `null` for an invalid number. |
| `country` | `string \| null` | ISO 3166-1 alpha-2 code. US territories have their own codes (`PR`, `GU`, `VI`, `AS`, `MP`). Pattern `^[A-Z]{2}$`. |
| `region` | `string \| null` | US state or Canadian province code, such as `NV` or `ON`. |
| `city` | `string \| null` | The locality where the number was issued. |
| `time_zone` | `string \| null` | IANA time zone where the number was issued. `null` when the area spans more than one zone. |
| `line_type` | `string \| null` | `mobile`, `landline`, `voip`, `toll_free` or `unknown`; `null` for an invalid number. New values may be added; treat an unknown value as `unknown`. One of `mobile`, `landline`, `voip`, `toll_free`, `unknown`. |
| `carrier` | `string \| null` | The number's current carrier, or the carrier its number block is assigned to. For a number on a wholesale VoIP network, the company behind it, such as `GOOGLE VOICE`. |
| `ported` | `boolean \| null` | Whether the number has been ported to another carrier. `null` where port data is not available, including Canada. |
| `cost` | `string` | Amount billed for this request, in USD. Pattern `^\d+\.\d{4}$`. |
| `balance` | `string` | Available balance after this request, in USD. `GET /v5/account` returns the exact balance while other lookups are in progress. Pattern `^\d+\.\d{4}$`. |
| `test_mode` | `boolean` | `true` for a request made with a test key. |

**`invalid_reason`**

| Value | Meaning |
| --- | --- |
| `not_a_phone_number` | Empty, only punctuation, letters, an extension, or other characters |
| `too_short` | Fewer digits than the country's numbers have |
| `too_long` | More digits than the country's numbers have |
| `invalid_area_code` | Area code not in service, or not for customer numbers |
| `invalid_prefix` | Exchange not valid or not assigned |
| `not_in_service` | Thousands block or exchange not in service |
| `invalid_number` | International number that is not valid or has no record |
| `country_not_allowed` | Outside the request's `allowed_countries` |

**`line_type`**

- `voip`: served by a VoIP or wholesale carrier, including app
  numbers such as Google Voice.
- `unknown`: the line could not be classified, as with some
  premium-rate and pager numbers.

Examples:

**A valid mobile number**

```json
{
  "phone_number": "+17029270000",
  "valid": true,
  "invalid_reason": null,
  "format_national": "(702) 927-0000",
  "format_international": "+1 702-927-0000",
  "country": "US",
  "region": "NV",
  "city": "LAS VEGAS",
  "time_zone": "America/Los_Angeles",
  "line_type": "mobile",
  "carrier": "T-MOBILE",
  "ported": true,
  "cost": "0.0040",
  "balance": "41.2760",
  "test_mode": false
}
```

**An invalid number**

```json
{
  "phone_number": "+11115550100",
  "valid": false,
  "invalid_reason": "invalid_area_code",
  "format_national": null,
  "format_international": null,
  "country": "US",
  "region": null,
  "city": null,
  "time_zone": null,
  "line_type": null,
  "carrier": null,
  "ported": null,
  "cost": "0.0040",
  "balance": "41.2720",
  "test_mode": false
}
```

**A test-mode response**

```json
{
  "phone_number": "+12025550101",
  "valid": true,
  "invalid_reason": null,
  "format_national": "(202) 555-0101",
  "format_international": "+1 202-555-0101",
  "country": "US",
  "region": "DC",
  "city": "WASHINGTON",
  "time_zone": "America/New_York",
  "line_type": "mobile",
  "carrier": "T-MOBILE",
  "ported": true,
  "cost": "0.0000",
  "balance": "41.2760",
  "test_mode": true
}
```

#### 400 Bad Request

The request is malformed.

- `invalid_request`: a field is missing, unknown or out of range, such
  as `phone_number` over 64 characters, a country that is not an ISO
  3166-1 alpha-2 code, an empty `allowed_countries`, a
  `default_country` outside `allowed_countries`, or an
  `Idempotency-Key` that is blank or over 255 characters.
- `invalid_json`: the body is not a JSON object.
- `test_mode_number`: a test key sent a number that is not a test
  number.

Examples:

**A field is missing or wrong**

```json
{
  "error": {
    "code": "invalid_request",
    "message": "phone_number is required and must be a string."
  }
}
```

**The body is not JSON**

```json
{
  "error": {
    "code": "invalid_json",
    "message": "The request body is not valid JSON."
  }
}
```

**A test key sent a number outside the test set**

```json
{
  "error": {
    "code": "test_mode_number",
    "message": "Test keys accept only the documented test numbers."
  }
}
```

#### 401 Unauthorized

The API key is missing, invalid or revoked, or was sent in the URL.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `WWW-Authenticate` | `string` | The authentication scheme, `Bearer`. Sent when the key is missing or not valid. |

Examples:

**No bearer token**

```json
{
  "error": {
    "code": "missing_key",
    "message": "Send your API key as Authorization: Bearer <key>."
  }
}
```

**Unknown or revoked key**

```json
{
  "error": {
    "code": "invalid_key",
    "message": "This API key is not valid or was revoked."
  }
}
```

**The key was in the query string**

```json
{
  "error": {
    "code": "key_in_url",
    "message": "Send the API key in the Authorization header."
  }
}
```

#### 402 Payment Required

The balance does not cover the lookup. Not billed.

Example:

```json
{
  "error": {
    "code": "insufficient_funds",
    "message": "This lookup costs $0.0040; $0.0012 is available."
  }
}
```

#### 403 Forbidden

The account cannot make requests: `account_inactive`, `api_disabled`
(API access is off for the account) or `account_on_hold` (contact
support).

Examples:

```json
{
  "error": {
    "code": "account_inactive",
    "message": "This account is not active."
  }
}
```

```json
{
  "error": {
    "code": "api_disabled",
    "message": "API access is turned off for this account."
  }
}
```

```json
{
  "error": {
    "code": "account_on_hold",
    "message": "This account is on hold. Contact support."
  }
}
```

#### 405 Method Not Allowed

Method not allowed. Lookups use `POST`.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `Allow` | `string` | The method this endpoint accepts. |

Example:

```json
{
  "error": {
    "code": "method_not_allowed",
    "message": "Use POST for this endpoint."
  }
}
```

#### 409 Conflict

A request with this `Idempotency-Key` is in progress. Retry with the
same key after `Retry-After`.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `Retry-After` | `integer` | Seconds to wait before retrying. |

Example:

```json
{
  "error": {
    "code": "idempotency_in_progress",
    "message": "A request with this Idempotency-Key is still running."
  }
}
```

#### 415 Unsupported Media Type

The body must be JSON, sent with `Content-Type: application/json`.

Example:

```json
{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send a JSON body with Content-Type: application/json."
  }
}
```

#### 422 Unprocessable Content

This `Idempotency-Key` was used with a different request body.

Example:

```json
{
  "error": {
    "code": "idempotency_key_reused",
    "message": "This Idempotency-Key was used with a different request."
  }
}
```

#### 429 Too Many Requests

The account's rate limit is exceeded. Not billed; retry after
`Retry-After`.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `Retry-After` | `integer` | Seconds to wait before retrying. |

Example:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after the Retry-After seconds."
  }
}
```

#### 500 Internal Server Error

An internal error. Retry with the same `Idempotency-Key`; a request that
was billed before the error is not billed again.

Example:

```json
{
  "error": {
    "code": "internal_error",
    "message": "Internal error. Retry with the same Idempotency-Key."
  }
}
```

#### 503 Service Unavailable

Carrier data is temporarily unavailable. Not billed; retry after
`Retry-After`.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `Retry-After` | `integer` | Seconds to wait before retrying. |

Example:

```json
{
  "error": {
    "code": "lookup_unavailable",
    "message": "The carrier lookup is unavailable; retry shortly."
  }
}
```

## Read the account

`GET https://api.phonevalidator.com/v5/account`

Returns the account's balance, price, rate limit, auto-refill and
balance-alert settings, and usage for the current month. Settings are
managed in the dashboard. Not billed; counts toward the rate limit.

### Request headers

| Header | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | yes | `Bearer <api key>`. Your API key. |

### Code samples

#### cURL

```bash
curl https://api.phonevalidator.com/v5/account \
  -H "Authorization: Bearer $PHONEVALIDATOR_API_KEY"
```

#### Python

```python
import os
import requests

response = requests.get(
    "https://api.phonevalidator.com/v5/account",
    headers={"Authorization": f"Bearer {os.environ['PHONEVALIDATOR_API_KEY']}"},
    timeout=30,
)
print(response.status_code, response.json())
```

#### Node

```javascript
const response = await fetch("https://api.phonevalidator.com/v5/account", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PHONEVALIDATOR_API_KEY}`,
  },
});
console.log(response.status, await response.json());
```

#### PHP

```php
<?php
$curl = curl_init("https://api.phonevalidator.com/v5/account");
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PHONEVALIDATOR_API_KEY"),
    ],
]);
$response = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
echo $status, " ", $response, "\n";
```

#### Ruby

```ruby
require "json"
require "net/http"

uri = URI("https://api.phonevalidator.com/v5/account")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("PHONEVALIDATOR_API_KEY")}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

#### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://api.phonevalidator.com/v5/account", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHONEVALIDATOR_API_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(data))
}
```

#### C#

```csharp
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("PHONEVALIDATOR_API_KEY"));

var response = await client.GetAsync("https://api.phonevalidator.com/v5/account");
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Responses

#### 200 OK

The account.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `Request-Id` | `string` | Identifies this request. Quote it to support. |
| `RateLimit-Limit` | `integer` | Requests allowed per second. |
| `RateLimit-Remaining` | `integer` | Requests left in the current second. |
| `RateLimit-Reset` | `integer` | Seconds until the limit refills. |
| `Cache-Control` | `string` | Always `no-store`. |

Fields:

| Field | Type | Description |
| --- | --- | --- |
| `balance` | `string` | Available balance in USD, including lookups in progress. Pattern `^\d+\.\d{4}$`. |
| `price_per_lookup` | `string` | Price of one lookup for this account, in USD. Pattern `^\d+\.\d{4}$`. |
| `trial_lookups_remaining` | `integer` | Free lookups remaining. Minimum 0. |
| `rate_limit_per_second` | `integer` | Requests per second allowed for the account. Minimum 1. |
| `auto_refill_enabled` | `boolean` | Whether auto-refill is on. |
| `auto_refill_threshold` | `string` | Balance that triggers auto-refill, in USD. Pattern `^\d+\.\d{4}$`. |
| `auto_refill_amount` | `string` | Amount added by auto-refill, in USD. Pattern `^\d+\.\d{4}$`. |
| `balance_alert_enabled` | `boolean` | Whether the low-balance email is on. |
| `balance_alert_threshold` | `string` | Balance that triggers the low-balance email, in USD. Pattern `^\d+\.\d{4}$`. |
| `lookups_this_month` | `integer` | Billed lookups in the current UTC calendar month. Minimum 0. |
| `spent_this_month` | `string` | Amount billed in the current UTC calendar month, in USD. Pattern `^\d+\.\d{4}$`. |
| `test_mode` | `boolean` | `true` for a request made with a test key. |

Example:

**A live account with auto-refill**

```json
{
  "balance": "41.2760",
  "price_per_lookup": "0.0040",
  "trial_lookups_remaining": 0,
  "rate_limit_per_second": 25,
  "auto_refill_enabled": true,
  "auto_refill_threshold": "5.0000",
  "auto_refill_amount": "25.0000",
  "balance_alert_enabled": false,
  "balance_alert_threshold": "5.0000",
  "lookups_this_month": 18234,
  "spent_this_month": "72.9360",
  "test_mode": false
}
```

#### 401 Unauthorized

The API key is missing, invalid or revoked, or was sent in the URL.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `WWW-Authenticate` | `string` | The authentication scheme, `Bearer`. Sent when the key is missing or not valid. |

Examples:

**No bearer token**

```json
{
  "error": {
    "code": "missing_key",
    "message": "Send your API key as Authorization: Bearer <key>."
  }
}
```

**Unknown or revoked key**

```json
{
  "error": {
    "code": "invalid_key",
    "message": "This API key is not valid or was revoked."
  }
}
```

**The key was in the query string**

```json
{
  "error": {
    "code": "key_in_url",
    "message": "Send the API key in the Authorization header."
  }
}
```

#### 403 Forbidden

The account cannot make requests: `account_inactive`, `api_disabled`
(API access is off for the account) or `account_on_hold` (contact
support).

Examples:

```json
{
  "error": {
    "code": "account_inactive",
    "message": "This account is not active."
  }
}
```

```json
{
  "error": {
    "code": "api_disabled",
    "message": "API access is turned off for this account."
  }
}
```

```json
{
  "error": {
    "code": "account_on_hold",
    "message": "This account is on hold. Contact support."
  }
}
```

#### 405 Method Not Allowed

Method not allowed. The account uses `GET`.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `Allow` | `string` | The method this endpoint accepts. |

Example:

```json
{
  "error": {
    "code": "method_not_allowed",
    "message": "Use GET for this endpoint."
  }
}
```

#### 429 Too Many Requests

The account's rate limit is exceeded. Not billed; retry after
`Retry-After`.

Headers:

| Header | Type | Description |
| --- | --- | --- |
| `Retry-After` | `integer` | Seconds to wait before retrying. |

Example:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after the Retry-After seconds."
  }
}
```

#### 500 Internal Server Error

An internal error. Retry with the same `Idempotency-Key`; a request that
was billed before the error is not billed again.

Example:

```json
{
  "error": {
    "code": "internal_error",
    "message": "Internal error. Retry with the same Idempotency-Key."
  }
}
```

## Test mode

Test keys (`pv_test_...`) return fixed responses for the numbers below
and are never billed. Any other number returns `400 test_mode_number`.
Test responses set `test_mode: true` and count toward the account's rate
limit.

The numbers are in the reserved 555-01XX range:

| Number | Result |
| --- | --- |
| `+12025550100` | Valid, `mobile`, `T-MOBILE`, `ported: false` |
| `+12025550101` | Valid, `mobile`, `T-MOBILE`, `ported: true` |
| `+12025550102` | Valid, `landline`, `VERIZON` |
| `+12025550103` | Valid, `voip`, `BANDWIDTH.COM` |
| `+18005550100` | Valid, `toll_free` |
| `+12025550104` | Valid, `unknown` line type |
| `+1202555010` | Invalid, `too_short` |
| `+11115550100` | Invalid, `invalid_area_code` |
| `+12025550106` | Invalid, `not_in_service` |
| `not a number` | Invalid, `not_a_phone_number` |
| `+12025550190` | `402 insufficient_funds` |
| `+12025550191` | `403 account_on_hold` |
| `+12025550192` | `429 rate_limited` |
| `+12025550193` | `503 lookup_unavailable` |

Valid test numbers report `city: "WASHINGTON"` and `region: "DC"` (`null`
for the toll-free number).

## Error codes

Each error `code` with its HTTP status.

| Code | HTTP status | Example message |
| --- | --- | --- |
| `invalid_json` | 400 | The request body is not valid JSON. |
| `invalid_request` | 400 | phone_number is required and must be a string. |
| `test_mode_number` | 400 | Test keys accept only the documented test numbers. |
| `invalid_key` | 401 | This API key is not valid or was revoked. |
| `key_in_url` | 401 | Send the API key in the Authorization header. |
| `missing_key` | 401 | Send your API key as Authorization: Bearer <key>. |
| `insufficient_funds` | 402 | This lookup costs $0.0040; $0.0012 is available. |
| `account_inactive` | 403 | This account is not active. |
| `account_on_hold` | 403 | This account is on hold. Contact support. |
| `api_disabled` | 403 | API access is turned off for this account. |
| `not_found` | 404 | No such endpoint. |
| `method_not_allowed` | 405 | Use POST for this endpoint. |
| `idempotency_in_progress` | 409 | A request with this Idempotency-Key is still running. |
| `unsupported_media_type` | 415 | Send a JSON body with Content-Type: application/json. |
| `idempotency_key_reused` | 422 | This Idempotency-Key was used with a different request. |
| `rate_limited` | 429 | Too many requests. Retry after the Retry-After seconds. |
| `internal_error` | 500 | Internal error. Retry with the same Idempotency-Key. |
| `lookup_unavailable` | 503 | The carrier lookup is unavailable; retry shortly. |
