NoKYCPhone

Developers

Automating SMS with
webhooks in ten minutes.

Polling an inbox every five seconds works until it does not. This is the same job done properly: a line ordered from code, a webhook registered, a signature verified, and a handler that survives a replay.

  • 9 min read
  • Updated 21 May 2026
  • No signup needed to read it
A green packet arcing between a pane of glass and a server rack, with fainter repeat arcs

Before you start

Three things, five minutes.

  1. An account and a balance. Create the access key, top up in crypto. Ordering spends the balance, so there has to be one.
  2. An API key with admin scope. Ordering numbers spends money, which read and write keys cannot do. Create it under API keys; it is shown once.
  3. An HTTPS endpoint we can reach. Plain HTTP is refused. For local development, any tunnelling tool works.

Keep the key out of the repository. An admin key can order numbers until the balance is empty. Environment variable, secret manager, anything but a committed file — and if one leaks, revoke it in the panel before doing anything else.

Step 1 — order a line

One call. It charges the balance and returns once the allocation is confirmed; the line starts receiving within about a minute.

POST /v1/numbers
curl -X POST https://api.nokycphone.com/v1/numbers \
  -H "Authorization: Bearer $NKP_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"country":"de","type":"mobile","period":1,"label":"signup-bot"}'

# 201 Created
{
  "id": "num_8f2c1a94",
  "e164": "+4915735550088",
  "status": "active",
  "renews_at": "2026-10-21T09:14:02Z",
  "charged": { "monthly": 12.90, "activation": 10.00, "total": 22.90 }
}

Always send an Idempotency-Key. A timeout on your side is indistinguishable from a failure. With the header, a retry replays the stored response instead of ordering a second number; we keep it for 24 hours.

Step 2 — register the webhook

Register once per endpoint, then stop polling. The secret comes back in this response and never again, so store it immediately.

POST /v1/webhooks
curl -X POST https://api.nokycphone.com/v1/webhooks \
  -H "Authorization: Bearer $NKP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.org/hooks/nkp","events":["message.received"]}'

# 201 Created
{
  "id": "whk_77c1e0",
  "url": "https://example.org/hooks/nkp",
  "events": ["message.received"],
  "secret": "whsec_2b91f4c7a0d3…"
}

Subscribe only to what you handle. The events worth knowing about for this job:

  • message.received — an SMS landed, with the code already extracted.
  • number.active — an ordered line has gone live.
  • number.grace — a renewal failed for lack of balance. Alert on this one; it is 3 days' warning before the line disappears.
  • topup.credited — a deposit confirmed.

Step 3 — verify the signature

An endpoint that accepts anything posted to it is not a webhook, it is a public write API. Verify before you trust the body.

The signature is HMAC-SHA256 over timestamp + "." + raw_body, keyed with the endpoint secret, hex-encoded, in the X-NKP-Signature header as v1=….

handler.py
import hmac, hashlib, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["NKP_WEBHOOK_SECRET"].encode()
seen = set()   # en production : Redis, avec une expiration

@app.post("/hooks/nkp")
def hook():
    raw = request.get_data()                      # le corps BRUT, jamais re-serialise
    ts  = request.headers.get("X-NKP-Timestamp", "")
    sig = request.headers.get("X-NKP-Signature", "").split("=", 1)[-1]

    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        abort(400)                                 # trop vieux : rejeu
    mine = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(mine, sig):
        abort(401)

    delivery = request.headers["X-NKP-Delivery"]
    if delivery in seen:
        return "", 200                             # deja traite : on acquitte quand meme
    seen.add(delivery)

    event = request.get_json()
    if event["event"] == "message.received":
        enqueue(event["data"])                     # traiter APRES avoir repondu
    return "", 200

Three things that matter in that handler, and each one is a real outage if you skip it:

  • Sign the raw body. Parsing and re-serialising JSON changes the bytes and breaks the HMAC.
  • Use a constant-time comparison. == on a signature leaks timing.
  • Reject old timestamps. Without the five-minute window, a captured request replays forever.

