# Look up a phone number

One call answers what a number is. Everything here needs an API key with the `lookup` scope.

## Run a base lookup

Send the number and nothing else. The base lookup is always billed once, and it always answers the country, the serving network, the issuing network, and a coarse line type.

<!-- bird:tabs typescript,python,go,php,cli,curl -->

<!-- bird:snippet lookup.phone_number -->

```typescript
const answer = await bird.lookup.phoneNumber({
  phone_number: "+31612345678",
  type: ["classification", "score"],
});
console.log(answer.country_code, answer.line_type);
// Only a block whose status is ok carries a value, and only that one is billed.
if (answer.score?.status === "ok") console.log(answer.score.value);
```

<!-- bird:snippet lookup.phone_number -->

```python
answer = client.lookup.phone_number(
    phone_number="+31612345678", type=["classification", "score"]
)
print(answer.country_code, answer.line_type)
# Only a block whose status is ok carries a value, and only that one is billed.
if answer.score is not None and answer.score.status == "ok":
    print(answer.score.value)
```

<!-- bird:snippet lookup.phone_number -->

```go
answer, err := client.Lookup.PhoneNumber(context.Background(), bird.LookupPhoneNumberParams{
	PhoneNumber: "+31612345678",
	Type:        []bird.LookupProperty{"classification", "score"},
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(*answer.CountryCode, *answer.LineType)
// Only a block whose status is ok carries a value, and only that one is billed.
if answer.Score != nil && *answer.Score.Status == "ok" {
	fmt.Println(*answer.Score.Value)
}
```

<!-- bird:snippet lookup.phone_number -->

```php
$answer = $bird->lookup->phoneNumber(
    (new PhoneNumberLookupRequest())
        ->setPhoneNumber('+31612345678')
        ->setType(['classification', 'score']),
);
echo $answer->getCountryCode(), ' ', $answer->getLineType();
// Only a block whose status is ok carries a value, and only that one is billed.
if ($answer->getScore()?->getStatus() === 'ok') {
    echo $answer->getScore()->getValue();
}
```

```bash
bird lookup phone-number \
  --phone-number +31612345678 \
  --type classification \
  --type presence
```

```bash
curl -X POST "https://us1.platform.bird.com/v1/lookup/phone-number" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+31612345678",
    "type": [
      "classification",
      "presence"
    ]
  }'
```

<!-- /bird:tabs -->

## How to write the number

Send the country calling code, then the national number. The leading `+` is optional and `00` works in its place, so `+31612345678`, `31612345678` and `0031612345678` are all the same number.

A number written for dialling inside one country, with no country code, is rejected rather than guessed at. `0612345678` returns [`E22000`](/docs/api/errors/E22000), because prefixing a country code would name a real number somewhere else and bill you for looking it up.

## What the base lookup answers

`country_code` is the number's country, absent when the number belongs to no single country, as a non-geographic range does.

`network_info` is the network serving the number today, and `original_network_info` is the network that issued its range. The two differ when the number has been ported, and in that case `flags` contains `ported`.

`line_type` is what kind of line the number is: `mobile`, `fixed_line`, `voip`, `toll_free`, `premium_rate`, `satellite`, `pager`, `payphone`, `m2m`, `service`, `other`, or `unknown`. `unknown` means the carrier platform holds no classification for the range; `other` means it holds one with no equivalent here. For the allocated service at finer precision, request the `classification` property. It answers from a different source with a wider vocabulary, and is reported separately so you can always tell the two apart.

## Add properties

Name the properties you want in `type`. Each is billed separately and only when delivered.

| Property         | What it answers                                                                     |
| ---------------- | ----------------------------------------------------------------------------------- |
| `classification` | The precise allocated service of the range: premium rate, satellite, M2M, payphone. |
| `porting`        | When the number last moved network, and every move on record.                       |
| `presence`       | Whether the number is currently live on the network.                                |
| `roaming`        | Whether it is roaming, and on which network.                                        |
| `sim_swap`       | When its SIM last changed.                                                          |
| `score`          | A credibility score from 0 to 100.                                                  |

`classification`, `porting` and `score` read stored data and return quickly. `presence`, `roaming` and `sim_swap` reach the live network, so they are slower and their coverage varies by operator. Expect `unavailable` or `inconclusive` for these more often than for the stored ones.

Two properties answer questions the base lookup already touches, at higher resolution. `porting` gives you the date and the full history where the base lookup's `ported` flag only says whether a move ever happened. `classification` resolves `line_type` to the exact allocated service.

## Read the status before the value

Every property block carries a `status`, and only `ok` carries a value.

```json
{
  "phone_number": "+441904123456",
  "country_code": "GB",
  "line_type": "service",
  "classification": {
    "status": "ok",
    "value": "premium_rate"
  },
  "score": {
    "status": "unavailable"
  }
}
```

In that answer you were billed for the base lookup and for `classification`. You were not billed for `score`.

Read `status` first and treat anything other than `ok` as "not answered". It is an open vocabulary, so a value you do not recognize is a future status rather than an error.

Two blocks report a bound rather than an exact figure when the network will not release one. `sim_swap` returns `min_days` and `max_days` instead of `last_swapped_at` when only a recency band is known. `porting` sets `last_ported_at_is_approximate` when a registry records the period of a move but not its day.

One field reads as a negative but is a positive finding: `porting.ported` set to `false` means the registry was consulted and holds no move for this number, not that nothing could be checked. That distinction is what `status` is for.

## Retry without paying twice

A lookup is billed, so a retried request must not buy a second answer. Send an `Idempotency-Key` and a repeat of the same request replays the stored answer instead of running a new lookup. See [Idempotency](/docs/guides/idempotency).

The `GET` form of this operation, which puts the number in the URL, cannot carry an idempotency key. Use the `POST` form for anything automated.

## Errors

| Code                                | What happened                                                                                |
| ----------------------------------- | -------------------------------------------------------------------------------------------- |
| [`E22000`](/docs/api/errors/E22000) | The number is not a valid phone number in international format. Nothing was charged.         |
| [`E22001`](/docs/api/errors/E22001) | The organization's wallet cannot cover the lookup. Top it up and retry. Nothing was charged. |
| [`E22002`](/docs/api/errors/E22002) | Lookup is temporarily unavailable. Retry with backoff. Nothing was charged.                  |

A failing property is not an error. It comes back as a status on its block, with the base lookup served alongside it.

## Next steps

- [Look up an email address](/docs/guides/lookup/email-addresses) is the other half of Lookup.
- [Lookup API reference](/docs/api/reference/create-phone-number-lookup) documents every field and every property block.
- [Rate limits](/docs/guides/rate-limits) covers the `lookup` bucket these calls draw on.