# Buy a number

Search live inventory, buy a number in one request, watch sold-out countries and release numbers you no longer need.

Buying a number takes two calls: search the inventory, then purchase. In countries that do not regulate numbers (the US among them) the number is active when the purchase returns. Regulated countries answer with a KYC step instead.

## Before you start

- A payment method on file. Telephony is billed on your monthly invoice.
- A profile id. Each number belongs to one profile. See [Profiles](https://pozy.ai/docs/build/profiles).

## Search available numbers

`GET /v1/phone-numbers/available` queries the carrier's live inventory for a country (default `US`). Every result lists its `features`, and voice is always included.

```bash cURL
curl "https://api.pozy.ai/v1/phone-numbers/available?country=US&areaCode=415&sms=true&limit=10" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/phone-numbers/available?country=US&areaCode=415&sms=true&limit=10", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

for (const n of data.numbers) console.log(n.phoneNumber, n.locality, n.features);
```
```python Python
import os, requests

resp = requests.get(
    "https://api.pozy.ai/v1/phone-numbers/available",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"country": "US", "areaCode": "415", "sms": "true", "limit": 10},
    timeout=30,
)
for n in resp.json()["numbers"]:
    print(n["phoneNumber"], n.get("locality"), n["features"])
```

```json
{
  "country": "US",
  "numberType": "local",
  "requireSms": true,
  "numbers": [
    { "phoneNumber": "+14155550142", "features": ["voice", "sms", "mms"], "locality": "San Francisco" },
    { "phoneNumber": "+14155550143", "features": ["voice", "sms", "mms"], "locality": "San Francisco", "bestEffort": true }
  ]
}
```

| Query parameter | What it does |
| --- | --- |
| `country` | ISO 3166 alpha-2 code. Must be a country from `GET /v1/phone-numbers/countries`. |
| `numberType` | `local`, `mobile`, `national` or `toll_free`. Defaults to the country's default type. |
| `areaCode` | An area code (`415`) or a city name. A city covers every area code it has. |
| `locality` | A city name, matched against the national numbering plan. Accents and common aliases work. |
| `contains` | Digits the number must contain. |
| `sms` | `true` returns SMS-capable numbers only. |
| `limit` | Results to return. Default 20, maximum 100. |

When too few numbers match, nearby numbers fill the list and carry `bestEffort: true`. Drop them if the exact area matters. An unknown query parameter is a `400` that lists the accepted ones.

## Purchase

`POST /v1/phone-numbers/purchase` takes a `profileId` and a `country`. Leave out `phoneNumber` and Pozy assigns one. Pass a `phoneNumber` from your search to buy that exact number.

```bash cURL
curl -X POST https://api.pozy.ai/v1/phone-numbers/purchase \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6650c1f0a2b3c4d5e6f70812",
    "country": "US",
    "phoneNumber": "+14155550142",
    "wantsSms": true,
    "connectWhatsapp": false,
    "purchaseIntentId": "order-8841-line-1"
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/phone-numbers/purchase", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    profileId: "6650c1f0a2b3c4d5e6f70812",
    country: "US",
    phoneNumber: "+14155550142",
    wantsSms: true,
    connectWhatsapp: false,
    purchaseIntentId: "order-8841-line-1",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.phoneNumber?.id, data.phoneNumber?.status);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/phone-numbers/purchase",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "profileId": "6650c1f0a2b3c4d5e6f70812",
        "country": "US",
        "phoneNumber": "+14155550142",
        "wantsSms": True,
        "connectWhatsapp": False,
        "purchaseIntentId": "order-8841-line-1",
    },
    timeout=60,
)
print(resp.status_code, resp.json())
```

```json
{
  "message": "Phone number provisioned",
  "phoneNumber": {
    "id": "6650d2e1b3c4d5e6f7081923",
    "phoneNumber": "+14155550142",
    "status": "active",
    "country": "US",
    "provisionedAt": "2026-10-30T10:12:00Z",
    "profileId": "6650c1f0a2b3c4d5e6f70812"
  }
}
```

Keep `phoneNumber.id`. It is the `{id}` for every feature endpoint (`/voice`, `/sms`, `/sip-trunk`).

| Body field | Default | What it does |
| --- | --- | --- |
| `profileId` | required | The profile that owns the number |
| `country` | `US` | Country to buy in |
| `numberType` | country default | `local`, `mobile`, `national` or `toll_free` |
| `phoneNumber` | none | Buy this exact E.164 number. Only works where numbers activate instantly. |
| `areaCode` | none | Hold an auto-assigned number to this area. The purchase fails rather than give you another area. |
| `wantsSms` | `false` | Draw from SMS-capable stock and flag the number for SMS |
| `connectWhatsapp` | `true` | Start the WhatsApp connection on the new number. Send `false` for a calls or SMS line. |
| `wantsWhatsapp` | `false` | With `connectWhatsapp: false`, still pick a number WhatsApp will accept |
| `purchaseIntentId` | none | Your own key for this purchase, up to 100 characters. A retry with the same key returns `already_purchased` instead of buying twice. |
| `allowMultiple` | `false` | Confirm that a second purchase within 10 minutes is intentional |

> **Purchases are not idempotent by default:**
> 
> Always send a `purchaseIntentId`, one per number you mean to buy. A retry with the same value returns `{ "status": "already_purchased", "numberId", "phoneNumber", "profileId" }` and does not charge again. The key survives the round trip through adding a payment method.

### When the profile already has a number

A profile holds one number. If the profile you pass already has one, Pozy puts the new number on the next free profile (creating one if needed) and returns it in `profileId`. Read `profileId` from the response rather than assuming.

### Regulated countries

A country with `needsKyc: true` answers `202` instead of provisioning:

```json
{
  "status": "kyc_required",
  "country": "GB",
  "numberType": "local",
  "kycUrl": "https://pozy.ai/kyc/..."
}
```

Send your customer to `kycUrl`, or collect the details yourself with the [KYC endpoints](https://pozy.ai/docs/telephony/kyc). Passing `phoneNumber` for a regulated country or type is a `400`: the number is assigned after review.

## Check one country

`GET /v1/phone-numbers/availability?country=BR&numberType=local` answers for a single country: whether stock exists, which area codes are in stock (`areaOptions`), and the address rule for KYC (`addressConstraint`).

| `addressConstraint` | What the registrant needs |
| --- | --- |
| `geo` | An address inside the area of the number |
| `country` | Any address in the country |
| `none` | No address |

`areaAvailability` sorts every area of the country into `inStock`, `preOrder` and `outOfStock`. Pass an `ndc` from `inStock` as `areaCode` on the purchase to hold the order to that area.

## Watch a sold-out country

When a country or area has no stock, you have two options:

- **Pre-order it.** If the type is `preOrderable`, submit the [KYC form](https://pozy.ai/docs/telephony/kyc#out-of-stock-pre-order) with `preOrder: true`. The number is sourced for you, usually within 2 to 4 weeks, and nothing is billed until it is active.
- **Watch it.** `POST /v1/phone-numbers/stock-watches` with a `country` (and optionally `numberType` and `areaCode`) puts you on the list.

```bash
curl -X POST https://api.pozy.ai/v1/phone-numbers/stock-watches \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "country": "IE", "numberType": "local" }'
```

Stock is checked every few hours. When numbers come back, Pozy sends the `phone_number.stock_available` webhook, so subscribe to it to hear about it. A watch fires once and is then used up, so create it again if you miss the stock.

| Rule | Value |
| --- | --- |
| Watches per workspace | 20 |
| Watches per country and type | 1 (a repeat request returns the existing watch) |
| Watch on a country that is in stock | `409`: buy instead |

## Manage numbers

- `GET /v1/phone-numbers` lists your numbers. Filter by `status` or `profileId`. Released numbers are left out unless you ask for `status=released`.
- `GET /v1/phone-numbers/{id}` returns one number with its features and voice settings.
- `DELETE /v1/phone-numbers/{id}` releases a number.

> **Releasing is permanent:**
> 
> A released number goes back to the carrier pool and stops billing. You cannot get it back. A number attached to a SIP trunk must be [detached](https://pozy.ai/docs/telephony/sip-trunks) first, or the release returns `409 invalid_resource_state`.

## Limits

| Limit | Value |
| --- | --- |
| Numbers per profile | 1 |
| Numbers per workspace | Your profile limit (no cap where profiles are unlimited) |
| Purchase velocity | A second purchase within 10 minutes needs `allowMultiple: true` |
| Search results | 20 by default, 100 at most |

## Errors

| Status | Code | Cause | Fix |
| --- | --- | --- | --- |
| `400` | | The country is not offered, or `phoneNumber` was sent for a regulated country | Pick a country from the countries list, or drop `phoneNumber` and run KYC |
| `400` | | `numberType: "toll_free"` with `connectWhatsapp: true` | Send `connectWhatsapp: false` |
| `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 |
| `409` | `AREA_CODE_UNAVAILABLE` | No deliverable stock in that `areaCode` | Pick another area, or leave it out |
| `409` | `NO_WHATSAPP_ELIGIBLE_NUMBER` | Every number offered is one WhatsApp refuses | Pass an `areaCode`, or retry |
| `409` | `PURCHASE_VELOCITY` | A second purchase within 10 minutes | Send `allowMultiple: true` |

All errors use the standard [error shape](https://pozy.ai/docs/getting-started/errors).

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/phone-numbers/countries`](https://pozy.ai/docs/api/listPhoneNumberCountries) | List offerable number countries |
| [`GET /v1/phone-numbers/availability`](https://pozy.ai/docs/api/checkPhoneNumberAvailability) | Check country availability |
| [`GET /v1/phone-numbers/available`](https://pozy.ai/docs/api/searchAvailablePhoneNumbers) | Search available numbers |
| [`POST /v1/phone-numbers/purchase`](https://pozy.ai/docs/api/purchasePhoneNumber) | Purchase phone number |
| [`GET /v1/phone-numbers/claims/{claimId}`](https://pozy.ai/docs/api/getPhoneNumberClaim) | Resolve a number claim |
| [`GET /v1/phone-numbers`](https://pozy.ai/docs/api/listPhoneNumbers) | List phone numbers |
| [`GET /v1/phone-numbers/{id}`](https://pozy.ai/docs/api/getPhoneNumber) | Get phone number |
| [`DELETE /v1/phone-numbers/{id}`](https://pozy.ai/docs/api/releasePhoneNumber) | Release phone number |
| [`GET /v1/phone-numbers/stock-watches`](https://pozy.ai/docs/api/listPhoneNumberStockWatches) | List stock watches |
| [`POST /v1/phone-numbers/stock-watches`](https://pozy.ai/docs/api/createPhoneNumberStockWatch) | Watch an out-of-stock country |
| [`DELETE /v1/phone-numbers/stock-watches/{id}`](https://pozy.ai/docs/api/deletePhoneNumberStockWatch) | Stop watching a country |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`whatsapp.number.activated`](https://pozy.ai/docs/webhooks/events#whatsapp-number-activated) | WhatsApp number activated. |
| [`whatsapp.number.suspended`](https://pozy.ai/docs/webhooks/events#whatsapp-number-suspended) | WhatsApp number suspended. |
| [`whatsapp.number.reactivated`](https://pozy.ai/docs/webhooks/events#whatsapp-number-reactivated) | WhatsApp number reactivated. |
| [`whatsapp.number.released`](https://pozy.ai/docs/webhooks/events#whatsapp-number-released) | WhatsApp number released. |
| [`phone_number.stock_available`](https://pozy.ai/docs/webhooks/events#phone-number-stock-available) | Phone-number stock available. |
