> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tracklysms.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Validate Phone Number

> Validate a phone number for list cleaning — line type, current carrier, portability, and DNC status.

Validate a single phone number against Trackly's list cleaning pipeline. Returns current (post-port) carrier, line type (mobile / fixed line / VoIP), ported flag, and DNC membership — everything you need to decide whether the number is safe to include in an SMS campaign.

Backed by carrier network (NPAC-sourced) data, not a static numbering-plan table. Results reflect the current state of the number, including recent ports.

## Pricing & Billing

Each non-cached lookup is billed at **\$0.003**. Once a phone has been validated for your account, repeat lookups of the same number are free **indefinitely** (`cached: true`, `cost: 0`) — pass `force=true` to bypass the cache and re-validate at \$0.003 (rare; LRN data only changes on carrier porting). Lookups accrue throughout the month and are charged to your Stripe-on-file payment method on the 1st of the following month.

Your account must have an active payment method to use this endpoint. Requests from accounts with `payment_failed` or `suspended` billing status return `402 Payment Required`.

## Request

<ParamField body="phone" type="string" required>
  Phone number to validate. E.164 format recommended (e.g., `+14155551234`). Common US formats are normalized automatically (`4155551234`, `(415) 555-1234`).
</ParamField>

<ParamField body="force" type="boolean" default="false">
  Bypass the per-account cache and run a fresh paid lookup. Defaults to `false`. Must be a strict boolean — string `"true"` is rejected with `invalid_force`. LRN data only changes on carrier porting (rare), so cache hits are correct for the vast majority of repeat lookups.
</ParamField>

## Response

<ResponseField name="phone" type="string">
  Normalized E.164 phone number.
</ResponseField>

<ResponseField name="valid" type="boolean">
  `true` if the phone number is valid per the carrier network.
</ResponseField>

<ResponseField name="line_type" type="string">
  One of: `mobile`, `fixed line`, `voip`, `other`, `unknown`. Note the literal value for landlines is `"fixed line"` with a space.
</ResponseField>

<ResponseField name="carrier" type="string">
  Normalized, customer-friendly carrier display name (e.g., `Verizon Wireless`, `T-Mobile`, `Frontier`). Derived from `carrier_raw`.
</ResponseField>

<ResponseField name="carrier_raw" type="string">
  Raw carrier string from the underlying SPID (Service Provider ID) registry (e.g., `CELLCO PARTNERSHIP DBA VERIZON`). Useful for programmatic matching.
</ResponseField>

<ResponseField name="ported" type="boolean">
  `true` if the number has been ported away from the carrier that originally owned its NPA-NXX prefix.
</ResponseField>

<ResponseField name="country" type="string">
  ISO-3166 alpha-2 country code (e.g., `US`).
</ResponseField>

<ResponseField name="state" type="string">
  Region / state associated with the number (US only).
</ResponseField>

<ResponseField name="city" type="string">
  City associated with the number (US only; may be abbreviated).
</ResponseField>

<ResponseField name="on_dnc" type="boolean">
  `true` if the number is on Trackly's DNC list (populated nightly from the FTC Do Not Call Registry and state registries).
</ResponseField>

<ResponseField name="disposition" type="string">
  Summary verdict combining line type and DNC status:

  * `ok` — mobile, valid, not on DNC. Safe to send.
  * `unreachable` — landline or VoIP. Cannot receive SMS reliably.
  * `risky` — valid mobile but flagged on a DNC list.
  * `invalid` — not a valid number per the carrier network.
  * `unknown` — line type could not be determined.
</ResponseField>

<ResponseField name="cost" type="number">
  USD billed for this lookup. `0` when served from cache.
</ResponseField>

<ResponseField name="cached" type="boolean">
  `true` if the result was served from Trackly's per-account cache (no charge). Cache is permanent per (account, phone) once the number has been validated; pass `force=true` on the request to bypass.
