Sign in with NFT Number
Встройте вход через NFT‑номер в свой сервис за 5 минут. OTP-код летит в Telegram + Web Push + live в наш webapp, ответ — подписанный JWT id_token + refresh_token.
В проекте два разных API
Не путайте их — они решают разные задачи.
Быстрый старт
otp.request + otp.verify).⚡ Instant setup — одна команда на сервере
1. Copy-paste snippet в модалке
После создания интеграции открывается модальное окно с тремя табами: API key, Quick Start, Docs. В Quick Start выберите карточку вашей платформы — там уже подставлены ваши client_id, redirect_uri, discovery URL и (один раз) client_secret. Копируете в настройки — готово:
- 🛡️ oauth2-proxy — docker-compose, защищает ЛЮБОЙ веб-сервис (Grafana, Prometheus, внутренние дашборды и т.п.)
- 🦊 Gitea — одна команда CLI, без перезапуска
- 📊 Grafana — готовый блок для
grafana.ini - ☁️ Nextcloud — конфиг для OpenID Connect Login app
- 🟢 Node.js / Express — 40 строк с openid-client + PKCE
- 🐍 Python / FastAPI — authlib + starlette-session
- 🌐 curl — для отладки или языков без OIDC-библиотеки
- 🔌 Generic OIDC — все 4 URL для любой платформы (Keycloak, Auth0, AWS Cognito, Azure AD, Okta как identity broker и т.д.)
2. One-command installer
Максимально быстрый вариант. Одна команда на вашем сервере — ставит oauth2-proxy как systemd-сервис, пишет конфиг с нашими endpoint'ами:
bashexport NFT_CLIENT_SECRET='pk_live_...'
curl -fsSL https://phantomnumbers.org/integrate/<slug>.sh | bash
Нужно:
- Ubuntu / Debian с systemd, root
- Ваш
NFT_CLIENT_SECRETв env (скрипт его не хранит — тянет из окружения)
Что делает:
- Скачивает официальный
oauth2-proxyv7.6.0 (amd64/arm64) - Создаёт системного юзера
oauth2proxy - Пишет
/etc/oauth2-proxy.cfg+ systemd unit, запускает - Слушает на
0.0.0.0:4180, форвардит вUPSTREAM(по умолчаниюlocalhost:8080, можно переопределить env)
После этого ставите nginx / Caddy → 127.0.0.1:4180 — и ваш сервис за логином NFT Number.
3. Docker-compose
Если у вас docker-окружение:
bashcurl -fsSL "https://phantomnumbers.org/integrate/<slug>/oauth2-proxy.yml" > docker-compose.yml
# отредактируйте OAUTH2_PROXY_UPSTREAMS под ваш сервис
export NFT_CLIENT_SECRET='pk_live_...'
export COOKIE_SECRET="$(openssl rand -base64 32)"
docker compose up -d
- Ваш
client_secretникогда не попадает в URL — только в env переменных или в ссылках в браузере владельца интеграции (страница создания, ротации). После первого показа — только SHA-256 hash в нашей БД. - Redirect URIs из
allowed_redirect_uris— обязательное full-match совпадение. - Installer-скрипт — плоский bash, ~90 строк — можно прочитать перед запуском:
curl -fsSL https://phantomnumbers.org/integrate/<slug>.sh | less
Как это работает
Пользователь вводит свой NFT-номер (формат 111-22-101-18) на вашем сайте. Вы вызываете наш /api/request-code с его номером и вашим API-ключом. Мы:
- находим владельца в нашей БД;
- генерируем 6-значный OTP (TTL 10 мин);
- доставляем по трём каналам: Telegram-бот, Web Push, live-уведомление в открытом webapp;
- в сообщении всегда указано имя вашей интеграции.
Пользователь вводит код у вас, вы вызываете /api/verify-code — мы возвращаем id_token и опционально refresh_token.
API endpoints
Два способа встроить вход — выбирайте под себя:
| Group | Method | Path | Scope | Описание |
|---|---|---|---|---|
| Простой request/verify |
POST | /api/request-code | otp.request | Сгенерировать OTP для номера |
| POST | /api/verify-code | otp.verify | Проверить OTP, получить id_token + refresh_token | |
| POST | /api/refresh-token | otp.verify | Обменять refresh_token на новый id_token | |
| OAuth 2.0 / OIDC стандартный flow |
GET | /api/authorize | — | Authorization endpoint (redirect flow, consent-страница) |
| POST | /api/token | — | Обмен code → access_token+id_token+refresh_token | |
| GET | /api/userinfo | — | Claims пользователя по Bearer access_token | |
| GET | /.well-known/openid-configuration | — | OIDC discovery (auto-config для Gitea / Grafana / oauth2-proxy) | |
| Identity read-only |
GET | /api/identity/user/{phone} | user.identity | Прочитать данные пользователя (после недавнего verify) |
| GET | /api/integrations/me | any | Self-test: увидеть свою интеграцию по ключу | |
| GET | /.well-known/nft-numbers.jwks | — | Публичный ключ для RS256 |
Заголовок аутентификации для простого API: X-API-Key: pk_live_.... Для OAuth2 — см. раздел ниже.
OAuth 2.0 / OpenID Connect
Стандартный authorization-code flow. Подходит когда вы не хотите принимать номер и OTP у себя на фронте — мы показываем consent-страницу сами, возвращаем code, вы обмениваете его на токены.
https://phantomnumbers.org/.well-known/openid-configuration. Всё остальное (scopes, endpoints, signing keys) они возьмут сами.
Шаги flow
allowed_redirect_uris (полное совпадение, включая схему + хост + порт + путь).code_verifier = случайная строка 43–128 символов, code_challenge = BASE64URL(SHA-256(code_verifier)) без padding. Сохраните code_verifier на сервере/клиенте, он понадобится на шаге 4.https://phantomnumbers.org/api/authorize
?response_type=code
&client_id=<ваш slug>
&redirect_uri=<URL в allowed_redirect_uris>
&scope=openid%20profile%20email
&state=<случайная строка для CSRF>
&nonce=<случайная строка для id_token>
&code_challenge=<BASE64URL(SHA-256(verifier))>
&code_challenge_method=S256
<redirect_uri>?code=ac_...&state=<ваш state>. Code живёт 60 секунд, используется один раз.code_verifier обязателен — без него сервер вернёт invalid_grant:
POST /api/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=ac_...
&redirect_uri=<тот же URI>
&client_id=<slug>
&client_secret=<ваш API key pk_live_...>
&code_verifier=<сохранённый на шаге 2>
{access_token, id_token, refresh_token, token_type: Bearer, expires_in, scope}. access_token — JWT, живёт 1 час. refresh_token — 30 дней.GET /api/userinfo
Authorization: Bearer <access_token>
→ {"sub":"123","email":"[email protected]",
"nft_number":"1112210118","preferred_username":"1112210118",
"name":"...","ton_wallet":"0:..."}
PKCE — обязателен для всех клиентов
С апреля 2026 PKCE (RFC 7636, method=S256) требуется на /api/authorize для всех приложений — и backend-интеграций с client_secret, и SPA без секрета. Это закрывает атаку «украденный auth-code → обмен на токен одним лишь client_id». Минимальный TypeScript-snippet:
const verifier = crypto.randomUUID().replace(/-/g, '') + crypto.randomUUID().replace(/-/g, '');
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
sessionStorage.setItem('pkce_verifier', verifier);
location.assign(`/api/authorize?...&code_challenge=${challenge}&code_challenge_method=S256`);
Что получит ваш сервис в userinfo
| Claim | Scope | Пример |
|---|---|---|
sub | всегда | "123" — наш user_id (стабильный) |
nft_number | openid / user.identity | "1112748829" |
preferred_username | openid | "1112748829" |
email | "[email protected]" — синтетический, стабильный | |
email_verified | true | |
name, picture | profile | display name пользователя |
ton_wallet | user.identity.wallet | "0:abc..." |
telegram_username | user.identity.telegram | "ivan" |
Готовые клиенты, протестированные с нами
- Gitea — Site Administration → Authentication Sources → Add → OAuth2 provider =
OpenID Connect, Discovery URL = наш openid-configuration. 3 минуты настройки. - oauth2-proxy — провайдер
oidc, указать--oidc-issuer-url=https://phantomnumbers.org. Можно защитить любой non-OIDC сервис за этим прокси. - Grafana — Generic OAuth в
[auth.generic_oauth], указать endpoints из discovery.
id_token: HS256 или RS256
Алгоритм подписи выбирается при создании интеграции:
- HS256 — shared secret. Секрет = ваш API-ключ (он уже есть у обеих сторон). Самый простой вариант: одной функцией подписал, одной проверил. Ключ обязан оставаться на бэкенде.
- RS256 — асимметричный. Мы подписываем приватным ключом, вы валидируете публичным из
/.well-known/nft-numbers.jwks. Совместимо с любыми OIDC-библиотеками, подходит для раздачи токенов в мобильные приложения.
Payload: {iss, sub, aud, iat, exp, number, telegram_username?, wallet?, scope?}. TTL — 1 час (настраивается env ID_TOKEN_LIFETIME_SEC).
Refresh tokens
Вместе с id_token возвращается refresh_token (30 дней). Обменять на новую пару:
curlcurl -X POST https://phantomnumbers.org/api/refresh-token \
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"refresh_token":"rt_..."}'
На каждом обмене старый refresh инвалидируется, выдаётся новый (rotation). Выдачу refresh можно отключить в настройках интеграции.
Webhooks
Укажите webhook_url в настройках интеграции — на него будут POST'иться события:
login.requested— OTP сгенерированlogin.verified— OTP подтверждёнlogin.denied— пользователь нажал «Это не я»integration.suspended/integration.resumed
Подпись в заголовке X-NFT-Numbers-Signature: sha256=<hmac>. Всегда проверяйте подпись до обработки payload.
Пример на JavaScript
pk_live_* во frontend JS. Любой посетитель сможет его прочитать и минтить токены от вашего имени.
Frontend (вызывает ваш бэкенд)
html<button id="loginBtn">Sign in with NFT Number</button>
<script>
document.getElementById('loginBtn').addEventListener('click', async () => {
const phone = prompt("Ваш NFT-номер (111-22-101-18):");
await fetch("/auth/nft/request", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ phone })
});
const code = prompt("Проверьте Telegram или webapp. Введите код:");
const r = await fetch("/auth/nft/verify", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ phone, code })
});
if (r.ok) location.reload();
else alert("Неверный код");
});
</script>
Backend (Node.js / Express)
javascriptconst API = "https://phantomnumbers.org";
const KEY = process.env.NFT_NUMBERS_API_KEY;
app.post("/auth/nft/request", async (req, res) => {
const r = await fetch(API + "/api/request-code", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": KEY },
body: JSON.stringify({ phone: req.body.phone })
});
res.status(r.status).json(await r.json());
});
app.post("/auth/nft/verify", async (req, res) => {
const r = await fetch(API + "/api/verify-code", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": KEY },
body: JSON.stringify({ phone: req.body.phone, code: req.body.code })
});
const data = await r.json();
if (!data.success) return res.status(401).json(data);
req.session.userId = data.user.id;
req.session.nftNumber = data.user.number;
res.json({ ok: true });
});
Пример на Python
pythonimport os, httpx, jwt
API = "https://phantomnumbers.org"
KEY = os.environ["NFT_NUMBERS_API_KEY"]
async def request_code(phone: str) -> None:
async with httpx.AsyncClient() as c:
r = await c.post(f"{API}/api/request-code",
headers={"X-API-Key": KEY},
json={"phone": phone})
r.raise_for_status()
async def verify(phone: str, code: str) -> dict | None:
async with httpx.AsyncClient() as c:
r = await c.post(f"{API}/api/verify-code",
headers={"X-API-Key": KEY},
json={"phone": phone, "code": code})
if r.status_code != 200:
return None
j = r.json()
if not j.get("success"):
return None
# HS256 (shared secret = API-ключ):
claims = jwt.decode(
j["id_token"], key=KEY,
algorithms=["HS256"],
audience="your-integration-slug",
)
return {"user": j["user"], "claims": claims,
"refresh_token": j.get("refresh_token")}
curl
bash# Запросить OTP
curl -X POST https://phantomnumbers.org/api/request-code \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_live_..." \
-d '{"phone":"1112210118"}'
# Подтвердить OTP → получить id_token
curl -X POST https://phantomnumbers.org/api/verify-code \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_live_..." \
-d '{"phone":"1112210118","code":"482193"}'
Безопасность и rate-limits
- Per-integration: по умолчанию 100 req/h (настраивается админом). Sandbox — 10/h.
- Per-phone recipient: 3 OTP в час на номер (защита владельца от спама).
- Per-IP: глобальный throttle.
- Abuse score: каждый «Это не я» инкрементит счётчик; при достижении threshold (5 по умолчанию) — авто-suspend.
- Scope escalation:
user.identity.wallet/user.identity.telegramвыдаются только после одобрения админа. - Identity window:
GET /api/identity/user/{phone}требует успешный verify для этой пары (integration, phone) за последние 30 мин. - Redirect URI: если в настройках интеграции заполнен
allowed_redirect_uris, передать можно только exact-match URI.
Billing
Платформа может быть бесплатной, по подписке, pay-per-verification или pay-per-RPM — режим выбирает админ в Platform Settings. В платном режиме в Developer Panel появляется кнопка Pay with TON: открывается deep-link ton://transfer/<wallet>, после on-chain подтверждения баланс пополняется автоматически.
Следующий шаг
Открыть Developer Panel → + New Integration — получите первый API-ключ за минуту.
Sign in with NFT Number
Ship NFT-Number auth in 5 minutes. OTP over Telegram + Web Push + in-webapp live panel; you get back a signed JWT id_token + refresh_token.
Two different APIs in this project
Don't mix them up — they solve different problems.
Quick start
otp.request + otp.verify).⚡ Instant setup — one command on your server
1. Copy-paste snippet in the modal
After creating an integration, the modal has three tabs: API key, Quick Start, Docs. On Quick Start, pick the chip for your platform — the snippet is pre-filled with your client_id, redirect_uri, discovery URL and (once) client_secret. Copy, paste into your service config — done:
- 🛡️ oauth2-proxy — docker-compose, protects ANY web service (Grafana, Prometheus, internal dashboards, etc.)
- 🦊 Gitea — one CLI command, no restart
- 📊 Grafana — ready
grafana.iniblock - ☁️ Nextcloud — config for the OpenID Connect Login app
- 🟢 Node.js / Express — 40 lines with openid-client + PKCE
- 🐍 Python / FastAPI — authlib + starlette-session
- 🌐 curl — for debugging or languages without an OIDC library
- 🔌 Generic OIDC — all 4 URLs for any platform (Keycloak, Auth0, AWS Cognito, Azure AD, Okta as identity broker, …)
2. One-command installer
Fastest option. A single command on your server — installs oauth2-proxy as a systemd service with our endpoints pre-configured:
bashexport NFT_CLIENT_SECRET='pk_live_...'
curl -fsSL https://phantomnumbers.org/integrate/<slug>.sh | bash
Requirements:
- Ubuntu / Debian with systemd, root
NFT_CLIENT_SECRETin env (the script never carries it — pulls from your env)
What it does:
- Downloads the official
oauth2-proxyv7.6.0 (amd64/arm64) - Creates a system user
oauth2proxy - Writes
/etc/oauth2-proxy.cfg+ a systemd unit, starts it - Listens on
0.0.0.0:4180, forwards toUPSTREAM(defaultlocalhost:8080, override via env)
Then point nginx / Caddy at 127.0.0.1:4180 — your service is now behind NFT Number login.
3. Docker Compose
For a container setup:
bashcurl -fsSL "https://phantomnumbers.org/integrate/<slug>/oauth2-proxy.yml" > docker-compose.yml
# Edit OAUTH2_PROXY_UPSTREAMS to your service
export NFT_CLIENT_SECRET='pk_live_...'
export COOKIE_SECRET="$(openssl rand -base64 32)"
docker compose up -d
- Your
client_secretnever appears in a URL — only in env vars or as a one-time reveal inside the integration owner's browser (create / rotate page). After that we keep only the SHA-256 hash. - Redirect URIs from
allowed_redirect_uris— mandatory full-match. - Installer script is plain bash, ~90 lines — feel free to read before running:
curl -fsSL https://phantomnumbers.org/integrate/<slug>.sh | less
How it works
Your user enters their NFT number (format 111-22-101-18) on your page. You call our /api/request-code with the number and your API key. We:
- look up the owner in our DB;
- generate a 6-digit OTP (10 min TTL);
- deliver it over three channels: Telegram bot, Web Push, live webapp notification;
- the message always names your integration.
The user types the code on your page, you call /api/verify-code — we return a signed id_token and optionally a refresh_token.
API endpoints
Two ways to integrate — pick the one that fits:
| Group | Method | Path | Scope | Description |
|---|---|---|---|---|
| Simple request/verify |
POST | /api/request-code | otp.request | Generate OTP for a number |
| POST | /api/verify-code | otp.verify | Verify OTP, receive id_token + refresh_token | |
| POST | /api/refresh-token | otp.verify | Exchange refresh for fresh id_token | |
| OAuth 2.0 / OIDC standard flow |
GET | /api/authorize | — | Authorization endpoint (redirect flow with consent page) |
| POST | /api/token | — | Exchange code → access_token+id_token+refresh_token | |
| GET | /api/userinfo | — | User claims via Bearer access_token | |
| GET | /.well-known/openid-configuration | — | OIDC discovery (auto-config for Gitea / Grafana / oauth2-proxy) | |
| Identity read-only |
GET | /api/identity/user/{phone} | user.identity | Read user data (after recent verify) |
| GET | /api/integrations/me | any | Self-test: inspect your own integration | |
| GET | /.well-known/nft-numbers.jwks | — | Public key for RS256 |
Auth header for the simple API: X-API-Key: pk_live_.... For OAuth2 — see below.
OAuth 2.0 / OpenID Connect
Standard authorization-code flow. Pick this when you don't want to handle the NFT number + OTP on your own page — we render the consent page, you exchange the returned code for tokens.
https://phantomnumbers.org/.well-known/openid-configuration. The client pulls endpoints, scopes, and signing keys automatically.
Flow steps
allowed_redirect_uris (full match on scheme + host + port + path).code_verifier = 43–128 random chars, code_challenge = BASE64URL(SHA-256(code_verifier)) without padding. Keep code_verifier on your server/client — you'll need it in step 5.https://phantomnumbers.org/api/authorize
?response_type=code
&client_id=<your slug>
&redirect_uri=<URI from allowed_redirect_uris>
&scope=openid%20profile%20email
&state=<random CSRF string>
&nonce=<random nonce for id_token>
&code_challenge=<BASE64URL(SHA-256(verifier))>
&code_challenge_method=S256
<redirect_uri>?code=ac_...&state=<your state>. Code lives 60 seconds, single-use.code_verifier is required — missing it returns invalid_grant:
POST /api/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=ac_...
&redirect_uri=<same URI>
&client_id=<slug>
&client_secret=<your API key pk_live_...>
&code_verifier=<the one saved in step 2>
{access_token, id_token, refresh_token, token_type: Bearer, expires_in, scope}. access_token is a JWT, 1h TTL. refresh_token — 30 days.GET /api/userinfo
Authorization: Bearer <access_token>
→ {"sub":"123","email":"[email protected]",
"nft_number":"1112210118","preferred_username":"1112210118",
"name":"...","ton_wallet":"0:..."}
PKCE — mandatory for every client
As of April 2026 PKCE (RFC 7636, method=S256) is required on /api/authorize for every integration — backends with a client_secret and browser-only SPAs alike. This closes the "stolen auth-code → token exchange with only the client_id" attack. Minimal TypeScript helper:
const verifier = crypto.randomUUID().replace(/-/g, '') + crypto.randomUUID().replace(/-/g, '');
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
sessionStorage.setItem('pkce_verifier', verifier);
location.assign(`/api/authorize?...&code_challenge=${challenge}&code_challenge_method=S256`);
Claims you receive in userinfo
| Claim | Scope | Example |
|---|---|---|
sub | always | "123" — our user_id (stable) |
nft_number | openid / user.identity | "1112748829" |
preferred_username | openid | "1112748829" |
email | "[email protected]" — synthetic, stable | |
email_verified | true | |
name, picture | profile | user display name |
ton_wallet | user.identity.wallet | "0:abc..." |
telegram_username | user.identity.telegram | "ivan" |
Off-the-shelf clients tested with us
- Gitea — Site Administration → Authentication Sources → Add → OAuth2 provider =
OpenID Connect, Discovery URL = our openid-configuration. 3 minutes to set up. - oauth2-proxy — provider
oidc, point--oidc-issuer-url=https://phantomnumbers.org. Protects any non-OIDC service behind this proxy. - Grafana — Generic OAuth in
[auth.generic_oauth], endpoints from discovery.
id_token: HS256 or RS256
- HS256 — shared secret. Secret = your API key (already on both ends). Simplest option: one function to sign, one to verify. The key must stay on the backend.
- RS256 — asymmetric. We sign with a private key; you verify with the public key from
/.well-known/nft-numbers.jwks. Compatible with any OIDC library, fine for mobile apps.
Payload: {iss, sub, aud, iat, exp, number, telegram_username?, wallet?, scope?}. TTL — 1 hour (env ID_TOKEN_LIFETIME_SEC).
Refresh tokens
A refresh_token (30-day TTL) comes back alongside the id_token. Exchange it for a fresh pair:
curlcurl -X POST https://phantomnumbers.org/api/refresh-token \
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"refresh_token":"rt_..."}'
Each exchange rotates the token — the old one is revoked, a new one is issued. You can disable refresh emission per integration.
Webhooks
Set webhook_url on your integration — we POST events to you:
login.requestedlogin.verifiedlogin.deniedintegration.suspended/integration.resumed
Signature: X-NFT-Numbers-Signature: sha256=<hmac>. Always verify before processing the payload.
JavaScript example
pk_live_* in frontend JS. Any visitor can read it and mint tokens in your name.
Frontend (calls your backend)
html<button id="loginBtn">Sign in with NFT Number</button>
<script>
document.getElementById('loginBtn').addEventListener('click', async () => {
const phone = prompt("Your NFT number (111-22-101-18):");
await fetch("/auth/nft/request", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ phone })
});
const code = prompt("Check Telegram or the webapp. Enter the code:");
const r = await fetch("/auth/nft/verify", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ phone, code })
});
if (r.ok) location.reload();
else alert("Invalid code");
});
</script>
Backend (Node.js / Express)
javascriptconst API = "https://phantomnumbers.org";
const KEY = process.env.NFT_NUMBERS_API_KEY;
app.post("/auth/nft/request", async (req, res) => {
const r = await fetch(API + "/api/request-code", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": KEY },
body: JSON.stringify({ phone: req.body.phone })
});
res.status(r.status).json(await r.json());
});
app.post("/auth/nft/verify", async (req, res) => {
const r = await fetch(API + "/api/verify-code", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": KEY },
body: JSON.stringify({ phone: req.body.phone, code: req.body.code })
});
const data = await r.json();
if (!data.success) return res.status(401).json(data);
req.session.userId = data.user.id;
req.session.nftNumber = data.user.number;
res.json({ ok: true });
});
Python example
pythonimport os, httpx, jwt
API = "https://phantomnumbers.org"
KEY = os.environ["NFT_NUMBERS_API_KEY"]
async def verify(phone, code):
async with httpx.AsyncClient() as c:
r = await c.post(f"{API}/api/verify-code",
headers={"X-API-Key": KEY},
json={"phone": phone, "code": code})
j = r.json()
if not j.get("success"): return None
claims = jwt.decode(j["id_token"], key=KEY,
algorithms=["HS256"], audience="your-slug")
return {"user": j["user"], "claims": claims}
curl
bash# Request OTP
curl -X POST https://phantomnumbers.org/api/request-code \
-H "Content-Type: application/json" -H "X-API-Key: pk_live_..." \
-d '{"phone":"1112210118"}'
# Verify OTP → get id_token
curl -X POST https://phantomnumbers.org/api/verify-code \
-H "Content-Type: application/json" -H "X-API-Key: pk_live_..." \
-d '{"phone":"1112210118","code":"482193"}'
Security & rate-limits
- Per-integration: default 100 req/h (admin-configurable). Sandbox — 10/h.
- Per-phone recipient: 3 OTPs per hour per number (owner flood protection).
- Per-IP: global throttle.
- Abuse score: each «It wasn't me» bumps a counter; at the threshold (default 5) — auto-suspend.
- Scope escalation:
user.identity.wallet/user.identity.telegramrequire admin approval. - Identity window:
GET /api/identity/user/{phone}requires a successful verify for the (integration, phone) pair in the last 30 minutes. - Redirect URI: if
allowed_redirect_urisis populated, the caller must pass an exact match.
Billing
The platform can be free, flat-subscription, pay-per-verification, or pay-per-RPM — the admin picks a mode in Platform Settings. In a paid mode the Developer Panel shows a Pay with TON button opening a ton://transfer/<wallet> deep-link; the balance is credited automatically once the transfer is confirmed on-chain.
Next step
Open the Developer Panel → + New Integration and get your first API key in a minute.