What you receive

POST to your endpoint
POST /hooks/nkp HTTP/1.1
X-NKP-Event: message.received
X-NKP-Delivery: evt_6f0b28d4
X-NKP-Timestamp: 1789033541
X-NKP-Signature: v1=6b3a1f8e7c0d94aa5e2b…
Content-Type: application/json

{
  "event": "message.received",
  "created_at": "2026-09-21T09:45:41Z",
  "data": {
    "id": "msg_5b1f77c0",
    "number": "num_8f2c1a94",
    "e164": "+4915735550088",
    "from": "Telegram",
    "body": "Your login code is 72194. Do not share it.",
    "code": "72194",
    "received_at": "2026-09-21T09:45:41Z"
  }
}

code is extracted server-side with the same rules the panel uses: digit groups of four to eight characters, spaces and dashes stripped, the sender's own number ignored. When nothing looks like a code it is null and body is still there for you to parse yourself.

Retries, ordering and the things that bite

  • Delivery is at-least-once. Deduplicate on X-NKP-Delivery. The same ID is never two different events.
  • Order is not guaranteed. Two messages a second apart can arrive in either order. Sort on created_at if sequence matters.
  • Ten seconds to respond. A slower handler counts as a failure and the event is replayed. Acknowledge first, work afterwards.
  • Eight attempts over roughly a day, backing off 10 s, 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h.
  • Three days of total failure disables the endpoint, and the panel says so. Nothing is lost: the messages are still readable on the API.

Keep a polling fallback. Webhooks are the fast path, not the source of truth. A cheap GET /v1/messages?since=… every few minutes catches anything your endpoint missed while it was down, and costs nothing when there is nothing to catch.

Step 4 — keep it alive

An automation that orders numbers and never releases them quietly drains the balance. Two habits:

  • Release what you finished with. DELETE /v1/numbers/{id} is immediate and stops the renewal. The messages go with it, so read them first.
  • Watch number.grace. It is the only warning before a line releases itself, and it comes 3 days ahead.
sweep.sh
# release every line labelled "signup-bot" that has been idle for a week
curl -s "https://api.nokycphone.com/v1/numbers?label=signup-bot" \
     -H "Authorization: Bearer $NKP_KEY" |
jq -r '.data[] | select(.last_message_at < (now - 604800 | todate)) | .id' |
while read -r id; do
  curl -s -X DELETE "https://api.nokycphone.com/v1/numbers/$id" \
       -H "Authorization: Bearer $NKP_KEY"
done

Rate limits sit at 120 reads and 30 writes per minute per key, with ordering capped separately. Every response carries X-RateLimit-Remaining; honour Retry-After on a 429 rather than hammering. The full numbers are in the API reference.

Questions

Do I need webhooks, or is polling fine?

Polling is fine at small scale and simpler to debug. Webhooks arrive within a second instead of within a polling interval, and they cost nothing when nothing happens. Most people end up with both: webhooks for speed, a slow poll as a safety net.

Why is my signature check failing?

Almost always because the body was parsed and re-serialised before hashing. Sign the raw bytes exactly as received. The second most common cause is hashing the body alone instead of timestamp, a dot, then the body.

Can I have different webhooks per number?

Yes. A per-line webhook URL set with PATCH on the number overrides the account-wide endpoint, which is the clean way to route different automations to different services.

What happens if my server is down?

We retry eight times over about a day. After three days of total failure the endpoint is disabled and the panel says so. No message is lost either way — they are all still readable on the API.

Is there an official SDK?

No, deliberately. The surface is small enough that a wrapper would age worse than the HTTP calls. The API reference has the whole client in Node and Python, about fifteen lines each.


Written by the people who run the service, updated 21 May 2026. If something here is wrong or out of date, tell us — we fix guides faster than we fix code.

Four calls,
then it runs itself.

Create a key in the panel and order the first line from your own code. No document changes hands at any point.