MMeerPartners docs

SSO / token-exchange

Обмен внешнего идентификатора пользователя на партнёрский JWT по RFC 8693. Встройте кабинет инфлюенсера MeerPartners в свой продукт без повторной регистрации. Поля, ответ и пример.

Token-exchange позволяет авторизовать ваших пользователей внутри партнёрки, не заставляя их регистрироваться в MeerPartners и не проводя через MeerID. Ваш бэкенд обменивает свой идентификатор пользователя (например telegram user id или внутренний UUID) на короткоживущий партнёрский JWT со скоупом affiliate:embed. С этим токеном можно открыть виджеты партнёрки прямо в вашем интерфейсе.

Механика следует RFC 8693 (OAuth 2.0 Token Exchange).

POST/api/v1/auth/token-exchange🔒 HMAC (scope sso)

Запрос подписывается HMAC по секрету API-ключа со скоупом sso — той же схемой, что и postback. Заголовки и алгоритм: API-ключи и подпись.

Когда это нужно

  • У вас есть свой продукт с авторизованными пользователями (сайт, приложение, Telegram Mini App).
  • Вы хотите дать им кабинет инфлюенсера (ссылки, статистика, выплаты) внутри продукта.
  • Вы не хотите вторую регистрацию и не хотите гонять их через экран входа MeerID.

Если же нужно просто отправлять конверсии — token-exchange не требуется, достаточно postback.

Как это работает

Ваш фронтенд            Ваш бэкенд                 MeerPartners
     │                       │                          │
     │ открыть «Партнёрку»   │                          │
     │ ────────────────────► │                          │
     │                       │ POST /api/v1/auth/token-exchange
     │                       │ (HMAC по ключу scope sso) │
     │                       │ ────────────────────────►│
     │                       │                          │ резолв ключа → tenant
     │                       │                          │ find-or-create партнёра
     │                       │                          │ выпуск scoped JWT
     │                       │ ◄──── access + refresh ──│
     │ ◄── scoped-токен ─────│                          │
     │ открыть виджеты с этим токеном                    │

Tenant определяется только по API-ключу в базе — поле tenant из тела игнорируется (защита от подмены). Если партнёр под вашим subject_token ещё не существует, платформа создаёт его на лету (JIT) под защитой от гонок.

Поля запроса

Тело — JSON.

grant_typestringoptional

Тип гранта. Значение по умолчанию и ожидаемое — urn:ietf:params:oauth:grant-type:token-exchange.

subject_tokenstringrequired

Ваш идентификатор пользователя (1–255 символов). Платформа не интерпретирует его — хранит как opaque-строку для связывания вашего пользователя с партнёрским профилем.

subject_token_typestringoptional

Тип субъекта. По умолчанию external_user_id.

subject_profileobjectoptional

Необязательный профиль, применяется только при первом создании партнёра (JIT). Распознаются ключи: email, display_name, username (если нет display_name, берётся username). Это недоверенные данные — на существующего пользователя они не влияют.

tenant в теле игнорируется

Не пытайтесь передать tenant в теле запроса — он всё равно берётся из API-ключа (анти-IDOR). Один ключ работает строго со своим бизнесом.

Ответ

200 — scoped токен
{
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 600,
  "scope": "affiliate:embed",
  "affiliate_id": 8123,
  "tenant_id": 17
}
ПолеЗначение
access_tokenScoped JWT, scope affiliate:embed, TTL 10 минут
refresh_tokenСтандартный refresh (TTL 30 дней) — продлевает сессию без повторного обмена
expires_inTTL access-токена в секундах (600)
scopeВсегда affiliate:embed
affiliate_idID партнёрского профиля — чтобы SDK не делал /me сразу
tenant_idID вашего бизнеса (из ключа)

Scoped-токен ≠ полный доступ

Токен affiliate:embed урезан: он годится для виджетов кабинета инфлюенсера, но не даёт «опасных» действий. Например, «выйти из всех сессий» (/api/v1/auth/logout-all) и онбординг ролей требуют полного токена и для embed-токена вернут EMBED_SCOPE_DENIED (403).

Коды отклонения

КодHTTPКогда
TENANT_API_KEY_INVALID401Ключ неизвестен, неактивен или истёк; нет заголовка X-Api-Key-Id
TENANT_API_KEY_SCOPE_DENIED403У ключа нет скоупа sso
EXCHANGE_DENIED401Подпись невалидна, нет/просрочен X-Timestamp, tenant неактивен
EXCHANGE_REPLAY401Повтор той же подписи в окне памяти
USER_BANNED403Найденный пользователь заблокирован
SERVICE_UNAVAILABLE503Хранилище replay-защиты недоступно (fail-closed) — повторите позже

Свежесть X-Timestamp — те же ±300 с, что у postback. Формат конверта ошибки — Коды ошибок API.

Пример

Обмен внутреннего user-id на scoped-токен. Подпись считается по сырому телу запроса.

API_BASE="https://api.meerpartners.com"
KEY_ID="key_sso_77"
SECRET="sk_live_…"
 
BODY='{"grant_type":"urn:ietf:params:oauth:grant-type:token-exchange","subject_token":"vpn-user-99281","subject_token_type":"external_user_id","subject_profile":{"email":"u@vpn.io","display_name":"Neo"}}'
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')
SIGNING=$(printf '%s\n%s\n%s\n%s' "POST" "/api/v1/auth/token-exchange" "$TS" "$BODY_HASH")
SIG=$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
 
curl -sS -X POST "$API_BASE/api/v1/auth/token-exchange" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key-Id: $KEY_ID" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  --data "$BODY"

Что дальше