# Phone Numbers API

Search, buy or port in real phone numbers through the API, then use each one for calls, SMS and WhatsApp.

import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

The Phone Numbers API gets you real phone numbers in many countries. Search live stock, buy a number in one request or port in your own, then turn on calls, SMS and WhatsApp.

## What you can build

- **A number per customer in your SaaS.** Search stock with `GET /v1/phone-numbers/available` and buy with `POST /v1/phone-numbers/purchase`.
- **A local presence in a new market.** Check which countries and number types are offered with `GET /v1/phone-numbers/countries`, with their prices.
- **A move from another carrier.** Check a number with `POST /v1/phone-numbers/port-in/check`, then port it with `POST /v1/phone-numbers/port-in`.
- **A business line for an AI agent.** Buy a number and route its calls to your agent with `POST /v1/phone-numbers/{id}/voice`.
- **Number management in your dashboard.** List every number and its features with `GET /v1/phone-numbers`, and release one with `DELETE /v1/phone-numbers/{id}`.

## Platforms

<ProductPlatforms product="phone-numbers" />

A number is the unit of telephony on Zernio, and each tile above is a channel it can carry. You turn each one on per number:

| Channel | How to turn it on | Notes |
|---|---|---|
| <PlatformName slug="voice" /> | On from the start. Set where inbound calls go with `POST /v1/phone-numbers/{id}/voice` | Phone, SIP or AI agent destinations |
| <PlatformName slug="sms" /> | Buy with `wantsSms: true`, then `POST /v1/phone-numbers/{id}/sms` | US numbers also need a carrier registration to send |
| <PlatformName slug="whatsapp" page="connect" /> | Buy with `connectWhatsapp` left on, or connect the number later | Toll-free numbers cannot connect WhatsApp |

On US numbers, [Branded Calling](/platforms/phone-numbers/branded-calling) also shows your business name and logo on the callee's phone.

## How it works

There are three ways to get a number:

| Way | Call | When |
|---|---|---|
| Buy from inventory | `POST /v1/phone-numbers/purchase` | Most countries: the number is active in the same request |
| Fill a KYC form first | `GET /v1/phone-numbers/kyc` | Regulated countries, where the purchase answers `202` with `kyc_required` |
| Port in | `POST /v1/phone-numbers/port-in` | You already own the number at another carrier |

A number belongs to a profile. Its `id` from the purchase or `GET /v1/phone-numbers` is the `{id}` on every feature call.

Each number bills per month at the price stamped when you bought it. New accounts pay from the [prepaid balance](/pricing/balance); older accounts pay on the monthly invoice. Prices by country are on [phone pricing](/pricing/phone).

## Your first request

You need an API key, a profile id and a payment method on file. This buys a US number that can send SMS, without WhatsApp.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data } = await zernio.phonenumbers.purchasePhoneNumber({
  body: {
    profileId: '66a1f0c2a4b9d3e8f1a2b3c4',
    country: 'US',
    wantsSms: true,
    connectWhatsapp: false,
    purchaseIntentId: 'order-1042-number',
  },
});

console.log(data.phoneNumber.id, data.phoneNumber.phoneNumber);
```
</Tab>
<Tab value="Python">
```python
from zernio import Zernio

client = Zernio()

purchase = client.phone_numbers.purchase_phone_number(
    profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
    country="US",
    wants_sms=True,
    connect_whatsapp=False,
    purchase_intent_id="order-1042-number",
)