</ResponseField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.tracklysms.com/api/v1/phone/validate \
    -H "X-Api-Key: your_api_key" \
    -H "Content-Type: application/json" \
    -d '{"phone": "+14155551234"}'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.tracklysms.com/api/v1/phone/validate",
      headers={
          "X-Api-Key": "your_api_key",
          "Content-Type": "application/json",
      },
      json={"phone": "+14155551234"},
  )

  result = response.json()
  if result["disposition"] != "ok":
      print(f"Skip: {result['disposition']} ({result['line_type']})")
  ```

  ```javascript Node.js theme={null}
  const axios = require('axios');

  const { data } = await axios.post(
    'https://api.tracklysms.com/api/v1/phone/validate',
    { phone: '+14155551234' },
    {
      headers: {
        'X-Api-Key': 'your_api_key',
        'Content-Type': 'application/json',
      },
    }
  );

  if (data.disposition !== 'ok') {
    console.log(`Skip: ${data.disposition} (${data.line_type})`);
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 200 OK — Mobile, clean theme={null}
  {
    "phone": "+14155551234",
    "valid": true,
    "line_type": "mobile",
    "carrier": "Verizon Wireless",
    "carrier_raw": "CELLCO PARTNERSHIP DBA VERIZON",
    "ported": false,
    "country": "US",
    "state": "California",
    "city": "San Francisco",
    "on_dnc": false,
    "disposition": "ok",
    "cost": 0.003,
    "cached": false
  }
  ```

  ```json 200 OK — Landline (unreachable) theme={null}
  {
    "phone": "+13103059808",
    "valid": true,
    "line_type": "fixed line",
    "carrier": "Frontier",
    "carrier_raw": "FRONTIER CALIFORNIA, INC.",
    "ported": true,
    "country": "US",
    "state": "California",
    "city": "Santa Monica",
    "on_dnc": false,
    "disposition": "unreachable",
    "cost": 0.003,
    "cached": false
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "validation_error",
    "code": "invalid_phone",
    "message": "invalid phone number format"
  }
  ```

  ```json 402 Payment Required theme={null}
  {
    "error": "billing_not_enabled",
    "code": "no_payment_method",
    "message": "An active payment method is required to use phone validation."
  }
  ```

  ```json 503 Service Unavailable theme={null}
  {
    "error": "provider_unavailable",
    "code": "provider_error",
    "message": "Phone validation service is temporarily unavailable."
  }
  ```
</ResponseExample>

## Error Codes

| Code                  | HTTP | Description                                                                |
| --------------------- | ---- | -------------------------------------------------------------------------- |
| `missing_phone`       | 400  | `phone` field missing from request body                                    |
| `invalid_phone`       | 400  | Phone could not be parsed into E.164                                       |
| `invalid_force`       | 400  | `force` is not a strict boolean (string `"true"` / int `1` are rejected)   |
| `invalid_credentials` | 401  | Missing or invalid `X-Api-Key`                                             |
| `account_suspended`   | 403  | Your account is suspended. Resolve outstanding billing or contact support. |
| `rate_limited`        | 429  | Account or upstream provider hit a rate limit; retry after a short backoff |
| `no_billing_config`   | 402  | Account has no billing configuration                                       |
| `no_payment_method`   | 402  | No Stripe customer / card on file                                          |
| `payment_failed`      | 402  | Most recent charge failed; update card to re-enable                        |
| `suspended`           | 402  | Account is suspended                                                       |
| `provider_error`      | 503  | Upstream lookup provider unavailable                                       |

## Billing Status

<Info>
  Check your billing status at any time under **Settings → Billing**. A dashboard banner will alert you if a recent charge failed, with a one-click link to update your payment method.
</Info>

## Caching Behavior

Once a phone has been validated for your account, the result is cached **permanently** for that `(account, phone)` pair. Repeat validations return the same data with no charge (`cached: true`, `cost: 0`). Pass `force=true` to bypass the cache and refresh the stored result with a new LRN call (the existing record is overwritten, not deleted). Reasoning: LRN data only changes when a number is ported between carriers — a rare event — so persistent caching gives correct results for the vast majority of repeat lookups.

A short-lived cache (30 days) sits in front for read latency, but the underlying per-account record is what makes repeat lookups free indefinitely.

<Tip>
  For bulk list cleaning, the cache makes re-validation across campaigns effectively free after the first pass. A 100K-contact list cleaned today and re-cleaned a year later will cost approximately \$0 on the second pass — assuming the same account and unchanged numbers.
</Tip>

## What's Detected vs Not

**Detected at \$0.003:**

* Line type (mobile / landline / VoIP)
* Current post-port carrier name
* Number validity (is this number assigned?)
* Portability status
* DNC membership (FTC + state registries)

**Not detected (not included in this endpoint):**

* Disconnected / inactive numbers — requires a live HLR query, not included
* Caller name (CNAM) — separate product
* Reassigned number status (FCC RND) — surfaced separately in the Compliance Engine
* Litigator risk — surfaced separately via Blacklist Alliance in the Compliance Engine
