Zernio
APIFor you
Zernio
APIFor you
Platforms

Phone Numbers

Overview

Set up

Availability & pricingBuying a numberPortingKYC (regulated countries)

Calls

Branded calling

Reference

Limits & errors
Dashboard
OverviewPlatformsAPI ReferenceResources
Phone Numbers

Buying a number

Search a country's inventory with GET /v1/phone-numbers/available, buy a number with POST /v1/phone-numbers/purchase, then enable SMS, WhatsApp and voice routing on it.


By the end of this page, you own a number that is active and billing, with its id for the feature endpoints.

Before you start

You need a profile id, usage-based billing and a payment method on file.

Step 1: Search available numbers

Search confirms stock and capabilities in a country or prefix. The monthly price is on GET /v1/phone-numbers/countries.

Call GET /v1/phone-numbers/available with country (default US). It queries the carrier's live inventory.

Voice is always in the filter. sms=true draws only from the SMS-capable pool.

Each result carries its full capability set and its locality (the town the number belongs to). Numbers a purchase would refuse are left out.

Padded results. When too few numbers match, the carrier pads the list with nearby ones and marks them bestEffort: true. Drop those when the prefix or town matters.

ParamPurpose
countryISO-2 country, must be offerable
numberTypelocal, mobile, national or toll_free; defaults to the country's WhatsApp-safe type. Where a country stocks more than one (Spain: local geographic 9xx, national 902), pass it explicitly
areaCodeAn area code (415, 912) or a city name. A city is every area code the numbering plan gives it (Madrid is 910 to 919), so the search covers all of them
localityA city name, matched against the numbering plan (accents and common aliases allowed: Gießen, Sevilla, Milano). A name no city of the plan matches returns no numbers rather than a guess
containsDigit pattern the number must contain
smstrue to return SMS-capable numbers only
limitMax results (default 20, max 100)

These are the same names POST /v1/phone-numbers/purchase and GET /v1/phone-numbers/availability use. (type and prefix, the original names, still work.)

A parameter the endpoint does not know is a 400 that lists the accepted ones. Nothing is silently ignored.

import Zernio from '@zernio/node';

const zernio = new Zernio();
const profileId = '66a1f0c2a4b9d3e8f1a2b3c4';

const { data: inventory } = await zernio.phonenumbers.searchAvailablePhoneNumbers({
  query: { country: 'US', areaCode: '415', sms: true, limit: 10 }
});
for (const n of inventory.numbers) {
  console.log(n.phoneNumber, n.features);
}

Response (200):

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

Step 2: Purchase

Purchase draws from the same pool. It auto-assigns a number, or buys one exact number you pass from the results.

Call POST /v1/phone-numbers/purchase with profileId and country.

With usage-based billing and a payment method on file, the number provisions in the same request. It starts billing per month on your usage-based invoice.

  • wantsSms: true draws an SMS-capable number and flags it for SMS (default off).
  • connectWhatsapp defaults to on. For a Calls/SMS-only number, send connectWhatsapp: false.

One exact number. Pass a search result's phoneNumber (E.164). It is a hard constraint.

When that number is gone by the time you buy, or WhatsApp's buy-time check rejects it, the purchase fails with 409 and code: "PHONE_NUMBER_UNAVAILABLE". It does not assign another, so search again and pick another.

It only works where the number activates instantly. A regulated country or type, which answers 202 with kyc_required, returns 400 when phoneNumber is set.

Auto-assigned. Without phoneNumber the assignment is Zernio's to make. Two optional fields narrow it:

  • areaCode (1 to 4 digits, listed as areaOptions by GET /v1/phone-numbers/availability) is a hard constraint. The purchase fails with a 409 rather than hand you a number in another area, and a later replacement stays in the same area.
  • wantsWhatsapp: true declares WhatsApp intent on a standalone purchase (connectWhatsapp: false). A number WhatsApp rejects at buy time is swapped for an eligible one during the purchase, instead of arriving without WhatsApp.
const { data: purchase } = await zernio.phonenumbers.purchasePhoneNumber({
  body: {
    profileId,
    country: 'US',
    wantsSms: true,
    connectWhatsapp: false,
  }
});
if (purchase.status === 'kyc_required') {
  console.log('Regulated country, collect KYC at', purchase.kycUrl);
} else {
  console.log('Provisioned', purchase.phoneNumber.phoneNumber, purchase.phoneNumber.id);
}

To buy the first number from Step 1's search instead of an auto-assigned one, add phoneNumber:

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", "phoneNumber": "+14155550123", "connectWhatsapp": false}'

Response (200):

{
  "message": "Phone number provisioned",
  "phoneNumber": {
    "id": "66d4e5f6a7b8c9d0e1f2a3b4",
    "phoneNumber": "+14155550100",
    "status": "active",
    "country": "US",
    "provisionedAt": "2027-01-01T12:00:00Z",
    "profileId": "66a1f0c2a4b9d3e8f1a2b3c4"
  }
}