print(purchase["phoneNumber"]["id"], purchase["phoneNumber"]["phoneNumber"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/phone-numbers/purchase" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "66a1f0c2a4b9d3e8f1a2b3c4",
    "country": "US",
    "wantsSms": true,
    "connectWhatsapp": false,
    "purchaseIntentId": "order-1042-number"
  }'
```
</Tab>
</Tabs>

Response (`200`):

```json
{
  "message": "Phone number provisioned",
  "phoneNumber": {
    "id": "66d4e5f6a7b8c9d0e1f2a3b4",
    "phoneNumber": "+14155550100",
    "status": "active",
    "country": "US",
    "provisionedAt": "2026-11-10T12:00:00Z",
    "profileId": "66a1f0c2a4b9d3e8f1a2b3c4"
  }
}
```

The number is active and billing. `purchaseIntentId` makes a retry return the same number instead of buying a second one. `connectWhatsapp` defaults to `true`, so send `false` for a calls and SMS number.

## Examples

### Pick an exact number

Search the live stock, then pass one result as `phoneNumber`. `sms: true` returns only numbers that can text, and `areaCode` narrows the area.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const { data: inventory } = await zernio.phonenumbers.searchAvailablePhoneNumbers({
  query: { country: 'US', areaCode: '415', sms: true, limit: 10 },
});

const { data: purchase } = await zernio.phonenumbers.purchasePhoneNumber({
  body: {
    profileId: '66a1f0c2a4b9d3e8f1a2b3c4',
    country: 'US',
    phoneNumber: inventory.numbers[0].phoneNumber,
    wantsSms: true,
    connectWhatsapp: false,
  },
});
```
</Tab>
<Tab value="Python">
```python
inventory = client.phone_numbers.search_available_phone_numbers(
    country="US", area_code="415", sms=True, limit=10
)

purchase = client.phone_numbers.purchase_phone_number(
    profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
    country="US",
    phone_number=inventory["numbers"][0]["phoneNumber"],
    wants_sms=True,
    connect_whatsapp=False,
)
```
</Tab>
<Tab value="curl">
```bash
curl "https://zernio.com/api/v1/phone-numbers/available?country=US&areaCode=415&sms=true&limit=10" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"
```
</Tab>
</Tabs>

The search returns each number with its `features`:

```json
{
  "country": "US",
  "numberType": "local",
  "requireSms": true,
  "numbers": [
    { "phoneNumber": "+14155550100", "features": ["voice", "sms", "mms"] },
    { "phoneNumber": "+14155550101", "features": ["voice", "sms", "mms"] }
  ]
}
```

If someone buys the number first, the purchase answers `409` `PHONE_NUMBER_UNAVAILABLE` instead of assigning another. Search again and pick one.

### Turn on SMS

Enable SMS on the number you bought. The response says whether the number can text and whether it still needs a US registration.

```bash
curl -X POST "https://zernio.com/api/v1/phone-numbers/66d4e5f6a7b8c9d0e1f2a3b4/sms" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"
```

```json
{
  "enabled": true,
  "id": "66b2e19d8c3f5a7e9d0b1c2d",
  "phoneNumber": "+14155550100",
  "isActive": true,
  "country": "US",
  "smsCapable": true,
  "mmsCapable": true,
  "needsRegistration": true,
  "alreadyRegistered": false,
  "registrationStatus": null,
  "reusable": null
}
```

The number receives texts right away. With `needsRegistration: true`, it sends once a [carrier registration](/platforms/sms/setup) is approved. Then text with the [SMS API](/products/sms).

### Check a number before porting

Ask whether numbers you own elsewhere can move to Zernio. Coverage varies by number type, so the check answers per number.

```bash
curl -X POST "https://zernio.com/api/v1/phone-numbers/port-in/check" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phoneNumbers": ["+34911234567"]}'
```

```json
{
  "results": [
    {
      "phoneNumber": "+34911234567",
      "portable": true,
      "fastPortable": false,
      "lineType": "landline",
      "countryCode": "ES",
      "phoneNumberType": "local",
      "notPortableReason": null
    }
  ]
}
```

The full port, with its documents and dates, is on [Porting](/platforms/phone-numbers/porting).

## How it behaves

- **Profiles.** A profile holds one WhatsApp number. The response's `profileId` says where the number landed, which can differ from the one you sent.
- **Plan limit.** Your plan caps how many numbers you hold. Numbers waiting on KYC count toward it.
- **Out of stock.** When a country has no stock, `POST /v1/phone-numbers/stock-watches` sends a webhook when numbers come back.
- **Release is final.** `DELETE /v1/phone-numbers/{id}` stops the charge and returns the number to the carrier. It cannot be undone.

## If it fails

| Error | Cause | Fix |
|---|---|---|
| `402` `PAYMENT_REQUIRED` | No payment method on file | Add a card, then retry with the same `purchaseIntentId` |
| `409` `PHONE_NUMBER_UNAVAILABLE` | The exact number you picked is gone | Search again and pick another |
| `409` `AREA_CODE_UNAVAILABLE` | No stock in that `areaCode` | Try another area, or omit `areaCode` |
| `409` `PURCHASE_VELOCITY` | A second purchase within 10 minutes | Pass `allowMultiple: true` when buying in bulk |
| `400` | The plan's number limit is reached | Release a number or upgrade the plan |

## Related

- [Buying a number](/platforms/phone-numbers/setup)
- [Availability](/platforms/phone-numbers/availability)
- [KYC](/platforms/phone-numbers/kyc)
- [Porting](/platforms/phone-numbers/porting)
- [Purchase phone number](/phone-numbers/purchase-phone-number)
- [Phone pricing](/pricing/phone)