REST · JSON · Signed webhooks
Phone Number API
The PhoneBorn Phone Number API is a REST API for buying real mobile numbers in 200 countries on monthly or yearly plans and receiving their SMS in your own app — by polling or HMAC-signed webhooks. One API key, one crypto-funded wallet, JSON everywhere.
Overview
The PhoneBorn API lets you list countries and prices, buy phone-number plans (real mobile numbers you keep), read incoming SMS and buy single-use OTP numbers — from your own code. It is a small, predictable REST API with JSON bodies, API-key authentication and signed webhooks. Purchases are charged to the same prepaid wallet you top up with crypto, so there are no separate billing contracts.
This page covers phone-number plans. Outgoing SMS and calls are made from the dashboard; the API focuses on buying numbers and receiving messages. For single-use verification codes, see the OTP Verification API.
Authentication
All requests go to https://phoneborn.com/v1 over HTTPS and are authenticated with an API key in the X-API-Key header. Create and revoke keys in Dashboard → API; each key is shown once, so store it in a secret manager. Request and response bodies are JSON, money values are USD strings with two decimals (e.g. "4.99"), and timestamps are ISO 8601 in UTC. Errors return a non-2xx status with {"detail": "message"}.
curl https://phoneborn.com/v1/balance \
-H "X-API-Key: pb_live_xxxxxxxxxxxxxxxx"
# 200 OK
{ "balance": "25.00" }Endpoint reference
Base URL: https://phoneborn.com/v1
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/balance | Current wallet balance |
| GET | /v1/countries | Countries with monthly, yearly and OTP prices and live stock |
| GET | /v1/services | Supported services with lowest OTP price |
| GET | /v1/prices?country=&service= | Exact prices for a country (and optional service) |
| POST | /v1/otp | Buy a single-use OTP number for a service |
| GET | /v1/otp/{id} | Poll an OTP order — status, code and SMS text |
| POST | /v1/otp/{id}/cancel | Cancel before a code arrives (full refund) |
| POST | /v1/numbers | Buy a phone number on a monthly or yearly plan |
| GET | /v1/numbers | List your phone numbers and their plans |
| GET | /v1/numbers/{id}/sms | Read SMS received on one of your phone numbers |
List countries & prices
Returns every enabled country with the current monthly, yearly and minimum OTP price, plus the number of numbers in stock. Cache it for a few minutes — prices rarely change.
curl https://phoneborn.com/v1/countries -H "X-API-Key: $PHONEBORN_API_KEY"[
{
"iso": "GB", "name": "United Kingdom", "slug": "united-kingdom", "flag": "🇬🇧",
"dial_code": "+44", "region": "Europe", "available": 34,
"monthly": "5.49", "yearly": "27.45", "otp_from": "0.30"
}
]For one country use GET /v1/prices?country=GB, which returns { "monthly", "yearly", "otp", "yearly_saving_pct" }.
Buy a phone number
POST /v1/numbers with a country ISO code and a plan (monthly = 30 days, yearly = 365 days). The price is charged to your wallet, the number is active immediately and it stays yours on every renewal. If your balance is too low you get 402; if the country is out of stock you get 409.
curl -X POST https://phoneborn.com/v1/numbers \
-H "X-API-Key: $PHONEBORN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"country": "GB", "plan": "monthly"}'{
"id": 912,
"number": { "e164": "+44XX123456XX", "display": "+44 XX 1234 56XX" },
"country": { "iso": "GB", "name": "United Kingdom", "flag": "🇬🇧" },
"plan": "monthly",
"price": "5.49",
"started_at": "2026-09-24T10:12:03Z",
"expires_at": "2026-10-24T10:12:03Z",
"auto_renew": true,
"status": "active",
"days_left": 30,
"label": null,
"sms_count": 0,
"renew_price": { "monthly": "5.49", "yearly": "27.45" }
}Check the auto_renew field in the response and manage renewals from the dashboard. List all your numbers with GET /v1/numbers.
Read incoming SMS
GET /v1/numbers/{id}/sms returns messages newest-last. Pass after_id with the highest id you have already seen to fetch only new messages. When we can detect a verification code, it is returned separately in code.
curl "https://phoneborn.com/v1/numbers/912/sms?after_id=0" -H "X-API-Key: $PHONEBORN_API_KEY"[
{
"id": 55012,
"sender": "Telegram",
"body": "Telegram code: 70582. Do not give this code to anyone.",
"code": "70582",
"received_at": "2026-09-24T10:14:41Z"
}
]Polling every 5–10 seconds is fine; for real-time delivery without polling, use webhooks.
Webhooks & signatures
Set a webhook URL in Dashboard → API and we will POST an event the moment an SMS arrives on any of your numbers — phone-number plan or OTP. OTP events include order_id; phone-number events include rental_id.
{
"event": "sms.received",
"number": "+44XX123456XX",
"sender": "WhatsApp",
"body": "Your WhatsApp code is 482-913. Do not share it.",
"code": "482913",
"order_id": 1842
}Every request carries an X-PhoneBorn-Signature header: sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your webhook secret. Always verify it with a constant-time comparison before trusting the payload, and respond with a 2xx within a few seconds.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.PHONEBORN_WEBHOOK_SECRET;
// Use the RAW body — re-serialised JSON will not match the signature.
app.post("/webhooks/phoneborn", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-PhoneBorn-Signature") || "";
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const a = Buffer.from(header);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.event === "sms.received") {
console.log(`Code ${event.code} from ${event.sender} on ${event.number}`);
}
res.sendStatus(200); // respond quickly; do heavy work async
});
app.listen(3000);import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["PHONEBORN_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/phoneborn")
def phoneborn_webhook():
raw = request.get_data() # raw bytes, exactly as sent
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
received = request.headers.get("X-PhoneBorn-Signature", "")
if not hmac.compare_digest(received, expected):
abort(401)
event = request.get_json()
if event["event"] == "sms.received":
print(f"Code {event['code']} from {event['sender']} on {event['number']}")
return "", 200Complete examples
const API = "https://phoneborn.com/v1";
const headers = { "X-API-Key": process.env.PHONEBORN_API_KEY, "Content-Type": "application/json" };
// 1. Buy a UK number on a monthly plan
const rental = await fetch(`${API}/numbers`, {
method: "POST", headers, body: JSON.stringify({ country: "GB", plan: "monthly" }),
}).then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); });
console.log("Your number:", rental.number.e164);
// 2. Poll for new messages (or use webhooks — recommended)
let lastId = 0;
setInterval(async () => {
const sms = await fetch(`${API}/numbers/${rental.id}/sms?after_id=${lastId}`, { headers }).then((r) => r.json());
for (const m of sms) {
lastId = Math.max(lastId, m.id);
console.log(`[${m.sender}] ${m.body}`, m.code ? `→ code ${m.code}` : "");
}
}, 5000);import os, time, requests
API = "https://phoneborn.com/v1"
H = {"X-API-Key": os.environ["PHONEBORN_API_KEY"]}
rental = requests.post(f"{API}/numbers", json={"country": "GB", "plan": "yearly"}, headers=H)
rental.raise_for_status()
rental = rental.json()
print("Your number:", rental["number"]["e164"], "expires", rental["expires_at"])
last_id = 0
while True:
for m in requests.get(f"{API}/numbers/{rental['id']}/sms", params={"after_id": last_id}, headers=H).json():
last_id = max(last_id, m["id"])
print(m["sender"], m["body"], m.get("code"))
time.sleep(5)Errors
Errors use standard HTTP status codes with a JSON body: {"detail": "Insufficient balance"}.
| Status | Meaning | When it happens |
|---|---|---|
| 400 | Bad request | Malformed JSON or an unknown country / service / plan. |
| 401 | Unauthorized | Missing, invalid or revoked X-API-Key. |
| 402 | Payment required | Insufficient wallet balance — top up and retry. |
| 404 | Not found | The order, number or resource does not exist or is not yours. |
| 409 | Conflict | Action not allowed in the current state (e.g. cancelling after a code arrived) or no stock for that country. |
| 422 | Validation error | A field is missing or has the wrong type. |
| 429 | Too many requests | Rate limit exceeded — back off and retry with exponential delay. |
| 5xx | Server error | Temporary problem on our side or at the provider. Safe to retry reads; check order status before retrying purchases. |
Limits & best practices
- Keep API keys server-side only. Rotate them from the dashboard if one is exposed — revocation is immediate.
- Handle
429with exponential back-off. Poll no more often than every few seconds per number. - Purchases are not idempotent: if a
POSTtimes out, list your numbers before retrying to avoid buying twice. - Monitor your balance with
GET /v1/balanceso auto-renewals never fail. - Use of the API is subject to our Terms of Service, including the acceptable-use rules.
Build on PhoneBorn.
Create an account, generate a key and make your first call in minutes.