NoKYCPhone

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.

  • REST + JSON
  • One bearer key
  • Signed webhooks
  • 47 countries
  • Numbers live in ~60 s
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.

  1. Create an account in the panel and generate a key under API keys. Keys start with nkp_live_ and are shown once.
  2. Top up your balance in crypto. A number is charged from the balance, never from a card.
  3. Order a line. It is active within about a minute of the call returning.
  4. Read the inbox, or point a webhook at your own endpoint and stop polling.
POST /v1/numbers
# 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
}
GET /v1/numbers/{id}/messages
# 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 header
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.

ScopeCan doTypical use
readList and retrieve numbers, messages, calls, balance.Dashboards, monitoring, a bot that only reads codes.
writeEverything read can, plus send SMS, edit line settings, manage webhooks.The normal choice for an integration.
adminEverything 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.

GET /v1/messages
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.

402 Payment Required
{
  "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"
  }
}
StatusCodeWhat happened
400invalid_requestA parameter is missing or malformed. param names it.
401invalid_keyMissing, malformed or revoked key.
403insufficient_scopeThe key is valid but its scope does not cover this call.
403country_unavailableThat country is not open to new orders right now.
404not_foundNo such object, or it belongs to another account.
409number_releasedThe line has been released and cannot be used any more.
422unsupported_typeThat country has no line of the requested type.
402insufficient_balanceNot enough balance. Open a top-up and retry.
429rate_limitedToo many requests. Back off; see rate limits.
503temporarily_unavailableA dependency is down. Retry with backoff and check status.

Endpoint index

MethodPathDoes
GET/v1/countriesAvailability and price per country.
GET/v1/numbersList your lines.
POST/v1/numbersOrder 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}/renewRenew now for one period.
DELETE/v1/numbers/{id}Release the line immediately.
GET/v1/numbers/{id}/messagesInbox of one line.
GET/v1/messagesMessages across every line.
GET/v1/messages/{id}Retrieve one message.
POST/v1/messagesSend an SMS.
GET/v1/callsCall log.
GET/v1/voicemails/{id}Voicemail with transcript and audio URL.
GET/v1/balanceBalance and recent ledger entries.
POST/v1/topupsOpen a deposit address.
GET/v1/topups/{id}Follow a deposit.
GET/v1/webhooksList endpoints.
POST/v1/webhooksRegister 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.

GET /v1/countries
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.

FieldTypeNotes
countrystring, requiredLowercase ISO code, for example fr.
typestring, requiredmobile or landline.
periodinteger1, 3 or 12 months. Default 1. Quarterly takes 10% off, yearly 25%.
premiumbooleanAsk for a memorable pattern. Adds $4.90 a month.
e164stringReserve one specific number returned by /v1/numbers/available.
labelstringYour own name for the line, up to 40 characters.
auto_renewbooleanDefault true.
addonsarray of stringsAny of ai-pickup, ai-screen, ai-summary, ai-trans.
webhook_urlstringPer-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.

GET /v1/numbers/available
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

PATCH /v1/numbers/{id}
# 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}/renew charges one more period straight away and moves renews_at forward.
  • 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.grace on 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.

GET /v1/messages?has_code=true&since=2026-09-21T00:00:00Z
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.

POST /v1/messages
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.

GET /v1/voicemails/{id}
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_url needs the same bearer key and returns audio/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.

POST /v1/topups
# 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, or expired after 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.

POST /v1/webhooks
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

EventFires when
message.receivedAn SMS lands on one of your lines. Carries the extracted code.
message.statusAn outbound SMS moves to sent, delivered or failed.
call.missedA call was not answered. Voicemail follows separately.
voicemail.readyRecording and transcript are available.
number.activeAn ordered line has gone live.
number.graceA renewal failed for lack of balance; the countdown started.
number.releasedA line was released, by you or by expiry.
topup.creditedA deposit confirmed and the balance moved.

Payload and signature

POST to your endpoint
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.

verify.py
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_at if 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

BucketLimitScope
Reads (GET)120 requests / minutePer key
Writes (POST, PATCH, DELETE)30 requests / minutePer key
Ordering numbers10 / minute, 200 / dayPer account
Sending SMS60 / minutePer line
Top-ups open at once3Per 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.

node.mjs
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);
client.py
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.

DateChange
2026-07-14Added premium and /v1/numbers/available: pick a memorable number before ordering.
2026-05-02Added the addons field and voicemail.ready now carries summary.
2026-02-19Added has_code filter on messages. Extraction rewritten; code is now set on roughly 30% more messages.
2025-11-06Webhook signatures moved to v1= prefixed HMAC with a timestamp. The old unprefixed header was accepted until 2026-02.
2025-06-23Added starting_after pagination. Offset paging removed after six months of deprecation.
2024-09-16v1 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.