·
API
Техническая документация закрытого партнёрского API системы аутентификации TOKEN PAY ID. Описывает OAuth 2.0 / PKCE flow, native SDK, webhooks и эндпоинты, доступные утверждённым интеграторам.
client_id выдаются только после согласования интеграции. Чтобы получить доступ, напишите на dev@tokenpay.space с описанием проекта, целевой аудитории и ожидаемой нагрузки. Документация ниже доступна публично как справочник по контракту — для реальных запросов понадобятся выданные учётные данные.
https://id.tokenpay.space/api/v1
API использует JSON для всех запросов и ответов. Все запросы должны содержать заголовок Content-Type: application/json.
Как получить доступ
1. Написать на dev@tokenpay.space — укажите название проекта, домен, сценарий интеграции и контактное лицо
2. Пройти короткое ревью (обычно 1–3 рабочих дня)
3. После одобрения получить client_id, client_secret и доступ к личному кабинету партнёра
4. Настроить OAuth redirect-URI и использовать API как описано ниже
Быстрый старт (после одобрения)
curl -X GET https://id.tokenpay.space/api/v1/users/me \ -H "Authorization: Bearer tpid_sk_your_secret_key" \ -H "Content-Type: application/json"
Аутентификация
TOKEN PAY ID API поддерживает два метода аутентификации:
API ключи
Используйте секретный ключ (tpid_sk_...) в заголовке Authorization:
Authorization: Bearer tpid_sk_ваш_секретный_ключ
OAuth 2.0 Bearer Token
Для пользовательских запросов используйте access_token полученный через OAuth 2.0 flow:
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Лимиты запросов
API имеет следующие ограничения:
| Тарифный план | Запросов/мин | Запросов/день |
|---|---|---|
| Free | 60 | 10,000 |
| Pro | 300 | 100,000 |
| Enterprise | 1,000 | Unlimited |
Заголовки ответа содержат информацию о лимитах:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 45 X-RateLimit-Reset: 1678886400
Коды ошибок
API возвращает стандартные HTTP коды и JSON-объект ошибки:
{
"error": {
"code": "invalid_token",
"message": "The access token is invalid or expired",
"status": 401
}
}
| Код | Описание |
|---|---|
| 400 | Некорректный запрос |
| 401 | Неавторизован — неверный или отсутствующий токен |
| 403 | Запрещено — недостаточно прав |
| 404 | Ресурс не найден |
| 429 | Превышен лимит запросов |
| 500 | Внутренняя ошибка сервера |
OAuth 2.0 — Авторизация
TOKEN PAY ID поддерживает стандартный OAuth 2.0 Authorization Code flow для безопасной авторизации пользователей в сторонних сервисах.
Шаг 1: Redirect к авторизации
Перенаправьте пользователя на страницу авторизации:
https://id.tokenpay.space/api/v1/oauth/authorize? client_id=tpid_pk_your_public_key& redirect_uri=https://yourapp.com/callback& response_type=code& scope=profile email& state=random_csrf_token& prompt=login& code_challenge=SHA256_HASH& code_challenge_method=S256
code_challenge и code_challenge_method=S256 являются обязательными для всех клиентов. Запросы без PKCE будут отклонены.
| Параметр | Тип | Описание |
|---|---|---|
| client_id | string | required — Публичный ключ |
| redirect_uri | string | required — URL для редиректа |
| response_type | string | required — Только code |
| scope | string | Запрашиваемые разрешения (по умолч. profile) |
| state | string | CSRF-токен (рекомендуется) |
| prompt | string | login — принудительная переавторизация, consent, none |
| login_hint | string | Email для предзаполнения формы входа |
| code_challenge | string | required — PKCE challenge (SHA-256 хеш code_verifier) |
| code_challenge_method | string | required — Только S256 |
Шаг 2: Обмен кода на токен
После авторизации пользователь будет перенаправлен на ваш redirect_uri с параметром code. Обменяйте его на access_token:
POST /oauth/token
Обмен authorization code на access token.
| Параметр | Тип | Описание |
|---|---|---|
| grant_type | string | required — authorization_code или refresh_token |
| code | string | required — Authorization code |
| client_id | string | required — Ваш публичный ключ |
| client_secret | string | Секретный ключ (обязателен для confidential-клиентов, не нужен для public + PKCE) |
| redirect_uri | string | required — Должен совпадать с исходным |
| code_verifier | string | required — PKCE verifier (случайная строка 43–128 символов, из которой был создан code_challenge) |
curl -X POST https://id.tokenpay.space/api/v1/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "authorization_code", "code": "abc123...", "client_id": "tpid_pk_...", "client_secret": "tpid_sk_...", "redirect_uri": "https://yourapp.com/callback", "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" }'
Ответ
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tpid_rt_8a7b6c5d...",
"scope": "profile email",
"user": {
"id": "tpid_usr_a1b2c3d4",
"email": "user@example.com",
"name": "Alex Example",
"email_verified": true
}
}
POST /oauth/revoke
Отзыв access или refresh токена.
| Параметр | Тип | Описание |
|---|---|---|
| token | string | required — Токен для отзыва |
| token_type_hint | string | access_token или refresh_token |
GET /oauth/userinfo
Получить профиль пользователя по OAuth access token. Возвращает данные в зависимости от запрошенного scope.
curl -X GET https://tokenpay.space/api/v1/oauth/userinfo \ -H "Authorization: Bearer ACCESS_TOKEN"
{
"id": "tpid_usr_a1b2c3d4e5f6",
"email": "user@example.com",
"name": "Alex Example",
"role": "user",
"email_verified": true,
"locale": "ru",
"theme": "dark",
"telegram_linked": true,
"cupol_balance": 150,
"cupol_subscription_active": true,
"created_at": "2026-01-15T12:00:00Z"
}
POST /oauth/cancel
Уведомить enterprise о том, что пользователь закрыл окно согласия (вызывается автоматически через sendBeacon). Запускает webhook user.oauth_cancel.
| Параметр | Тип | Описание |
|---|---|---|
| client_id | string | required |
| reason | string | Причина: window_closed, user_cancel |
POST /oauth/deny
Пользователь явно отклонил запрос на авторизацию. Запускает webhook user.oauth_deny и перенаправляет с ошибкой access_denied.
| Параметр | Тип | Описание |
|---|---|---|
| client_id | string | required |
| redirect_uri | string | required |
| state | string | Передаётся обратно в redirect |
GET /oauth/branding
Получить конфигурацию кнопок, иконки и готовый код для интеграции виджета TOKEN PAY ID. Не требует аутентификации.
{
"provider": "TOKEN PAY ID",
"widget_url": "https://tokenpay.space/sdk/tpid-widget.js",
"widget_version": "1.2.0",
"icon": { "shield_svg": "<svg ...>" },
"buttons": {
"standard": { "label": "Войти через TOKEN PAY", ... },
"icon": { "shape": "circle", ... },
"logo": { "background": "transparent", ... }
},
"integration": {
"quick_start": "<script src=... data-client-id=...>",
"oauth_popup": "TPID.loginWithOAuth().then(...);"
},
"themes": ["dark", "light", "auto"],
"languages": ["ru", "en"]
}
POST /oauth/register — Dynamic Client Registration (RFC 7591)
Зарегистрировать новый OAuth-клиент программно. Требуется авторизация (Bearer token или API ключ).
| Параметр | Тип | Описание |
|---|---|---|
| client_name | string | required — Название приложения |
| redirect_uris | string[] | required — Массив разрешённых redirect URI |
| client_type | string | public или confidential (по умолч. public) |
| client_uri | string | URL сайта приложения |
| logo_uri | string | URL логотипа |
| tos_uri | string | URL условий использования |
| policy_uri | string | URL политики конфиденциальности |
| contacts | string | Контактные email (через запятую) |
| scope | string | Запрашиваемые scope: openid, profile, email |
{
"client_id": "tpid_pk_...",
"client_secret": "tpid_sk_...",
"client_name": "My App",
"redirect_uris": ["https://myapp.com/callback"],
"client_type": "public",
"registration_access_token": "Bearer ..."
}
client_secret показывается только один раз при регистрации. Сохраните его.
GET /oauth/client-info/:client_id
Получить метаданные зарегистрированного OAuth-клиента. Доступно только владельцу ключа.
{
"client_id": "tpid_pk_...",
"client_name": "My App",
"client_type": "public",
"redirect_uris": ["https://myapp.com/callback"],
"client_uri": "https://myapp.com",
"logo_uri": null,
"tos_uri": null,
"policy_uri": null,
"scope": "openid,profile,email"
}
Пользователи
GET /users/me
Получить профиль текущего авторизованного пользователя.
{
"id": "tpid_usr_a1b2c3d4",
"email": "user@example.com",
"name": "Alex Example",
"email_verified": true,
"two_factor_enabled": false,
"telegram_linked": true,
"created_at": "2025-01-15T10:30:00Z",
"last_login": "2026-03-13T09:45:00Z"
}
PUT /users/me
Обновить профиль пользователя.
| Параметр | Тип | Описание |
|---|---|---|
| name | string | Имя пользователя |
| avatar_url | string | URL аватара |
GET /users/activity
История активности пользователя: входы, API вызовы, изменения безопасности.
| Параметр | Тип | Описание |
|---|---|---|
| limit | integer | Количество записей (по умолчанию 50, максимум 200) |
| offset | integer | Смещение для пагинации |
| type | string | Фильтр: auth, api, security |
GET /users/sessions
Список активных сессий пользователя.
{
"sessions": [
{
"id": "ses_abc123",
"device": "Chrome — Windows",
"ip": "192.168.1.1",
"location": "Saint Petersburg, RU",
"last_active": "2026-03-13T12:00:00Z",
"current": true
}
]
}
Auth
POST /auth/send-code
Отправить 6-значный код верификации на email. Используется перед регистрацией и при входе.
| Параметр | Тип | Описание |
|---|---|---|
| string | required — Email адрес | |
| type | string | required — register или login |
{
"success": true,
"message": "Verification code sent",
"expires_in": 600
}
POST /auth/verify
Верификация access токена. Используется сторонними сервисами для проверки авторизации пользователя.
| Параметр | Тип | Описание |
|---|---|---|
| token | string | required — Access token для проверки |
{
"valid": true,
"user_id": "tpid_usr_a1b2c3d4",
"email": "user@example.com",
"scopes": ["profile", "email"],
"expires_at": 1678886400
}
POST /auth/register
Регистрация нового пользователя. Требуется предварительная отправка кода через /auth/send-code.
| Параметр | Тип | Описание |
|---|---|---|
| string | required | |
| password | string | required — Минимум 8 символов |
| name | string | required |
| email_code | string | required — 6-значный код из email (от /auth/send-code) |
POST /auth/send-code с type: "register".
POST /auth/login
Двухэтапная авторизация. Первый вызов с email+password отправляет код на почту и возвращает requires_email_code: true. Второй вызов с email+password+email_code завершает авторизацию.
| Параметр | Тип | Описание |
|---|---|---|
| string | required | |
| password | string | required |
| email_code | string | 6-значный код из email (обязателен на втором шаге) |
| two_factor_code | string | TOTP код, если 2FA включена |
Шаг 1 — отправка пароля:
{
"requires_email_code": true,
"requires_2fa": false,
"message": "Verification code sent to your email"
}
Шаг 2 — подтверждение кода:
Повторите запрос, добавив email_code. При успехе возвращается access/refresh token и данные пользователя.
POST /auth/refresh
Обновление access токена с помощью refresh токена.
| Параметр | Тип | Описание |
|---|---|---|
| refresh_token | string | required |
QR-вход
Двухэтапный вход через QR-код: десктоп генерирует сессию, мобильное устройство (с активной авторизацией) подтверждает.
1. Инициализация QR-сессии
Создаёт QR-сессию (не требует авторизации). Возвращает sessionId и URL для QR-кода.
{
"sessionId": "uuid-...",
"ttl": 300,
"qrUrl": "https://tokenpay.space/qr-login?sid=uuid-..."
}
2. Polling (десктоп)
Десктоп опрашивает каждые 2 секунды. Статус: pending → approved → возвращает токены.
3. Подтверждение (мобильное устройство)
Требует Authorization: Bearer. Мобильное устройство подтверждает вход.
status: "expired".
API ключи
GET /keys
Получить список API ключей пользователя.
POST /keys
Создать новый API ключ.
| Параметр | Тип | Описание |
|---|---|---|
| name | string | required — Название ключа |
| scopes | string[] | Массив разрешений |
DELETE /keys/:id
Отозвать (удалить) API ключ. Все запросы с этим ключом немедленно перестанут работать.
Webhooks
Webhooks позволяют получать уведомления о событиях TOKEN PAY ID в реальном времени. Настройте URL вашего сервера в личном кабинете.
Каждый webhook запрос содержит подпись в заголовке X-TPID-Signature формата Stripe-style для верификации подлинности:
X-TPID-Signature: t=1711000000,v1=hmac_sha256_hex
Для проверки подписи:
const crypto = require('crypto'); function verifyWebhook(body, signature, secret) { const parts = {}; signature.split(',').forEach(p => { const [k, v] = p.split('='); parts[k] = v; }); const expected = crypto .createHmac('sha256', secret) .update(parts.t + '.' + JSON.stringify(body)) .digest('hex'); return parts.v1 === expected; }
t (timestamp) — отклоняйте запросы старше 5 минут.
Payload
{
"event": "user.login",
"timestamp": "2026-03-13T12:00:00Z",
"data": {
"user_id": "tpid_usr_a1b2c3d4",
"ip": "192.168.1.1",
"device": "Chrome — Windows"
}
}
Доступные события
| Событие | Описание |
|---|---|
| user.register | Новая регистрация пользователя |
| user.login | Успешный вход в аккаунт |
| user.logout | Выход из аккаунта |
| user.updated | Профиль обновлён |
| user.deleted | Аккаунт удалён |
| user.2fa.enabled | 2FA включена |
| user.2fa.disabled | 2FA отключена |
| key.created | Создан новый API ключ |
| key.revoked | API ключ отозван |
| session.created | Новая сессия |
| session.revoked | Сессия завершена |
| token.refreshed | Токен обновлён |
| user.oauth_connect | Пользователь одобрил OAuth авторизацию |
| user.oauth_cancel | Пользователь закрыл окно OAuth |
| user.oauth_deny | Пользователь отклонил OAuth запрос |
Push Notifications API
Система push-уведомлений позволяет получать уведомления в реальном времени через SSE (Server-Sent Events) или через REST API.
Получить историю уведомлений (последние 50). Требует Authorization: Bearer <token>.
{
"notifications": [
{ "id": "...", "type": "oauth_connect", "title": "New user connected", "body": "user@example.com authorized via OAuth", "is_read": false, "created_at": "2026-04-02T12:00:00Z" }
],
"unread": 3
}Отметить уведомление как прочитанное.
Отметить все уведомления как прочитанные.
SSE Stream (Server-Sent Events)
Подключение к потоку уведомлений в реальном времени. JWT передаётся через query-параметр (EventSource не поддерживает заголовки).
const es = new EventSource('/api/v1/notifications/stream?token=' + token); es.addEventListener('notification', e => { const data = JSON.parse(e.data); console.log(data.event, data.data.title); }); es.addEventListener('connected', e => { console.log('SSE connected', JSON.parse(e.data)); });
oauth_connect, oauth_cancelled, oauth_denied, key_created, key_revoked, user_unlink
Widget SDK v1.2 — Быстрый старт
Виджет TOKEN PAY ID предоставляет три варианта кнопок для интеграции: стандартная, круглая иконка и прозрачный логотип. Все кнопки автоматически работают с OAuth 2.0 + PKCE.
https://tokenpay.space/sdk/tpid-widget.jsВерсия 1.2.0 — автообновления, без кэширования.
Минимальная интеграция (1 строка)
<script src="https://tokenpay.space/sdk/tpid-widget.js" data-client-id="tpid_pk_ваш_ключ"></script>
Виджет автоматически создаст кнопку «Войти через TOKEN PAY» на странице.
OAuth Popup Flow (Promise API)
Метод TPID.loginWithOAuth() открывает popup, проводит OAuth flow и возвращает Promise с authorization code.
TPID.loginWithOAuth({ prompt: 'login' })
.then(function(result) {
console.log('Auth code:', result.code);
console.log('State:', result.state);
// Отправьте code на ваш сервер для обмена на токен
})
.catch(function(err) {
console.error('OAuth error:', err);
});
Программный API (TPID)
| Метод | Описание |
|---|---|
| TPID.init(opts) | Инициализация с clientId, redirectUri, onSuccess, onError, lang, autoButton |
| TPID.open() | Открыть окно авторизации (redirect flow) |
| TPID.loginWithOAuth(opts) | OAuth popup flow → Promise<{code, state}> |
| TPID.renderButton(el, opts) | Рендер стандартной кнопки в контейнер |
| TPID.renderIconButton(el, opts) | Рендер круглой иконки |
| TPID.renderLogoButton(el, opts) | Рендер прозрачной кнопки-логотипа |
| TPID.version | Текущая версия виджета (1.2.0) |
SDK — JavaScript v2.0 (tokenpay-auth.js)
OAuth SDK с автоматической генерацией PKCE (S256). Рекомендуется как основной SDK для серверных и SPA-приложений. Для простой интеграции кнопок см. Widget SDK v1.2.
https://tokenpay.space/sdk/tokenpay-auth.jsv2.0.0 — PKCE (S256) генерируется автоматически. code_verifier сохраняется в sessionStorage.
Два типа кнопок
| Тип | Вид | Описание |
|---|---|---|
| icon | data-mode="icon" | Круглая иконка (щит TOKEN PAY) — как у Google/Apple. Размеры: 36×36 / 44×44 / 54×54 px |
| logo | data-mode="logo" | Прямоугольная кнопка с иконкой + текст «Войти через TOKEN PAY». Min/max ширина ограничена. |
1. Быстрое подключение (data-атрибуты)
<!-- Прямоугольная кнопка с текстом --> <script src="https://tokenpay.space/sdk/tpid-auth.js" data-client-id="tpid_pk_ваш_ключ" data-redirect-uri="https://ваш-сайт.com/callback" data-mode="logo" data-theme="dark" data-size="medium" data-lang="ru"> </script>
<!-- Круглая иконка --> <script src="https://tokenpay.space/sdk/tpid-auth.js" data-client-id="tpid_pk_ваш_ключ" data-redirect-uri="https://ваш-сайт.com/callback" data-mode="icon" data-theme="light" data-size="large"> </script>
2. Программный API
// Инициализация TokenPayAuth.init({ clientId: 'tpid_pk_ваш_ключ', redirectUri: 'https://ваш-сайт.com/callback', theme: 'dark', locale: 'ru', onSuccess: function(data) { console.log('Код:', data.code); console.log('Verifier:', data.codeVerifier); // Отправьте code + codeVerifier на ваш сервер } }); // Рендер кнопки в контейнер TokenPayAuth.renderButton('#my-container', { mode: 'button', // 'button' | 'icon' theme: 'dark', // 'dark' | 'light' | 'auto' size: 'medium', // 'small' | 'medium' | 'large' locale: 'ru' }); // Или запуск OAuth вручную (PKCE генерируется автоматически) TokenPayAuth.authorize(); // Получить code_verifier для серверного обмена const verifier = TokenPayAuth.getCodeVerifier();
3. Обмен кода на токен (серверная сторона)
curl -X POST https://tokenpay.space/api/v1/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "authorization_code", "code": "tpid_code_...", "client_id": "tpid_pk_...", "client_secret": "tpid_sk_...", "redirect_uri": "https://ваш-сайт.com/callback", "code_verifier": "PKCE_VERIFIER" }'
4. Получение профиля пользователя
curl -X GET https://tokenpay.space/api/v1/oauth/userinfo \ -H "Authorization: Bearer ACCESS_TOKEN"
{
"id": "tpid_usr_a1b2c3d4e5f6",
"email": "user@example.com",
"name": "Alex Example",
"email_verified": true,
"created_at": "2026-01-15T12:00:00Z"
}
Параметры
| Параметр / Атрибут | Тип | Описание |
|---|---|---|
| clientId / data-client-id | string | required — Публичный ключ API (tpid_pk_...) |
| redirectUri / data-redirect-uri | string | required — URL для редиректа после авторизации |
| mode / data-mode | string | Тип кнопки: icon (круглая) или logo (прямоугольная, по умолчанию) |
| theme / data-theme | string | Тема: dark (белая кнопка), light (тёмная кнопка), auto |
| size / data-size | string | Размер: small (36px), medium (44px), large (54px) |
| lang / data-lang | string | Язык: ru или en |
| scope / data-scope | string | OAuth scope (по умолчанию openid email profile) |
| onSuccess | function | Callback: ({code, state, type}) => {} |
| onError | function | Callback при ошибке |
Ограничения размеров
| Режим | Small | Medium | Large |
|---|---|---|---|
| icon | 36×36 px | 44×44 px | 54×54 px |
| logo | h:36, w:180–320 px | h:44, w:220–400 px | h:54, w:260–480 px |
SDK — Python
pip install tokenpay-id
from tokenpay_id import TokenPayID client = TokenPayID( client_id="tpid_pk_your_public_key", client_secret="tpid_sk_your_secret_key" ) # Verify a user's token result = client.verify(access_token) print(result.user_id) # tpid_usr_... # Get user profile user = client.get_user(access_token) print(user.email) # user@example.com # Webhook signature verification is_valid = client.verify_webhook( payload=request.body, signature=request.headers["X-TPID-Signature"] )
SDK — Go
go get github.com/tokenpay/id-go
package main import ( tpid "github.com/tokenpay/id-go" ) func main() { client := tpid.NewClient(tpid.Config{ ClientID: "tpid_pk_your_public_key", ClientSecret: "tpid_sk_your_secret_key", }) // Verify token result, err := client.Verify(accessToken) if err != nil { log.Fatal(err) } fmt.Println(result.UserID) // tpid_usr_... }
Live preview — кнопки и виджет нативных SDK
v2.6.0
Точная HTML/CSS-реплика того, как нативный TpidLoginButton и виджет TpidAuth.signIn() выглядят в Android (Jetpack Compose), Swift (SwiftUI) и JVM Desktop (Compose for Desktop). UI-код в трёх SDK ~95 % общий и даёт пиксель-в-пиксель ту же картинку, что вы видите ниже.
Три варианта кнопки входа
Согласно brand guidelines — на нативной кнопке только официальный логотип, без текстовых меток. Это снимает вопрос локализации и даёт мгновенную узнаваемость.
// 1. Dark pill (default) — for light app surfaces TpidLoginButton(onResult = ::handleLogin) // 2. Light pill — for dark app surfaces TpidLoginButton( variant = TpidButtonVariant.LIGHT, onResult = ::handleLogin ) // 3. Icon-only — for tab bars, FABs, compact areas TpidLoginButton( variant = TpidButtonVariant.ICON_ONLY, onResult = ::handleLogin )
Виджет авторизации — точная копия Compose / SwiftUI экрана
Виджет открывается полноэкранно (или в Sheet/Dialog на десктопе) и проводит пользователя через welcome → email → пароль → код / 2FA → success. Переключите тему и язык — увидите ровно то же поведение, что у нативных SDK через TpidConfig.theme и TpidConfig.language.
TpidLoginButton(onResult = …) в Compose, TpidLoginButton(onResult: …) в SwiftUI или tpidLoginButton(onResult = …) в Compose for Desktop. Полные примеры — ниже по разделам.
SDK — Kotlin / Android
tokenpay-id-android-2.6.0.tar.gz
· install instructions
· API контракт.
Публикация в Maven Central — Q2 2026. Сейчас — source release по NDA: dev@tokenpay.space.
// 1. curl -L -O https://tokenpay.space/sdk/android/tokenpay-id-android-2.6.0.tar.gz // 2. tar -xzf tokenpay-id-android-2.6.0.tar.gz // 3. In settings.gradle.kts: include(":tokenpay-id-sdk") project(":tokenpay-id-sdk").projectDir = file("sdk/android/tokenpay-id-sdk") // In app/build.gradle.kts: dependencies { implementation(project(":tokenpay-id-sdk")) }
Native widget (v2.4) — одна строка на Compose
import space.tokenpay.id.TpidAuth import space.tokenpay.id.TpidConfig import space.tokenpay.id.TpidLoginButton import space.tokenpay.id.TpidResult // In Application.onCreate(): TpidAuth.initialize( context = applicationContext, config = TpidConfig( clientId = "tpid_pk_your_public_key", redirectUri = "com.cupol.vpn:/auth/callback", scopes = listOf("openid", "profile", "email"), theme = TpidConfig.Theme.AUTO, ) ) // In Compose — native widget opens in an Activity, no browser: @Composable fun LoginScreen() { TpidLoginButton { result -> when (result) { is TpidResult.Success -> navigate(result.accessToken) is TpidResult.Cancelled -> {} is TpidResult.Failure -> showError(result.error) } } }
Legacy (v2.2) — OAuth + system browser
Старое API, оставлено для обратной совместимости. Новые интеграции — только v2.4 native widget выше.
// Legacy — requires browser redirect via Custom Tab
import space.tokenpay.id.TokenPayID val client = TokenPayID( clientId = "tpid_pk_your_public_key", clientSecret = "tpid_sk_your_secret_key", redirectUri = "myapp://callback" ) // OAuth — открыть браузер val authUrl = client.getAuthorizationUrl( scopes = listOf("profile", "email"), usePKCE = true ) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))) // Обработка callback в Activity override fun onNewIntent(intent: Intent) { val code = intent.data?.getQueryParameter("code") ?: return lifecycleScope.launch { val tokens = client.exchangeCode(code) val user = client.getUserInfo(tokens.accessToken) Log.d("TPID", "User: ${user.name} (${user.email})") } } // Верификация токена на сервере val result = client.verify(accessToken) println(result.userId) // tpid_usr_...
SDK — Swift (iOS / iPadOS / macOS / tvOS / watchOS / visionOS)
Единый SDK для всей Apple-экосистемы. Поддерживает iOS 15+, iPadOS 15+, macOS 12+, tvOS 15+, watchOS 8+, visionOS 1+.
ASWebAuthenticationSession, без браузера. Download:
tokenpay-id-swift-2.6.0.tar.gz
· install
· API контракт.
SPM registry publication — Q2 2026. Pre-release: source по NDA через dev@tokenpay.space.
// 1. curl -L -O https://tokenpay.space/sdk/swift/tokenpay-id-swift-2.6.0.tar.gz // 2. shasum -a 256 -c tokenpay-id-swift-2.6.0.tar.gz.sha256 // 3. tar -xzf tokenpay-id-swift-2.6.0.tar.gz // 4. Xcode → File → Add Package Dependencies… → Add Local… // Select the unpacked directory. Add "TokenPayID" product to your target. // Or, in your own Package.swift: dependencies: [ .package(path: "./tokenpay-id-swift-2.6.0/sdk/swift") ]
Native widget (v2.4) — SwiftUI
import SwiftUI import TokenPayID @main struct MyApp: App { init() { TpidAuth.shared.initialize(config: .init( clientId: "tpid_pk_your_public_key", redirectURI: URL(string: "com.cupol.vpn:/auth/callback")!, scopes: ["openid", "profile", "email"], theme: .auto )) } var body: some Scene { WindowGroup { ContentView() } } } struct ContentView: View { var body: some View { // Native widget — opens as a SwiftUI sheet, NOT ASWebAuthenticationSession TpidLoginButton { result in switch result { case .success(let session): print("Signed in: \(session.user.email)") case .cancelled: break case .failure(let err): print("Error: \(err.code) — \(err.localizedDescription)") } } } }
Device Flow (tvOS / watchOS)
let device = try await TpidAuth.shared.startDeviceFlow() // device.userCode → "ABCD-EFGH" // device.verificationURIComplete → display as QR on screen let session = try await TpidAuth.shared.pollDeviceFlow(device)
Legacy (v2.3) — ASWebAuthenticationSession
Старый browser-based flow. Оставлен для обратной совместимости. Новые интеграции — только v2.4 native widget выше.
import SwiftUI import AuthenticationServices import TokenPayID class AuthManager: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding { let client = TPIDClient( clientId: "tpid_pk_your_public_key", redirectURI: "myapp://callback" // public client, no secret on iOS ) @Published var user: TPIDUser? @Published var isLoading = false private var session: ASWebAuthenticationSession? func signIn() { let (authURL, verifier) = client.authorizationURL( scopes: [.profile, .email], usePKCE: true ) isLoading = true session = ASWebAuthenticationSession( url: authURL, callbackURLScheme: "myapp" ) { [weak self] callbackURL, error in guard let self, let url = callbackURL, let code = url.queryParam("code") else { DispatchQueue.main.async { self?.isLoading = false } return } Task { @MainActor in do { let tokens = try await self.client.exchangeCode(code, verifier: verifier) try TPIDKeychain.save(tokens) // secure storage self.user = try await self.client.userInfo(tokens.accessToken) } catch { print("Auth error: \(error)") } self.isLoading = false } } session?.presentationContextProvider = self session?.prefersEphemeralWebBrowserSession = false // share cookies with Safari session?.start() } func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { UIApplication.shared.connectedScenes .compactMap { ($0 as? UIWindowScene)?.windows.first } .first { $0.isKeyWindow } ?? ASPresentationAnchor() } } // SwiftUI View struct ContentView: View { @StateObject var auth = AuthManager() var body: some View { if let user = auth.user { Text("Привет, \(user.name)!") } else { Button("Войти через TOKEN PAY ID") { auth.signIn() } .disabled(auth.isLoading) } } }
macOS — SwiftUI AppKit
import AppKit import AuthenticationServices import TokenPayID // Добавьте в Info.plist: // CFBundleURLTypes → CFBundleURLSchemes → "myapp" class MacAuth: NSObject, ASWebAuthenticationPresentationContextProviding { let client = TPIDClient( clientId: "tpid_pk_your_public_key", redirectURI: "myapp://callback" ) func authenticate() async throws -> TPIDUser { let (authURL, verifier) = client.authorizationURL( scopes: [.profile, .email], usePKCE: true ) let callback: URL = try await withCheckedThrowingContinuation { cont in let s = ASWebAuthenticationSession( url: authURL, callbackURLScheme: "myapp" ) { url, err in if let url { cont.resume(returning: url) } else { cont.resume(throwing: err ?? TPIDError.cancelled) } } s.presentationContextProvider = self s.start() } guard let code = callback.queryParam("code") else { throw TPIDError.noCode } let tokens = try await client.exchangeCode(code, verifier: verifier) try TPIDKeychain.save(tokens) return try await client.userInfo(tokens.accessToken) } func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { NSApp.keyWindow ?? NSApp.windows.first ?? ASPresentationAnchor() } }
Linux / server-side Swift — raw URLSession (Foundation)
Для headless использования (Vapor, Kitura, CLI) — SDK работает без UI через пару HTTP-вызовов:
import Foundation #if canImport(FoundationNetworking) import FoundationNetworking // Linux #endif import TokenPayID let client = TPIDClient( clientId: ProcessInfo.processInfo.environment["TPID_CLIENT_ID"]!, clientSecret: ProcessInfo.processInfo.environment["TPID_CLIENT_SECRET"]!, redirectURI: "https://api.myapp.com/auth/callback" ) // 1) Генерация URL авторизации — редиректим пользователя сюда let (authURL, verifier) = client.authorizationURL( scopes: [.profile, .email, .openid], state: UUID().uuidString, usePKCE: true ) // Сохраните verifier в сессии (Redis/memcached) // 2) Callback handler — обмен кода на токены func handleCallback(code: String, verifier: String) async throws -> TPIDUser { let tokens = try await client.exchangeCode(code, verifier: verifier) return try await client.userInfo(tokens.accessToken) } // 3) Vapor integration app.get("auth", "login") { req in let (url, v) = client.authorizationURL(scopes: [.profile, .email], usePKCE: true) req.session.data["verifier"] = v return req.redirect(to: url.absoluteString) } app.get("auth", "callback") { req async throws -> Response in let code = try req.query.get(String.self, at: "code") let v = req.session.data["verifier"] ?? "" let user = try await handleCallback(code: code, verifier: v) req.session.data["user_id"] = user.id return req.redirect(to: "/dashboard") }
Keychain-хранилище токенов (iOS / macOS)
// SDK встроенно использует Keychain через TPIDKeychain try TPIDKeychain.save(tokens) // сохранить let tokens = try TPIDKeychain.load() // загрузить try TPIDKeychain.clear() // выход // Автоматический refresh при истечении access_token let freshTokens = try await client.ensureFresh(tokens) // Отзыв при logout try await client.revoke(tokens.refreshToken) try TPIDKeychain.clear()
ASWebAuthenticationSession на Apple-устройствах, прямые HTTP-вызовы на сервере/Linux).
SDK — JVM Desktop (Kotlin + Compose for Desktop)
tokenpay-id-jvm-desktop-2.6.0.tar.gz
· install.
Построен на JetBrains Compose for Desktop 1.6. 90%+ кода UI совпадает с Android SDK (та же программная модель Jetpack Compose).
Единый Kotlin-пакет для десктопных JVM-приложений на Windows 10/11, Linux (любой X11/Wayland с GNOME/KDE), macOS 12+. Работает в любом JVM-приложении: standalone Compose Desktop, встраивание в Swing через ComposePanel, в JavaFX через SwingNode. Min JDK 17.
// 1. Download + verify curl -L -O https://tokenpay.space/sdk/jvm-desktop/tokenpay-id-jvm-desktop-2.6.0.tar.gz sha256sum -c tokenpay-id-jvm-desktop-2.6.0.tar.gz.sha256 tar -xzf tokenpay-id-jvm-desktop-2.6.0.tar.gz // 2. settings.gradle.kts include(":tokenpay-id-jvm") project(":tokenpay-id-jvm").projectDir = file("sdk/jvm-desktop/tokenpay-id-jvm") // 3. app/build.gradle.kts plugins { id("org.jetbrains.compose") version "1.6.10" } dependencies { implementation(project(":tokenpay-id-jvm")) }
import androidx.compose.ui.window.* import space.tokenpay.id.jvm.* fun main() = application { TpidAuth.initialize(TpidConfig( clientId = "tpid_pk_your_key", redirectUri = "cupol-vpn://auth/callback" )) Window(onCloseRequest = ::exitApplication, title = "My App") { TpidLoginButton { result -> when (result) { is TpidResult.Success -> println("signed in as ${result.user.email}") is TpidResult.Failure -> println("error: ${result.error.code}") TpidResult.Cancelled -> {} } } } }
// Swing import androidx.compose.ui.awt.ComposePanel val panel = ComposePanel() panel.setContent { TpidLoginButton { /* ... */ } } jframe.add(panel) // JavaFX — wrap ComposePanel in SwingNode val node = SwingNode() SwingUtilities.invokeLater { val p = ComposePanel() p.setContent { TpidLoginButton { /* ... */ } } node.setContent(p) }
Хранение refresh-токена:
- Windows → DPAPI через JNA (
Crypt32.dll: CryptProtectData/CryptUnprotectData). Привязано к Windows-аккаунту. - macOS → Keychain через CLI
security add-generic-password. iCloud Keychain по умолчанию выключен — токен не синхронизируется между устройствами. - Linux → libsecret через
secret-tool(GNOME Keyring / KWallet). Работает со всеми популярными DE. - Fallback — AES-GCM 256 файл в
~/.local/share/%APPDATA%/~/Library/Application Support. Ключ выведен через PBKDF2 отuser.home + user.name + machine-id; скопированный файл на другой машине не расшифровать.
Полная документация на отдельной странице: tokenpay.space/sdk#jvm.
SDK — C# / .NET
dotnet add package TokenPayID --version 2.2.0
using TokenPayID; var client = new TPIDClient( clientId: "tpid_pk_your_public_key", clientSecret: "tpid_sk_your_secret_key", redirectUri: "https://yourapp.com/callback" ); // Генерация OAuth URL (с PKCE) var (authUrl, verifier) = client.GetAuthorizationUrl( scopes: new[] { "profile", "email" }, usePKCE: true ); // Обмен кода на токены var tokens = await client.ExchangeCodeAsync(code, verifier); var user = await client.GetUserInfoAsync(tokens.AccessToken); Console.WriteLine($"User: {user.Name} ({user.Email})"); // ASP.NET — middleware services.AddAuthentication() .AddTokenPayID(options => { options.ClientId = "tpid_pk_..."; options.ClientSecret = "tpid_sk_..."; });
SDK — PHP
composer require tokenpay/id-php
use TokenPay\ID\Client; $client = new Client([ 'client_id' => 'tpid_pk_your_public_key', 'client_secret' => 'tpid_sk_your_secret_key', 'redirect_uri' => 'https://yourapp.com/callback', ]); // Редирект на авторизацию (с PKCE) [$authUrl, $verifier] = $client->getAuthorizationUrl(['profile', 'email']); $_SESSION['tpid_verifier'] = $verifier; header("Location: $authUrl"); // Callback — обмен кода $tokens = $client->exchangeCode($_GET['code'], $_SESSION['tpid_verifier']); $user = $client->getUserInfo($tokens->access_token); echo "Hello, {$user->name}!"; // Laravel — встроенный Socialite драйвер // config/services.php 'tokenpay' => [ 'client_id' => env('TPID_CLIENT_ID'), 'client_secret' => env('TPID_CLIENT_SECRET'), 'redirect' => '/auth/tokenpay/callback', ]
SDK — Ruby
gem 'tokenpay-id', '~> 2.2'
require 'tokenpay/id' client = TokenPay::ID::Client.new( client_id: 'tpid_pk_your_public_key', client_secret: 'tpid_sk_your_secret_key', redirect_uri: 'https://yourapp.com/callback' ) # OAuth URL auth_url, verifier = client.authorization_url( scopes: ['profile', 'email'], pkce: true ) # Обмен кода tokens = client.exchange_code(params[:code], verifier) user = client.user_info(tokens.access_token) puts "#{user.name} (#{user.email})" # Rails — OmniAuth стратегия Rails.application.config.middleware.use OmniAuth::Builder do provider :tokenpay_id, ENV['TPID_CLIENT_ID'], ENV['TPID_CLIENT_SECRET'] end
SDK — Dart / Flutter
dependencies: tokenpay_id: ^2.2.0
import 'package:tokenpay_id/tokenpay_id.dart'; final client = TPIDClient( clientId: 'tpid_pk_your_public_key', clientSecret: 'tpid_sk_your_secret_key', redirectUri: 'myapp://callback', ); // Flutter — OAuth через url_launcher final authUrl = client.getAuthorizationUrl( scopes: ['profile', 'email'], usePKCE: true, ); await launchUrl(Uri.parse(authUrl)); // Обработка deep link void handleCallback(Uri uri) async { final code = uri.queryParameters['code']!; final tokens = await client.exchangeCode(code); final user = await client.getUserInfo(tokens.accessToken); print('Hello, ${user.name}!'); }
SDK — Rust
[dependencies]
tokenpay-id = "2.2"
use tokenpay_id::Client; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let client = Client::new( "tpid_pk_your_public_key", "tpid_sk_your_secret_key", ); // Верификация токена let result = client.verify(access_token).await?; println!("User: {}", result.user_id); // OAuth URL с PKCE let (auth_url, verifier) = client.authorization_url( &["profile", "email"], "https://yourapp.com/callback", )?; // Обмен кода let tokens = client.exchange_code(code, &verifier).await?; let user = client.user_info(&tokens.access_token).await?; println!("{} ({})", user.name, user.email); Ok(()) }
Desktop & Native Integration
Для десктопных приложений (Electron, Tauri, WPF, SwiftUI, Kotlin Desktop) используется OAuth через системный браузер с перехватом deep link.
Поддерживаемые платформы
| Платформа | Метод | Redirect URI |
|---|---|---|
| iOS / iPadOS | ASWebAuthenticationSession + Universal Link | myapp://callback или https://app.com/cb |
| macOS | ASWebAuthenticationSession + Custom URL scheme | myapp://callback |
| Android | Custom Tabs + Intent filter | myapp://callback |
| Linux (Ubuntu/Debian/Fedora/Arch) | xdg-open + localhost HTTP server | http://127.0.0.1:PORT/callback |
| Windows (WPF/WinUI/UWP) | URI activation / Custom protocol | myapp://callback |
| Electron (Win/macOS/Linux) | Custom protocol + second-instance | myapp://callback |
| Tauri (Win/macOS/Linux) | Deep link plugin | myapp://callback |
| Flutter (всё) | uni_links / app_links | myapp://callback |
| CLI / Server / Headless | Localhost HTTP callback | http://127.0.0.1:PORT/callback |
| Embedded (ESP32, RPi Pico W) | WiFiManager + mDNS | http://device.local/callback |
Параметры для десктопных приложений
GET https://tokenpay.space/api/v1/oauth/authorize ?client_id=tpid_pk_... &redirect_uri=myapp://callback // custom scheme &response_type=code &scope=profile email &state=RANDOM_STATE &code_challenge=BASE64URL(SHA256(verifier)) &code_challenge_method=S256 &theme=dark // dark | light — match app theme &lang=ru // ru | en | zh — UI language
Deep Link Flow
const { app } = require('electron'); // Регистрация custom protocol app.setAsDefaultProtocolClient('myapp'); // Обработка deep link (Windows/Linux) app.on('second-instance', (event, argv) => { const url = argv.find(a => a.startsWith('myapp://')); if (url) handleOAuthCallback(url); }); // Обработка deep link (macOS) app.on('open-url', (event, url) => { event.preventDefault(); handleOAuthCallback(url); }); async function handleOAuthCallback(url) { const { searchParams } = new URL(url); const code = searchParams.get('code'); const state = searchParams.get('state'); // Обменяйте code на токены через API }
// src-tauri/tauri.conf.json { "plugins": { "deep-link": { "desktop": { "schemes": ["myapp"] }, "mobile": [ { "host": "callback", "pathPrefix": "/" } ] } } } // Frontend (TypeScript) import { onOpenUrl } from '@tauri-apps/plugin-deep-link'; onOpenUrl((urls) => { const url = new URL(urls[0]); const code = url.searchParams.get('code'); // Exchange code for tokens });
Linux — GTK / Qt / xdg-open
На Linux используется xdg-open для системного браузера + встроенный HTTP-сервер на 127.0.0.1 для callback. Работает на Ubuntu, Fedora, Arch, Debian, openSUSE, Alpine и других дистрибутивах.
import http.server, socketserver, threading, webbrowser, urllib.parse import secrets, hashlib, base64, requests CLIENT_ID = "tpid_pk_your_public_key" REDIRECT_PORT = 53682 # порт для локального callback REDIRECT_URI = f"http://127.0.0.1:{REDIRECT_PORT}/callback" # PKCE verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode() challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode() state = secrets.token_urlsafe(16) # 1) Запустить локальный HTTP-сервер для callback auth_code = {} class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): q = urllib.parse.urlparse(self.path).query params = dict(urllib.parse.parse_qsl(q)) if "code" in params: auth_code["code"] = params["code"] self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(b"<h1>✓ Успешно!</h1><p>Можно закрыть окно.</p>") else: self.send_error(400) def log_message(self, *args): pass # suppress logs server = socketserver.TCPServer(("127.0.0.1", REDIRECT_PORT), Handler) threading.Thread(target=server.serve_forever, daemon=True).start() # 2) Открыть системный браузер с URL авторизации auth_url = ( "https://id.tokenpay.space/api/v1/oauth/authorize?" f"client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code" f"&scope=profile+email&state={state}" f"&code_challenge={challenge}&code_challenge_method=S256" ) webbrowser.open(auth_url) # использует xdg-open на Linux # 3) Ждать callback while "code" not in auth_code: pass server.shutdown() # 4) Обменять код на токены r = requests.post("https://id.tokenpay.space/api/v1/oauth/token", json={ "grant_type": "authorization_code", "code": auth_code["code"], "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "code_verifier": verifier, }) tokens = r.json() user = requests.get("https://id.tokenpay.space/api/v1/oauth/userinfo", headers={"Authorization": f"Bearer {tokens['access_token']}"}).json() print(f"Welcome, {user['name']}!")
# Ubuntu/Debian/Fedora/Arch — полный OAuth flow через curl + nc + xdg-open #!/bin/bash set -e CLIENT_ID="tpid_pk_your_public_key" PORT=53682 VERIFIER=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-43) CHALLENGE=$(echo -n "$VERIFIER" | openssl dgst -sha256 -binary | base64 | tr -d "=+/") STATE=$(openssl rand -hex 8) # Открыть браузер xdg-open "https://id.tokenpay.space/api/v1/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=http://127.0.0.1:${PORT}/callback&response_type=code&scope=profile+email&state=${STATE}&code_challenge=${CHALLENGE}&code_challenge_method=S256" # Ждать callback (одноразовый HTTP-сервер через netcat) CALLBACK=$(echo -e "HTTP/1.1 200 OK\r\n\r\n<h1>✓ OK</h1>" | nc -l -p $PORT -q 1) CODE=$(echo "$CALLBACK" | grep -oP 'code=\K[^& ]+') # Обменять код на токен TOKENS=$(curl -s -X POST https://id.tokenpay.space/api/v1/oauth/token \ -H "Content-Type: application/json" \ -d "{\"grant_type\":\"authorization_code\",\"code\":\"$CODE\",\"client_id\":\"$CLIENT_ID\",\"redirect_uri\":\"http://127.0.0.1:$PORT/callback\",\"code_verifier\":\"$VERIFIER\"}") ACCESS_TOKEN=$(echo "$TOKENS" | jq -r ".access_token") # Получить профиль curl -s https://id.tokenpay.space/api/v1/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" | jq .
// Запуск браузера через gio (работает на любом Linux с GIO) #include <gio/gio.h> g_app_info_launch_default_for_uri( "https://id.tokenpay.space/api/v1/oauth/authorize?client_id=...&redirect_uri=http://127.0.0.1:53682/callback&...", NULL, NULL); // Минимальный HTTP-сервер для callback — libsoup: #include <libsoup/soup.h> SoupServer *srv = soup_server_new("server-header", "myapp/1.0", NULL); soup_server_add_handler(srv, "/callback", callback_handler, NULL, NULL); soup_server_listen_local(srv, 53682, 0, NULL);
#include <QDesktopServices> #include <QOAuth2AuthorizationCodeFlow> #include <QOAuthHttpServerReplyHandler> QOAuth2AuthorizationCodeFlow oauth; oauth.setAuthorizationUrl(QUrl("https://id.tokenpay.space/api/v1/oauth/authorize")); oauth.setAccessTokenUrl(QUrl("https://id.tokenpay.space/api/v1/oauth/token")); oauth.setClientIdentifier("tpid_pk_your_public_key"); oauth.setScope("profile email"); oauth.setPkceMethod(QOAuth2AuthorizationCodeFlow::PkceMethod::S256); auto *handler = new QOAuthHttpServerReplyHandler(53682); oauth.setReplyHandler(handler); oauth.setModifyParametersFunction([](QAbstractOAuth::Stage, QMultiMap<QString, QVariant> *p) { /* */ }); QObject::connect(&oauth, &QAbstractOAuth::authorizeWithBrowser, &QDesktopServices::openUrl); QObject::connect(&oauth, &QOAuth2AuthorizationCodeFlow::granted, []{ qDebug() << "Authenticated!"; }); oauth.grant();
xdg-utils (обычно предустановлен) и любой системный браузер.
Raw HTTP — универсальный способ (любая платформа)
Если для вашей платформы нет SDK, используйте чистый HTTP. Работает на любом языке/ОС, которые умеют делать HTTPS-запросы:
# 1. Сгенерировать PKCE verifier + challenge verifier = base64url(random_bytes(32)) # 43 символа challenge = base64url(sha256(verifier)) # S256 # 2. Отправить пользователя на URL авторизации GET https://id.tokenpay.space/api/v1/oauth/authorize? client_id=YOUR_PK& redirect_uri=YOUR_REDIRECT& response_type=code& scope=profile+email& state=RANDOM_STATE& code_challenge=CHALLENGE& code_challenge_method=S256 # 3. В callback — обменять code на access_token POST https://id.tokenpay.space/api/v1/oauth/token Content-Type: application/json { "grant_type": "authorization_code", "code": "abc123...", "client_id": "YOUR_PK", "redirect_uri": "YOUR_REDIRECT", "code_verifier": "VERIFIER_FROM_STEP_1" } → {"access_token": "eyJ...", "refresh_token": "tpid_rt_...", ...} # 4. Использовать access_token для API-запросов GET https://id.tokenpay.space/api/v1/oauth/userinfo Authorization: Bearer {access_token} → {"id": "tpid_usr_...", "email": "...", "name": "..."}
Native Widget v2 — концепция
В отличие от tpid-widget.js (работает в браузере) и OAuth-редиректа через системный браузер, Native Widget v2 — это полностью нативный экран логина, встроенный в ваше приложение. Никаких WebView, никаких переключений в браузер — пользователь не выходит из вашего UX. Идеология совпадает с Apple Sign In и Google One Tap.
— Пиксельно-одинаковый брендинг TPID на всех платформах (iOS/Android/Windows/macOS/Linux/TV/Wear)
— Полный auth-flow без браузера: email, пароль, email-код, 2FA, passkey/biometric, регистрация, восстановление
— Приложение получает только OAuth
code (а не пароль) — всё через стандартный PKCE flow
— Анти-фишинговый бейдж «Официальный TOKEN PAY ID» на каждом экране
— Единый API-контракт во всех SDK (Kotlin/Swift/C#/Dart/Rust/KMP)
Как это работает под капотом
1. SDK при инициализации запрашивает GET /api/v1/sdk/config?client_id=... — получает брендинг, доступные методы авторизации, TLS-пины
2. При клике TpidLoginButton SDK открывает модальный native-экран с UI TPID
3. Перед показом экрана пароля SDK вызывает POST /auth/account-check — узнаёт доступные для юзера методы (password / passkey / TOTP) и показывает правильный экран
4. Все auth-запросы идут напрямую на TPID API через HTTPS с certificate pinning
5. После успешного входа — стандартный OAuth PKCE code → callback в приложение → обмен на access_token
| Платформа | Tier | SDK |
|---|---|---|
| Android 7.0+ (Jetpack Compose) | Tier 1 | Maven Central AAR |
| iOS 14+ / iPadOS 14+ (SwiftUI) | Tier 1 | SwiftPM + CocoaPods |
| macOS 12+ (SwiftUI/AppKit) | Tier 1 | SwiftPM |
| Windows 10 1809+ (WinUI 3/WPF) | Tier 1 | NuGet |
| Linux glibc 2.31+ (Qt 6/GTK 4) | Tier 1 | deb, rpm, Flatpak, AppImage, pkgconfig |
| Flutter 3.19+ (cross) | Tier 1 | pub.dev |
| React Native 0.72+ | Tier 1 | npm + native bridges |
| Kotlin/Compose Multiplatform 1.9+ | Tier 1 | Maven Central |
| watchOS / Wear OS / tvOS / Android TV / Fire TV | Tier 2 | Device Flow (RFC 8628) |
| Xbox / PlayStation / Switch | Tier 2 | Device Flow + companion app |
| Tizen / webOS / HarmonyOS | Tier 2 | JS widget или Device Flow |
| Tauri / Electron | Tier 2 | npm с N-API биндингами |
API контракт (единый на всех SDK)
Независимо от языка конфигурация/результат/ошибки имеют одинаковую форму. Это упрощает кросс-платформенную интеграцию и перевод документации.
TpidWidget.Config {
clientId: String // required — ваш tpid_pk_...
redirectUri: String // required — должен совпадать с разрешённым в личном кабинете
scope: List<String> // default: ["profile", "email"]
state: String? // auto-generated CSRF token, verified on callback
prompt: "login" | "consent" | "none" // optional
theme: "dark" | "light" | "auto" // default: "auto" (follows system)
lang: "ru" | "en" | "zh" | null // null = system locale
loginHint: String? // pre-fill email field
presentationStyle: "modal" | "fullscreen" | "bottomSheet" // default: "modal"
telemetry: Boolean // default: true — opt-out anytime
}
TpidWidget.Result {
success(code: String, state: String)
error(code: TpidError, message: String, httpStatus: Int?)
}
enum TpidError {
userCancelled, // пользователь закрыл виджет
networkError, // нет сети / таймаут
invalidClient, // неверный client_id или redirect_uri
invalidScope, // scope не разрешён для клиента
rateLimited, // 429 — слишком частые попытки
serverError, // 5xx от TPID
phishingDetected, // сорван TLS-pin → возможная MITM-атака
unknown
}
Вызов — единая строка на каждом SDK
TpidLoginButton(
clientId = "tpid_pk_...",
redirectUri = "cupol://callback",
scope = listOf("profile", "email"),
onSuccess = { code, state -> viewModel.exchangeCode(code) },
onError = { err -> toast(err.message) }
)
TPIDLoginButton(
clientId: "tpid_pk_...",
redirectURI: "cupol://callback",
scope: [.profile, .email],
style: .standard
) { result in
switch result {
case .success(let code): await exchange(code)
case .failure(let err): show(err)
}
}
TpidLoginButton( clientId: 'tpid_pk_...', redirectUri: 'cupol://callback', scope: const ['profile', 'email'], onSuccess: (code, state) => ref.read(auth).exchange(code), )
await TpidLoginButton.ShowAsync(new TpidConfig { ClientId = "tpid_pk_...", RedirectUri = "myapp://callback", Scope = new[] { "profile", "email" }, Theme = TpidTheme.Auto }, onSuccess: (code, state) => ExchangeCode(code));
Экраны и UI-потоки
Виджет показывает 8 экранов в едином стиле на всех платформах. Переход между ними — автоматический, SDK сам принимает решение по ответу /auth/account-check.
| # | Экран | Назначение | API вызов |
|---|---|---|---|
| 1 | Ввод email + «Продолжить» | POST /auth/account-check | |
| 2 | Пароль | Ввод пароля + «Забыли?» | POST /auth/login |
| 3 | Код из email | 6-значный код + таймер | POST /auth/send-code + /auth/verify |
| 4 | 2FA / TOTP | 6-значный TOTP | POST /auth/2fa/verify |
| 5 | Passkey / Biometric | FaceID / TouchID / Windows Hello | WebAuthn через /auth/passkey |
| 6 | Успех | Check-анимация → callback в приложение | OAuth code → /oauth/token |
| 7 | Регистрация | email + пароль + имя + код | POST /auth/register |
| 8 | Восстановление пароля | email → код → новый пароль | POST /auth/forgot-password + /auth/reset-password |
lang, темы dark/light/auto с system-follow.
Безопасность
- PKCE S256 обязателен — SDK генерирует
code_verifier+code_challenge, приложение никогда не видит пароль. - TLS pinning по SPKI для
*.tokenpay.space. Пины получаются черезGET /sdk/tls-pins, кешируются 24 часа. При mismatch —TpidError.phishingDetected. - Anti-phishing badge «Официальный TOKEN PAY ID» отображается на каждом экране, иконка из
/sdk/config. - Secure storage: refresh_token в Keychain (iOS/macOS), EncryptedSharedPreferences (Android), Credential Manager / DPAPI (Windows), libsecret / KWallet (Linux).
- Device binding: refresh_token привязан к device-fingerprint — нельзя украсть и использовать на другом устройстве без re-auth.
- Screenshot prevention: FLAG_SECURE на Android, UIView blur при app-switcher на iOS, блокировка Snipping Tool на Windows.
- No secret logging: пароль, коды и токены никогда не попадают в логи даже в debug-режиме.
- Rate-limit UX: при 429 показывается дружелюбный экран с таймером до разблокировки.
Device Flow — TV, watch, игровые консоли, IoT (RFC 8628)
Для устройств без клавиатуры/браузера (Apple TV, Android TV, Xbox, умные часы, ESP32): устройство показывает короткий код, пользователь открывает на телефоне id.tokenpay.space/device и вводит его. После подтверждения устройство получает токен.
POST /oauth/device
Инициирует Device Flow. Возвращает device_code (секретный, для polling) и user_code (короткий, показывается пользователю).
| Параметр | Тип | Описание |
|---|---|---|
| client_id | string | required |
| scope | string | Запрашиваемые разрешения (по умолч. profile email) |
{
"device_code": "tpid_dc_abc123...",
"user_code": "ABCD-EFGH",
"verification_uri": "https://id.tokenpay.space/device",
"verification_uri_complete": "https://id.tokenpay.space/device?user_code=ABCD-EFGH",
"expires_in": 600,
"interval": 5
}
POST /oauth/device/token
Устройство polling-ит этот endpoint раз в interval секунд до получения access_token.
| Параметр | Тип | Описание |
|---|---|---|
| grant_type | string | required — urn:ietf:params:oauth:grant-type:device_code |
| device_code | string | required |
| client_id | string | required |
Возможные ответы:
| Ответ | Описание |
|---|---|
200 OK + tokens | Пользователь подтвердил — выданы access/refresh_token |
400 authorization_pending | Ждём подтверждения — продолжайте polling |
400 slow_down | Слишком частые запросы — увеличьте interval на 5s |
400 access_denied | Пользователь нажал «Отклонить» |
400 expired_token | Истекли 10 минут — запросите новый device_code |
POST /oauth/device/verify
Вызывается с авторизованной сессии пользователя (обычно со страницы /device). Ассоциирует user_code с пользователем.
| Параметр | Тип | Описание |
|---|---|---|
| user_code | string | required — в формате ABCD-EFGH |
| action | string | approve (по умолч.) или deny |
// 1. Запросить device_code let init: DeviceInitResponse = try await api.post("/oauth/device", body: [ "client_id": clientId, "scope": "profile email" ]) let userCode = init.user_code // "ABCD-EFGH" — показать на экране ТВ showOnScreen(code: userCode, url: init.verification_uri) // 2. Polling каждые `interval` секунд while true { try await Task.sleep(seconds: init.interval) let r: TokenOrError = try await api.post("/oauth/device/token", body: [ "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": init.device_code, "client_id": clientId ]) switch r { case .success(let tokens): saveTokens(tokens); return case .pending: continue // authorization_pending case .slowDown: init.interval += 5; continue case .denied, .expired: throw TPIDError.userCancelled } }
Integration API
GET /sdk/config
Полная конфигурация для инициализации native widget: endpoints, supported flows/scopes/auth methods, брендинг, TLS-пины, лимиты. Не требует аутентификации — SDK вызывает при старте.
| Параметр | Тип | Описание |
|---|---|---|
| client_id | string | Опционально — если указан, добавляется блок branding |
{
"provider": "TOKEN PAY ID",
"version": "2.5.0",
"endpoints": {
"authorize": "https://id.tokenpay.space/api/v1/oauth/authorize",
"token": "https://id.tokenpay.space/api/v1/oauth/token",
"userinfo": "https://id.tokenpay.space/api/v1/oauth/userinfo",
"device_authorize": "https://id.tokenpay.space/api/v1/oauth/device",
"device_token": "https://id.tokenpay.space/api/v1/oauth/device/token",
"account_check": "https://id.tokenpay.space/api/v1/auth/account-check",
"telemetry": "https://id.tokenpay.space/api/v1/telemetry/event",
"openid_config": "https://id.tokenpay.space/.well-known/openid-configuration",
"jwks": "https://id.tokenpay.space/.well-known/jwks.json"
},
"supported_flows": ["authorization_code", "refresh_token", "device_code", "qr_login"],
"supported_scopes": ["openid", "profile", "email", "offline_access"],
"supported_auth_methods": ["email_password", "email_code", "totp_2fa", "passkey", "biometric", "magic_link"],
"supported_themes": ["dark", "light", "auto"],
"supported_languages": ["ru", "en", "zh"],
"presentation_styles": ["modal", "fullscreen", "bottomSheet"],
"pkce": { "required": true, "methods": ["S256"] },
"tls_pinning": {
"enabled": true,
"hosts": ["id.tokenpay.space", "tokenpay.space"],
"pins_endpoint": "https://id.tokenpay.space/api/v1/sdk/tls-pins"
},
"anti_phishing_badge": { "label": "Официальный TOKEN PAY ID", "verify_url": "https://tokenpay.space/verify" },
"rate_limits": { "auth": { "per_min": 8, "per_day": 1000 } },
"branding": { "client_name": "CUPOL VPN", "logo_uri": "..." }
}
GET /sdk/version
Версии всех официальных SDK, ссылки на пакеты, минимальная поддерживаемая версия API. SDK использует для авто-обновления и breaking-change уведомлений.
{
"api": "2.5.0",
"sdks": {
"kotlin_android": { "version": "2.6.0", "url": "https://tokenpay.space/sdk/android/" },
"swift": { "version": "2.6.0", "platforms": ["iOS 15+", "macOS 12+", ...] },
"jvm_desktop": { "version": "2.6.0", "url": "https://tokenpay.space/sdk/jvm-desktop/" },
"flutter_dart": { "version": "2.3.0", "status": "spec" },
"dotnet": { "version": "2.3.0", "status": "spec" },
"rust": { "version": "2.3.0", "status": "spec" }, ...
},
"min_supported": { "api": "2.1.0", "sdk": "2.0.0" },
"breaking_changes": false
}
GET /sdk/tls-pins
SPKI SHA-256 пины для certificate pinning в native-SDK. SDK должен кешировать их на 24 часа и отказывать в соединении при mismatch (TpidError.phishingDetected).
{
"hosts": ["id.tokenpay.space", "tokenpay.space", "api.tokenpay.space"],
"algorithm": "sha256",
"format": "base64",
"pins": ["PRIMARY_SPKI_B64=", "BACKUP_SPKI_B64="],
"refresh_interval_s": 86400,
"issued_at": "2026-04-17T13:00:00Z"
}
POST /auth/account-check
Pre-login probe: по email проверяет, зарегистрирован ли пользователь и какие методы авторизации у него доступны. Native widget использует это перед показом экрана пароля — чтобы сразу показать passkey/2FA если есть.
client_id и rate-limit 8/min. Timing uniform — внешний наблюдатель не может отличить несуществующий email от существующего по времени ответа.
| Параметр | Тип | Описание |
|---|---|---|
| string | required | |
| client_id | string | required — валидируется против active api_keys |
{
"exists": true,
"methods": ["email_password", "email_code", "totp_2fa", "passkey", "magic_link"],
"can_register": false,
"email_verified": true,
"has_password": true,
"has_totp": true,
"has_passkey": true,
"suggested_next_step": "passkey"
}
val r = api.accountCheck(email, clientId) when (r.suggested_next_step) { "passkey" -> showPasskeyScreen() // FaceID/TouchID/Hello "password" -> showPasswordScreen() // если нет passkey "email_code" -> sendCodeAndShowCodeScreen() // если нет пароля "register" -> showRegistrationScreen() // exists == false }
GET /auth/session-version
Позволяет SDK проверить, не была ли сессия инвалидирована (смена пароля / 2FA / logout всех сессий). SDK вызывает этот endpoint при старте приложения — если session_version изменилось, локальные токены отзываются и показывается экран логина.
{
"session_version": 7,
"user_id": "tpid_usr_...",
"should_reauth": false
}
POST /telemetry/event
Принимает события funnel-аналитики native widget для дашборда интегратора. PII никогда не сохраняется — входной фильтр режет любые поля похожие на email/password/code/token/secret/phone/card/ssn.
| Параметр | Тип | Описание |
|---|---|---|
| event | string | required — имя события (см. список ниже) |
| client_id | string | Для группировки в дашборде |
| session_id | string | Случайный UUID сессии widget'а (для funnel) |
| sdk_version | string | Версия native SDK |
| platform | string | ios, android, windows, macos, linux, flutter, etc. |
| properties | object | Свободные метаданные (только string/number/boolean, не более 200 символов на значение) |
Разрешённые события:
// Жизненный цикл widget widget.opened, widget.closed, widget.cancelled, widget.success // Пошаговая воронка widget.step_shown, widget.step_completed, widget.step_error // Проблемы widget.network_error, widget.rate_limited, widget.tls_pin_fail // SDK sdk.init, sdk.ready // Device flow device_flow.code_generated, device_flow.approved, device_flow.denied, device_flow.expired
TpidTelemetry.send(
event = "widget.step_shown",
clientId = "tpid_pk_...",
sessionId = widgetSession.id,
platform = "android",
sdkVersion = BuildConfig.SDK_VERSION,
properties = mapOf(
"step" to "password",
"theme" to "dark",
"lang" to "ru",
"app_bundle" to "space.cupol.vpn"
)
)
GET /telemetry/funnel — дашборд для владельца клиента
Возвращает агрегированную воронку событий по client_id. Требует auth + проверяет владельца ключа.
GET /api/v1/telemetry/funnel?client_id=tpid_pk_...&since=7%20days
Authorization: Bearer tpid_sk_...
→ { "events": [ { "event": "widget.opened", "count": 12403, "sessions": 9812 },
{ "event": "widget.success", "count": 7204, "sessions": 7204 } ] }
Changelog
v2.6.0 — 21 апреля 2026
Widget UX — "Создать аккаунт" теперь работает на всех платформах
— Кнопка "Создать аккаунт" на Welcome screen во всех 3 SDK (Android / Swift / JVM) теперь ведёт прямо в форму регистрации, а не на Email screen
— register_hint — пояснение ru/en/zh, почему виджет перешёл на регистрацию при вводе неизвестного email
— "Отправить код подтверждения" укорочено до "Далее" / "Next" / "下一步" во всех SDK
— Встроенная кнопка самообновления в update-banner: чёрно-белый стиль, открывает remote.download_url в системном браузере
— JVM-виджет: окно сжато до 540 dp, контент прижат к верху — убраны пустые полосы сверху и снизу, на которые жаловались интеграторы
Email шаблоны — один canonical layout, без внешних картинок
— Шапка и подвал писем теперь пара чёрных HTML-полос с белым текстом "TOKEN PAY LLC". Больше никаких битых email-header-black.png / email-footer-black.png, которые gmail рендерил как пустые белые прямоугольники
— Код подтверждения рендерится по цифрам в отдельных <span>, без letter-spacing на контейнере — устранён "фантомная точка" после цифр в Mail.ru/Outlook
— Шрифт почты Arial-first вместо Comfortaa fallback — письмо одинаково выглядит в Gmail Web/iOS/Android, Mail.ru, Yandex Mail, Outlook, Apple Mail
Gradle и совместимость
— Все три SDK скомпилены и упакованы как pre.7; archive checksums обновлены в releases.json
— JVM regression test привязан к SdkBuildInfo.version == "2.6.0"
v2.5.0-pre.1 — 17 апреля 2026
Native Widget v2 — реальные SDK-исходники (по запросу CUPOL VPN)
— Android SDK (Kotlin + Jetpack Compose) — полный source release
— Swift SDK (SwiftUI, iOS/macOS/tvOS/watchOS/visionOS)
— Download hub: tokenpay.space/sdk — версии, checksums, install per platform
— GET /api/v1/sdk/releases — manifest c артефактами для авто-обновления
— 9 экранов Native UI (Compose + SwiftUI): email, password, email code, 2FA, passkey, recovery, loading, success, fatal error
— OAuth 2.0 + PKCE S256 (mandatory), TLS pin via OkHttp CertificatePinner / URLSession delegate, EncryptedSharedPreferences / Keychain storage
Integration API
— Новые endpoints: /sdk/config, /sdk/tls-pins, /sdk/version расширенный (10 SDK)
— POST /auth/account-check — pre-login probe с уведомлением о доступных методах (password/passkey/TOTP)
— GET /auth/session-version — SDK проверяет инвалидацию сессий
— Device Flow (RFC 8628): /oauth/device + /oauth/device/token + /oauth/device/verify + /oauth/device/info
— Telemetry API: /telemetry/event (с PII-фильтром: email/password/code/token блокируются) + /telemetry/funnel (дашборд для владельца client_id)
— Спецификация API-контракта TpidWidget.Config / Result / Error для всех SDK
— Secure storage recommendations per platform (Keychain / EncryptedSharedPreferences / DPAPI / libsecret)
Инфраструктура
— CORS: fix HTTP 500 на OPTIONS preflight (ломал Android/iOS/Windows native HTTP клиенты)
— /api/v1/health + /api/v1/ping (HEAD/GET) — heartbeat для SDK
— JSON 404 handler вместо HTML Express default
— SSE stream heartbeat 25s для обхода Cloudflare idle timeout
Документация
— Закрытый API: обновлены главная и docs — API выдаётся по запросу, не публичный
— Swift SDK полная секция: iOS + iPadOS + macOS + tvOS + watchOS + Linux/Vapor
— Linux Desktop: Python/Bash/GTK/Qt 4 варианта интеграции
— Raw HTTP — универсальный 3-шаговый способ для любой платформы
— Language/theme toggles с поддержкой RU/EN/ZH на /docs
v2.3.0 — 13 апреля 2026
SDK-спецификации (design docs; реальные SDK выпущены с v2.5.0-pre.1, см. выше)
— Kotlin / Android — дизайн API
— Swift / iOS — дизайн API
— C# / .NET — спецификация (реализация Q2 2026)
— PHP / Laravel Socialite — спецификация
— Ruby / OmniAuth — спецификация
— Dart / Flutter — спецификация
— Rust — спецификация
Desktop & Native
— Документация по интеграции: Electron, Tauri, WPF, SwiftUI
— Поддержка ?theme= и ?lang= параметров для OAuth страниц
— Deep link flow с custom URL schemes
— Localhost callback для CLI-утилит
UI
— Чёрные звёзды-частицы в белой теме (инверсия тёмной)
— Частицы добавлены на все страницы экосистемы
— Обновлён favicon.ico (мультиразмерный из нового логотипа)
— Cache busting обновлён до v=11
v2.2.0 — 27 марта 2026
Безопасность
— PKCE (S256) теперь обязателен для всех OAuth-клиентов
— Ротация refresh-токенов: при обновлении выдаётся новый refresh_token
— session_version — смена пароля/2FA мгновенно инвалидирует все сессии
— API-ключи запрещены из браузерных окружений (только серверный вызов)
— requireScope middleware для гранулярного контроля доступа
— Webhook подписи в формате Stripe: t=timestamp,v1=hmac
— Отвязка TPID доступна только администратору
— /.well-known/security.txt (RFC 9116)
Dynamic Client Registration (RFC 7591)
— POST /oauth/register — программная регистрация OAuth-клиентов
— GET /oauth/client-info/:id — получение метаданных клиента
— PUT /oauth/register/:id — обновление метаданных клиента
— Consent screen отображает tos_uri, policy_uri, client_uri
— OpenID Discovery: registration_endpoint, client_info_endpoint
QR-вход
— POST /auth/qr/login-init — создание QR-сессии
— GET /auth/qr/login-poll/:id — polling статуса
— POST /auth/qr/login-confirm/:id — подтверждение с мобильного
— Исправлен критический баг: QR-токены теперь корректно проходят авторизацию
API
— Строгая валидация redirect_uri (exact match)
— OpenID Discovery: pkce_required: true, code_challenge_methods_supported: ["S256"]
— API версия обновлена до 2.2.0
SDK
— tokenpay-auth.js обновлён до v2.0.0
— Автоматическая генерация PKCE (S256) в authorize()
— getCodeVerifier() — получение code_verifier из sessionStorage
— CSRF state validation на callback
— code_verifier автоматически передаётся в exchangeCode()
— Кнопки обновлены: текст «TOKEN PAY ID», шрифт Comfortaa
v2.1.0 — 18 марта 2026
OAuth 2.0
— Поддержка prompt=login для принудительной переавторизации
— Поддержка login_hint для предзаполнения email
— Повторная валидация redirect_uri в /oauth/approve
— Новый эндпойнт: GET /oauth/branding
Widget SDK v1.2.0
— 3 варианта кнопок: standard, icon, logo
— TPID.loginWithOAuth() — OAuth popup flow с Promise API
— Автоматический рендер через data-tpid-button
— Поддержка тем dark/light/auto
