Developers
API reference.
Order a real phone number, read the codes that land on it and answer them — in four HTTP calls. Same terms as the panel: no identity, no contract, paid from your crypto balance.
- Base URL
- https://api.nokycphone.com/v1
- Version
- v1 — additive changes only
- Auth
- Authorization: Bearer nkp_live_…
- Content type
- application/json or form-encoded
Quickstart
Four calls take you from nothing to a working number with its inbox. Everything below is copy-pasteable; replace the key and you are live.
- Create an account in the panel and generate a key under API keys. Keys start with
nkp_live_and are shown once. - Top up your balance in crypto. A number is charged from the balance, never from a card.
- Order a line. It is active within about a minute of the call returning.
- Read the inbox, or point a webhook at your own endpoint and stop polling.
# order a French mobile line for one month
curl -X POST https://api.nokycphone.com/v1/numbers \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…" \
-H "Idempotency-Key: 7d1e4c22-90aa-4d1f-8b0e-1c2f9a55b311" \
-H "Content-Type: application/json" \
-d '{"country":"fr","type":"mobile","period":1,"label":"signups"}'
# 201 Created
{
"id": "num_8f2c1a94",
"e164": "+33647189022",
"country": "fr",
"type": "mobile",
"status": "active",
"label": "signups",
"auto_renew": true,
"created_at": "2026-09-21T09:14:02Z",
"renews_at": "2026-10-21T09:14:02Z",
"charged": { "monthly": 12.90, "activation": 10.00, "total": 22.90, "currency": "USD" },
"balance_after": 77.10
}# read the inbox, newest first — the code is already extracted for you
curl "https://api.nokycphone.com/v1/numbers/num_8f2c1a94/messages?limit=2" \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…"
# 200 OK
{
"object": "list",
"has_more": false,
"data": [
{
"id": "msg_5b1f77c0",
"direction": "inbound",
"from": "Telegram",
"to": "+33647189022",
"body": "Your login code is 72194. Do not share it.",
"code": "72194",
"received_at": "2026-09-21T09:15:41Z"
},
{
"id": "msg_5b1f7411",
"direction": "inbound",
"from": "+14155550142",
"to": "+33647189022",
"body": "Your verification code: 408-113",
"code": "408113",
"received_at": "2026-09-21T09:14:58Z"
}
]
}The code field does the boring part. We run the same extraction the panel uses: digit groups of four to eight characters, spaces and dashes removed, the sender's own phone number ignored. When nothing looks like a code, code is null and the body is still there.
Authentication
Every call carries a bearer key in the Authorization header. There is no second factor, no signature on the request and no IP allowlist by default — the key is the credential, so treat it like one.
Authorization: Bearer nkp_live_9f2c8a41d0b7e5…Creating and rotating keys
- Create a key from API keys in the panel. The full value is displayed once; we keep a hash, not the key.
- Give each integration its own key and label it. Revoking one never touches the others.
- Revocation is immediate. In-flight requests finish, the next one returns
401. - An account can hold up to ten live keys. Keys never expire on their own.
Scopes
A key carries one of three scopes, chosen when you create it.
| Scope | Can do | Typical use |
|---|---|---|
read | List and retrieve numbers, messages, calls, balance. | Dashboards, monitoring, a bot that only reads codes. |
write | Everything read can, plus send SMS, edit line settings, manage webhooks. | The normal choice for an integration. |
admin | Everything write can, plus order numbers, renew, release and open top-ups. | Anything that spends balance. |
A leaked key can spend your balance. If a key with admin scope leaks, revoke it in the panel first and check your ledger second. We cannot reverse an on-chain payment, and we have no e-mail address to warn you at — that is the trade-off of an account with no identity attached.
Conventions
Requests
- Base URL is
https://api.nokycphone.com/v1. HTTPS only; plain HTTP is refused, not redirected. - Bodies may be JSON (
Content-Type: application/json) or form-encoded. Responses are always JSON, UTF-8. - Unknown fields in a request body are ignored, never an error. That is what makes additive changes safe.
Identifiers and time
- IDs are opaque strings with a type prefix:
num_,msg_,call_,vm_,whk_,top_. Do not parse them. - Every timestamp is RFC 3339 in UTC, ending in
Z. Durations are seconds, money is USD with two decimals. - Country codes are lowercase ISO 3166-1 alpha-2. Numbers are always E.164, with the leading
+.
Pagination
List endpoints return an object with object: "list", a data array, and has_more. Page with limit (1–100, default 25) and starting_after, which takes the last ID you saw.
curl "https://api.nokycphone.com/v1/messages?limit=50&starting_after=msg_5b1f7411" \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…"Idempotency
Send an Idempotency-Key header on any POST. We store the first response for 24 hours and replay it byte for byte if the same key comes back, so a timeout or a retry never orders two numbers. Use a fresh UUID per logical operation.
Retry safely. A request that never reached us has no key stored, so the retry runs normally. A request that reached us and timed out on your side replays the stored answer. Either way you end up with exactly one number.
Errors
Errors use standard status codes and always carry the same shape. The code field is stable and safe to branch on; message is written for a human and may change.
{
"error": {
"type": "balance_error",
"code": "insufficient_balance",
"message": "Your balance is 4.10 USD; this order costs 22.90 USD.",
"param": null,
"doc": "https://nokycphone.com/api/#billing"
}
}| Status | Code | What happened |
|---|---|---|
400 | invalid_request | A parameter is missing or malformed. param names it. |
401 | invalid_key | Missing, malformed or revoked key. |
403 | insufficient_scope | The key is valid but its scope does not cover this call. |
403 | country_unavailable | That country is not open to new orders right now. |
404 | not_found | No such object, or it belongs to another account. |
409 | number_released | The line has been released and cannot be used any more. |
422 | unsupported_type | That country has no line of the requested type. |
402 | insufficient_balance | Not enough balance. Open a top-up and retry. |
429 | rate_limited | Too many requests. Back off; see rate limits. |
503 | temporarily_unavailable | A dependency is down. Retry with backoff and check status. |
Endpoint index
| Method | Path | Does |
|---|---|---|
GET | /v1/countries | Availability and price per country. |
GET | /v1/numbers | List your lines. |
POST | /v1/numbers | Order a line. |
GET | /v1/numbers/{id} | Retrieve one line. |
PATCH | /v1/numbers/{id} | Label, auto-renew, caller ID, quiet hours, webhook, add-ons. |
POST | /v1/numbers/{id}/renew | Renew now for one period. |
DELETE | /v1/numbers/{id} | Release the line immediately. |
GET | /v1/numbers/{id}/messages | Inbox of one line. |
GET | /v1/messages | Messages across every line. |
GET | /v1/messages/{id} | Retrieve one message. |
POST | /v1/messages | Send an SMS. |
GET | /v1/calls | Call log. |
GET | /v1/voicemails/{id} | Voicemail with transcript and audio URL. |
GET | /v1/balance | Balance and recent ledger entries. |
POST | /v1/topups | Open a deposit address. |
GET | /v1/topups/{id} | Follow a deposit. |
GET | /v1/webhooks | List endpoints. |
POST | /v1/webhooks | Register an endpoint. |
DELETE | /v1/webhooks/{id} | Remove an endpoint. |
Countries
Read availability and price before you order. Prices are the same as on the country pages and move together — this endpoint is the machine-readable version of that table.
curl https://api.nokycphone.com/v1/countries \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…"
# 200 OK
{
"object": "list",
"data": [
{
"code": "fr",
"name": "France",
"dial": "+33",
"region": "europe",
"types": ["mobile", "landline"],
"in_stock": true,
"price": { "mobile": 12.90, "landline": 10.30, "activation": 10.00, "currency": "USD" },
"premium_surcharge": 4.90
}
]
}Filter with ?type=mobile, ?region=europe or ?in_stock=true. Stock is checked live against our carrier pools, so a country that reads false will refuse an order with country_unavailable rather than take your money and queue you.
Numbers
Order a line
POST /v1/numbers charges your balance and provisions the line. The call returns once the allocation is confirmed, typically in under a second; the line starts receiving within about a minute.
| Field | Type | Notes |
|---|---|---|
country | string, required | Lowercase ISO code, for example fr. |
type | string, required | mobile or landline. |
period | integer | 1, 3 or 12 months. Default 1. Quarterly takes 10% off, yearly 25%. |
premium | boolean | Ask for a memorable pattern. Adds $4.90 a month. |
e164 | string | Reserve one specific number returned by /v1/numbers/available. |
label | string | Your own name for the line, up to 40 characters. |
auto_renew | boolean | Default true. |
addons | array of strings | Any of ai-pickup, ai-screen, ai-summary, ai-trans. |
webhook_url | string | Per-line endpoint. Overrides the account-wide one. |
Activation is charged once, per line. $10.00 on the first period, never again on renewal. Releasing a line and taking a new one is a new activation.
Pick a specific number
List what is free in a country, hold nothing, then order the one you want by passing its e164. Candidates are not reserved; order within a few minutes.
curl "https://api.nokycphone.com/v1/numbers/available?country=de&type=mobile&premium=true" \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…"
# 200 OK
{
"object": "list",
"data": [
{ "e164": "+4915735550088", "pattern": "repeating", "surcharge": 4.90 },
{ "e164": "+4915735551234", "pattern": "sequential", "surcharge": 4.90 },
{ "e164": "+4915735557000", "pattern": "round", "surcharge": 4.90 }
]
}Update, renew, release
# stop auto-renew, mask the caller ID, mute the line at night
curl -X PATCH https://api.nokycphone.com/v1/numbers/num_8f2c1a94 \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…" \
-H "Content-Type: application/json" \
-d '{"auto_renew":false,"mask_caller_id":true,"quiet_from":"23:00","quiet_to":"07:00"}'POST /v1/numbers/{id}/renewcharges one more period straight away and movesrenews_atforward.DELETE /v1/numbers/{id}releases the line at once. It stops receiving immediately and cannot be recovered — the number goes back to the carrier pool.- A line whose renewal fails for lack of balance stays reachable for 3 days, then releases itself. Watch
number.graceon your webhook.
Messages
Read
GET /v1/messages spans every line on the account; GET /v1/numbers/{id}/messages is the same list scoped to one. Filter with direction, from, since and has_code=true.
curl "https://api.nokycphone.com/v1/messages?has_code=true&since=2026-09-21T00:00:00Z" \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…"Send
POST /v1/messages sends from one of your lines. Outbound SMS is $0.08 per segment, taken from the balance; a long message is split into segments and priced per segment.
curl -X POST https://api.nokycphone.com/v1/messages \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…" \
-H "Content-Type: application/json" \
-d '{"from":"num_8f2c1a94","to":"+447700900123","body":"On my way."}'
# 202 Accepted
{
"id": "msg_9c04ab71",
"direction": "outbound",
"status": "queued",
"segments": 1,
"charged": 0.08,
"created_at": "2026-09-21T09:22:10Z"
}Status moves queued → sent → delivered, or failed with a failure_code. Watch it on the message.status webhook rather than polling.
Sending is for conversations, not campaigns. Bulk or unsolicited messaging gets the line cut without refund — see the acceptable use policy. Carriers block it long before we notice, and it burns the range for everyone on it.
Calls & voicemail
Every line takes calls. Missed calls land in voicemail, which is recorded, transcribed and — when you have the summary add-on — condensed to one line.
curl https://api.nokycphone.com/v1/voicemails/vm_31d9f0a2 \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…"
# 200 OK
{
"id": "vm_31d9f0a2",
"number": "num_8f2c1a94",
"from": "+33612345678",
"duration": 34,
"language": "fr",
"transcript": "Bonjour, c'est le service livraison, votre colis arrive demain entre 9h et 11h.",
"translation": "Hello, this is the delivery service, your parcel arrives tomorrow between 9 and 11.",
"summary": "Delivery tomorrow, 09:00–11:00.",
"audio_url": "https://api.nokycphone.com/v1/voicemails/vm_31d9f0a2/audio",
"expires_at": "2026-10-21T09:14:02Z",
"received_at": "2026-09-21T09:31:44Z"
}audio_urlneeds the same bearer key and returnsaudio/ogg. It is not a public link.- Recordings and transcripts are deleted 30 days after they arrive, or the moment you delete them, whichever comes first.
- Transcription covers 12 languages and is picked per line with
voicemail_lang.
Balance & top-ups
There is no card on file and no invoice to pay. You hold a balance, in USD, funded by crypto deposits; every order and every renewal draws from it.
# open a one-time deposit address for 50 USD in Monero
curl -X POST https://api.nokycphone.com/v1/topups \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…" \
-H "Content-Type: application/json" \
-d '{"amount_usd":50,"coin":"XMR"}'
# 201 Created
{
"id": "top_4a91c7e2",
"coin": "XMR",
"address": "46BeWrHpwXm…",
"amount_crypto": "0.2841",
"amount_usd": 50.00,
"rate_locked_until": "2026-09-21T10:05:00Z",
"status": "waiting",
"confirmations_required": 10
}- The rate is held for 30 minutes. Pay late and we credit whatever the coin is worth on arrival, never less than what landed.
- Under-pay and the difference stays credited as a partial; over-pay and the extra is credited too. Nothing is returned to sender.
- Each address is used once. Reusing an old one is the fastest way to lose a deposit.
- Statuses:
waiting → seen → confirming → credited, orexpiredafter 3 hours with nothing seen.
Webhooks
Register an endpoint and stop polling. We POST a JSON body and expect a 2xx within 10 seconds; anything else is a failure and gets retried.
curl -X POST https://api.nokycphone.com/v1/webhooks \
-H "Authorization: Bearer nkp_live_9f2c8a41d0b7e5…" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.org/hooks/nkp","events":["message.received","call.missed"]}'
# 201 Created
{
"id": "whk_77c1e0",
"url": "https://example.org/hooks/nkp",
"events": ["message.received", "call.missed"],
"secret": "whsec_2b91f4c7a0d3…",
"created_at": "2026-09-21T09:40:00Z"
}Events
| Event | Fires when |
|---|---|
message.received | An SMS lands on one of your lines. Carries the extracted code. |
message.status | An outbound SMS moves to sent, delivered or failed. |
call.missed | A call was not answered. Voicemail follows separately. |
voicemail.ready | Recording and transcript are available. |
number.active | An ordered line has gone live. |
number.grace | A renewal failed for lack of balance; the countdown started. |
number.released | A line was released, by you or by expiry. |
topup.credited | A deposit confirmed and the balance moved. |
Payload and signature
POST /hooks/nkp HTTP/1.1
Content-Type: application/json
X-NKP-Event: message.received
X-NKP-Delivery: evt_6f0b28d4
X-NKP-Timestamp: 1789033541
X-NKP-Signature: v1=6b3a1f8e7c0d94aa5e2b…
{
"event": "message.received",
"created_at": "2026-09-21T09:45:41Z",
"data": {
"id": "msg_5b1f77c0",
"number": "num_8f2c1a94",
"e164": "+33647189022",
"from": "Telegram",
"body": "Your login code is 72194. Do not share it.",
"code": "72194",
"received_at": "2026-09-21T09:45:41Z"
}
}The signature is HMAC-SHA256 over timestamp + "." + raw_body, keyed with the endpoint secret, hex-encoded. Compare in constant time and reject anything older than five minutes.
import hmac, hashlib, time
def verify(secret, headers, raw_body):
ts = headers["X-NKP-Timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
sent = headers["X-NKP-Signature"].split("=", 1)[1]
mine = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(sent, mine)Retries
- Eight attempts over roughly 24 hours, with exponential backoff: 10 s, 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h.
- Deliveries are at-least-once. Deduplicate on
X-NKP-Delivery; the same ID is never two different events. - Order is not guaranteed. Use
created_atif sequence matters. - An endpoint that fails every attempt for three days is disabled, and the panel says so.
Answer first, work later. Return 200 as soon as you have stored the body, then process it. A slow handler looks like a failure at 10 seconds and gets the whole event replayed.
Rate limits
| Bucket | Limit | Scope |
|---|---|---|
Reads (GET) | 120 requests / minute | Per key |
Writes (POST, PATCH, DELETE) | 30 requests / minute | Per key |
| Ordering numbers | 10 / minute, 200 / day | Per account |
| Sending SMS | 60 / minute | Per line |
| Top-ups open at once | 3 | Per account |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 also carries Retry-After in seconds — honour it; hammering extends the window.
Libraries
There is no official SDK, on purpose: the surface is small enough that a wrapper would age worse than the HTTP calls. Here is the whole client in each language.
const nkp = (path, init = {}) =>
fetch("https://api.nokycphone.com/v1" + path, {
...init,
headers: { Authorization: `Bearer ${process.env.NKP_KEY}`, "Content-Type": "application/json", ...init.headers },
}).then(async (r) => {
const body = await r.json();
if (!r.ok) throw new Error(body.error.code + ": " + body.error.message);
return body;
});
const line = await nkp("/numbers", { method: "POST", body: JSON.stringify({ country: "fr", type: "mobile" }) });
console.log(line.e164);import os, requests
S = requests.Session()
S.headers["Authorization"] = "Bearer " + os.environ["NKP_KEY"]
BASE = "https://api.nokycphone.com/v1"
def nkp(method, path, **kw):
r = S.request(method, BASE + path, timeout=15, **kw)
if not r.ok:
e = r.json()["error"]
raise RuntimeError(f"{e['code']}: {e['message']}")
return r.json()
line = nkp("POST", "/numbers", json={"country": "fr", "type": "mobile"})
print(line["e164"])Changelog
v1 only ever gains fields and endpoints. Anything that would break a working integration ships as v2, and v1 keeps running for at least twelve months after that.
| Date | Change |
|---|---|
| 2026-07-14 | Added premium and /v1/numbers/available: pick a memorable number before ordering. |
| 2026-05-02 | Added the addons field and voicemail.ready now carries summary. |
| 2026-02-19 | Added has_code filter on messages. Extraction rewritten; code is now set on roughly 30% more messages. |
| 2025-11-06 | Webhook signatures moved to v1= prefixed HMAC with a timestamp. The old unprefixed header was accepted until 2026-02. |
| 2025-06-23 | Added starting_after pagination. Offset paging removed after six months of deprecation. |
| 2024-09-16 | v1 opened with the service. |
Build against a number that is
live in a minute.
Create an account, top up, order the first line from the API. No document changes hands at any point.