MMeerPartners docs

SSO / token-exchange

Exchange an external user identifier for an affiliate JWT per RFC 8693. Embed the MeerPartners influencer cabinet into your product without a second registration. Fields, response and example.

Token-exchange lets you authorize your own users inside the affiliate program without forcing them to register with MeerPartners or sending them through MeerID. Your backend exchanges its user identifier (e.g. a Telegram user id or an internal UUID) for a short-lived affiliate JWT with the affiliate:embed scope. With this token you can open the affiliate widgets right inside your interface.

The mechanics follow RFC 8693 (OAuth 2.0 Token Exchange).

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

The request is signed with HMAC over the secret of an API key with the sso scope — the same scheme as the postback. Headers and algorithm: API keys and signing.

When you need this

  • You have your own product with authorized users (a website, an app, a Telegram Mini App).
  • You want to give them an influencer cabinet (links, statistics, payouts) inside the product.
  • You do not want a second registration and do not want to route them through the MeerID sign-in screen.

If you only need to send conversions, token-exchange is not required — the postback is enough.

How it works

Your frontend           Your backend               MeerPartners
     │                       │                          │
     │ open "Affiliate"      │                          │
     │ ────────────────────► │                          │
     │                       │ POST /api/v1/auth/token-exchange
     │                       │ (HMAC over the scope-sso key) │
     │                       │ ────────────────────────►│
     │                       │                          │ resolve key → tenant
     │                       │                          │ find-or-create the partner
     │                       │                          │ issue a scoped JWT
     │                       │ ◄──── access + refresh ──│
     │ ◄── scoped token ─────│                          │
     │ open the widgets with this token                 │

The tenant is resolved only from the API key in the database — a tenant field in the body is ignored (protection against spoofing). If the partner under your subject_token does not exist yet, the platform creates it on the fly (JIT) under race protection.

Request fields

The body is JSON.

grant_typestringoptional

The grant type. The default and expected value is urn:ietf:params:oauth:grant-type:token-exchange.

subject_tokenstringrequired

Your user identifier (1–255 characters). The platform does not interpret it — it stores it as an opaque string to bind your user to an affiliate profile.

subject_token_typestringoptional

The subject type. Defaults to external_user_id.

subject_profileobjectoptional

An optional profile, applied only on first creation of the partner (JIT). The recognized keys are: email, display_name, username (if there is no display_name, username is used). This is untrusted data — it does not affect an existing user.

The tenant in the body is ignored

Do not try to pass a tenant in the request body — it is taken from the API key anyway (anti-IDOR). One key works strictly with its own business.

Response

200 — scoped token
{
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 600,
  "scope": "affiliate:embed",
  "affiliate_id": 8123,
  "tenant_id": 17
}
FieldValue
access_tokenScoped JWT, scope affiliate:embed, TTL 10 minutes
refresh_tokenA standard refresh (TTL 30 days) — extends the session without another exchange
expires_inThe access token's TTL in seconds (600)
scopeAlways affiliate:embed
affiliate_idThe affiliate profile ID — so the SDK does not have to call /me right away
tenant_idYour business ID (from the key)

A scoped token ≠ full access

The affiliate:embed token is trimmed: it is fine for the influencer-cabinet widgets but does not allow "dangerous" actions. For example, "sign out of all sessions" (/api/v1/auth/logout-all) and role onboarding require a full token and will return EMBED_SCOPE_DENIED (403) for an embed token.

Rejection codes

CodeHTTPWhen
TENANT_API_KEY_INVALID401Key is unknown, inactive or expired; the X-Api-Key-Id header is missing
TENANT_API_KEY_SCOPE_DENIED403The key lacks the sso scope
EXCHANGE_DENIED401The signature is invalid, X-Timestamp is missing/expired, or the tenant is inactive
EXCHANGE_REPLAY401The same signature replayed within the memory window
USER_BANNED403The found user is banned
SERVICE_UNAVAILABLE503The replay-protection store is unavailable (fail-closed) — try again later

The freshness of X-Timestamp is the same ±300 s as for the postback. The error envelope format — API error codes.

Example

Exchanging an internal user id for a scoped token. The signature is computed over the raw request body.

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"

What's next