phoneNumber.id is the {id} for every feature endpoint.

One number goes on one profile. When the requested profile already holds a number, Zernio assigns the next free profile (or creates one) and returns it in profileId.

A regulated country returns 202 instead, and the number is ordered once the identity check clears:

{
  "status": "kyc_required",
  "country": "GB",
  "numberType": "local",
  "kycUrl": "https://zernio.com/kyc/..."
}

Send the customer to kycUrl, or run the KYC flow yourself.

If nothing is in stock when the form is submitted, the submission becomes a pre-order instead of failing.

Purchasing is not idempotent by default. Send a purchaseIntentId (any string, one per intended purchase) so a retry does not buy a second number.

A retry with the same key returns { "status": "already_purchased", "numberId", "phoneNumber", "profileId" }. The key survives the add-payment-method round trip.

Velocity check. Separately, any second purchase within 10 minutes returns 409 with code: "PURCHASE_VELOCITY". Pass allowMultiple: true to confirm bulk provisioning is intentional.

Number cap. The cap is your profile limit. Accounts with unlimited profiles have no phone-number count cap.

For accounts with a finite cap, the count includes every number that is provisioning, verifying, active, pending payment or pending regulatory review. A purchase past the cap returns 400.

Step 3: Enable features

A fresh number already has Calls on. Add the rest from the number's own routes:

  • SMS: enable and register with POST /v1/phone-numbers/{id}/sms. US numbers need an approved carrier registration before messages deliver.
  • WhatsApp: connect to a WABA.
  • Voice routing: set the forward destination, IVR and voicemail with POST /v1/phone-numbers/{id}/voice.

Manage numbers

  • GET /v1/phone-numbers lists every number on your team. Released numbers are excluded by default; filter with status or profileId.
  • GET /v1/phone-numbers/{id} returns one.
  • DELETE /v1/phone-numbers/{id} releases one.
Releasing stops the monthly charge and returns the number to the carrier pool. It cannot be undone.
const { data: owned } = await zernio.phonenumbers.listPhoneNumbers();
for (const n of owned.numbers) {
  console.log(n._id, n.phoneNumber, n.status, n.monthlyCents);
}

await zernio.phonenumbers.releasePhoneNumber({ path: { id: '66d4e5f6a7b8c9d0e1f2a3b4' } });

Response (200) of the list:

{
  "numbers": [
    {
      "_id": "66d4e5f6a7b8c9d0e1f2a3b4",
      "phoneNumber": "+14155550100",
      "country": "US",
      "status": "active",
      "monthlyCents": 300,
      "hostedByZernio": true,
      "sipTrunkId": null,
      "provisionedAt": "2027-01-01T12:00:00Z"
    }
  ],
  "connected": []
}

monthlyCents is stamped at purchase, so a number keeps its price when the rate card changes.

connected lists WhatsApp numbers you brought yourself through embedded signup. They are not billed, and SMS and Calls cannot be enabled on them.

Response (200) of the release:

{
  "message": "Phone number released",
  "phoneNumber": {
    "id": "66d4e5f6a7b8c9d0e1f2a3b4",
    "phoneNumber": "+14155550100",
    "status": "released",
    "releasedAt": "2027-02-01T12:00:00Z"
  }
}

If it fails

A 402 with code: "PAYMENT_REQUIRED" on purchase means there is no payment method on file:

{
  "error": "A payment method is required to purchase a phone number.",
  "type": "invalid_request_error",
  "code": "PAYMENT_REQUIRED"
}

Add a card in the dashboard, then retry with the same purchaseIntentId.

The 409 codes:

  • AREA_CODE_UNAVAILABLE: the requested areaCode has no deliverable inventory. Pick another area or omit it.
  • PHONE_NUMBER_UNAVAILABLE: the phoneNumber you picked is no longer available. Search again and pick another.
  • NO_WHATSAPP_ELIGIBLE_NUMBER: every number the carrier offered is one WhatsApp refuses to register. Pass an areaCode (see areaOptions on GET /v1/phone-numbers/availability) or try again for a fresh batch.
  • invalid_resource_state (on release): the number is attached to a SIP trunk. Detach it first.

Every error uses the envelope in error handling.

Related

  • Porting: bring a number you already own instead of buying.
  • KYC: the identity step for regulated countries.
  • Availability and pricing: countries, types and prices.
  • Purchase phone number and List phone numbers: every field.
  • Phone number webhooks: whatsapp.number.activated and the other status events.
Was this page helpful?

Availability & pricing

Read which countries and number types Zernio sells, what each costs per month, and which capabilities each supports, from GET /v1/phone-numbers/countries.

Porting

Move a number you own at another carrier onto Zernio: check portability, upload the LOA and invoice, submit the port-in and track it to ported.

On this page

Before you startStep 1: Search available numbersStep 2: PurchaseStep 3: Enable featuresManage numbersIf it failsRelated