# Lookup

Source: https://jointextkit.com/docs/lookup
Index: https://jointextkit.com/llms.txt

A lookup tells you which carrier a US or Canada number is on and whether it
is a mobile, a landline or a VoIP line. Run one before you text a list to skip
landlines, or at signup to reject VoIP numbers.

## Look up a number

`GET /v1/lookup/{phone}`

```bash
curl https://api.jointextkit.com/v1/lookup/+14155550132 \
  -H "Authorization: Bearer ssms_sk_test_YOUR_KEY"
```

```json
{
  "phone_number": "+14155550132",
  "valid": true,
  "line_type": "mobile",
  "carrier": { "name": "Sandbox Wireless", "type": "mobile" },
  "caller_name": null
}
```

The number can be written any way you like: `+14155550132`, `4155550132`
and `(415) 555-0132` are the same lookup. It must have 10 digits, or 11
starting with 1. The response always carries the E.164 form. A restricted key
needs the `lookup` [scope](/docs/authentication#scopes).

## Response fields

| Field | Notes |
| --- | --- |
| `phone_number` | The number you asked about, normalized to E.164. |
| `valid` | `false` only when the carrier network reports that the number does not exist (unassigned or vacant). Everything else is `true`, including a number whose line type is unknown. |
| `line_type` | `mobile`, `landline`, `voip`, or `null`. `null` means we could not classify the carrier; it is never a guess. |
| `carrier.name` | The carrier the number is on today. It follows ports, so a number moved to another carrier shows the new one. `null` when the network returned no name. |
| `carrier.type` | The same value as `line_type`. |
| `caller_name` | Always `null` today. The field is reserved, so check for `null` before using it. |

A successful response also carries an `X-Quota-Remaining` header: the lookups
left in your account's day.

## With the SDKs

```js
import { TextKit } from 'jointextkit';
const sms = new TextKit(process.env.TEXTKIT_API_KEY);

const result = await sms.lookup.phone('+14155550132');
if (result.line_type === 'landline') {
  // skip: this number cannot receive texts
}
```

```python
from jointextkit import TextKit

client = TextKit()  # reads TEXTKIT_API_KEY

result = client.lookup.phone("+14155550132")
print(result["line_type"], result["carrier"]["name"])
```

## Test keys

A test key never contacts a carrier and is never billed. The answer is decided
by the number alone, so your tests get the same result every time:

| Number | `line_type` | `carrier.name` |
| --- | --- | --- |
| Last digit even | `mobile` | `Sandbox Wireless` |
| Last digit odd | `landline` | `Sandbox Telecom` |
| `+15005550010` | `landline` | `Sandbox Telecom` |

`+15005550010` is the [sandbox](/docs/sandbox) landline number, so it answers
`landline` although it ends in 0. `valid` is always `true` and
`caller_name` is always `null` with a test key.

## Live keys

A live key returns real carrier data. Each answer is cached for 24 hours, so a
second lookup of the same number inside a day returns the stored answer, and a
port can take up to 24 hours to show. A cached answer still counts as a lookup.

On an account with a payment method, each successful live lookup costs
$0.008. A lookup that ends in an error is not billed.

Sending a message never runs a lookup. The `destination_carrier` field on a
[message](/docs/messages) is filled from the same 24-hour cache, so it is
`null` unless you looked the number up recently.

## Limits

Lookups are counted per account, per UTC day. Test and live keys draw from the
same counter.

| Account | Lookups per day |
| --- | --- |
| With a payment method | 5,000 |
| New account, no payment method | 100 |
| No payment method, after live access is approved | 250 |

A lookup that fails with `carrier_error` still uses one of the day's lookups;
a request rejected with `invalid_request` does not. The usual limit of 60
requests a minute per key applies too.

## Errors

| Status | Code | When |
| --- | --- | --- |
| `400` | `invalid_request` | The number is not a valid US or Canada number. `param` is `phone`. |
| `401` | `invalid_api_key` | The key is missing, malformed or revoked. |
| `403` | `insufficient_scope` | A restricted key without the `lookup` scope. `required_scope` names it. |
| `403` | `forbidden` | A live key on an account with no lookup allowance. Add a payment method in the console. |
| `429` | `quota_exceeded` | The day's lookups are used up. The message states the limit. |
| `429` | `rate_limited` | Too many requests in a minute. Wait for the `Retry-After` seconds. |
| `502` | `carrier_error` | The carrier network did not answer. Try again shortly. |

```json
{
  "error": {
    "code": "quota_exceeded",
    "message": "Daily lookup quota reached (5000/day).",
    "request_id": "req_a1B2c3D4e5F6g7H8i9J0"
  }
}
```

Every code is explained in [Errors](/docs/errors#codes).

## Retired: spam scores

`GET /v1/lookup/{phone}/spam` was retired on 2026-09-30 and answers
`410 endpoint_retired`. The Node SDK still has `sms.lookup.spam()`, which
now always throws that error.
