Backend validation
The SDK does not create user sessions. Your backend must validate the sessionToken with TryMellon and then create your own session (e.g. set a cookie or return a JWT).
Validate the token
Call the TryMellon API with the token your frontend received from register() or authenticate():
GET https://api.trymellonauth.com/v1/sessions/validate
Authorization: Bearer {sessionToken}
Or with a different base URL if you configured one:
GET {apiBaseUrl}/v1/sessions/validate
Authorization: Bearer {sessionToken}
Response
Success (200):
{
"ok": true,
"data": {
"valid": true,
"user_id": "usr_xxx",
"external_user_id": "user_123",
"tenant_id": "tenant_xxx",
"app_id": "app_xxx"
}
}
Use data.external_user_id to identify the user in your system. Then create your own session (e.g. set an httpOnly cookie or issue a JWT) and redirect or return the appropriate response.
Error (4xx/5xx):
{
"ok": false,
"error": {
"code": "invalid_session_token",
"message": "Invalid or expired session token"
}
}
| Status | Code | When |
|---|---|---|
401 | invalid_session_token | JWT signature invalid, token expired, or payload malformed |
401 | invalid_token_type | Token is not a user session token (e.g. onboarding token passed by mistake) |
401 | user_not_found | Token was valid but the user no longer exists or is inactive |
Token TTL: Session tokens expire after 24 hours. They are stateless JWTs — validation does not consume them. Call /v1/sessions/validate on the login callback; for subsequent requests trust your own session instead.
On any non-200 response, do not create a session; return a 401 to the client.
When to validate
| Approach | Latency | Security | Offline |
|---|---|---|---|
Always call /v1/sessions/validate | +50ms | Highest (real-time revocation) | No |
Validate JWT locally (check exp, sig) | 0ms | High (no revocation check) | Yes |
| Cache validation for 5 min | 0ms (hot) | Medium | Partial |
Recommendation: Call /v1/sessions/validate on the login callback
(this is your trust boundary). For subsequent API requests, trust the
session token locally by checking its expiration.
Login callback (always validate)
This is where your user “logs in.” Always call TryMellon here:
app.post('/api/auth/callback', async (req, res) => {
const { session_token } = req.body;
const response = await fetch('https://api.trymellonauth.com/v1/sessions/validate', {
headers: { Authorization: `Bearer ${session_token}` },
});
if (!response.ok) return res.status(401).json({ error: 'Invalid session' });
const { data } = await response.json();
// Create YOUR session (cookie, JWT, etc.)
req.session.userId = data.external_user_id;
res.json({ ok: true });
});
Subsequent requests (trust locally)
After login, your app has its own session. You don’t need to call TryMellon again unless you need real-time revocation checks.
Example (Node/Express-style)
// POST /api/login
const sessionToken = req.body.sessionToken
if (!sessionToken) {
return res.status(400).json({ error: 'sessionToken required' })
}
const response = await fetch('https://api.trymellonauth.com/v1/sessions/validate', {
method: 'GET',
headers: { Authorization: `Bearer ${sessionToken}` },
})
if (!response.ok) {
return res.status(401).json({ error: 'Invalid session' })
}
const { data } = await response.json()
// data.valid, data.external_user_id, data.tenant_id, data.app_id
// Create your own session (e.g. set cookie, store in DB)
// Then redirect or return success
After validation, issue your own session (cookie, JWT, etc.). The TryMellon session token is a stateless JWT valid for 24 hours — it is not consumed and can be re-validated if needed, but your backend should manage its own session to avoid coupling to TryMellon tokens.
On this site: The frontend calls setSessionOnServer(sessionToken), which POSTs the token to /api/auth/set-session. That endpoint validates the token with TryMellon (GET /v1/sessions/validate); only if the response is 200 does it set an httpOnly cookie. In development only, the sandbox token (SANDBOX_SESSION_TOKEN) is accepted without calling the API. See the auth flow in the repo for the exact pattern.
Development / sandbox: accepting the sandbox token
When the frontend uses the SDK with sandbox mode (sandbox: true), it receives a fixed session token (the constant SANDBOX_SESSION_TOKEN from @trymellon/js). In development only, your backend may accept this token and create a session for a test user without calling the TryMellon API.
Security: You MUST NOT accept the sandbox token in production. Only allow it when NODE_ENV === 'development' (or an equivalent flag). In production, always validate every session token with TryMellon.
Example (Node/Express-style):
import { SANDBOX_SESSION_TOKEN } from '@trymellon/js'
// POST /api/login
const sessionToken = req.body.sessionToken
if (!sessionToken) {
return res.status(400).json({ error: 'sessionToken required' })
}
// In development only: accept sandbox token without calling TryMellon
if (process.env.NODE_ENV === 'development' && sessionToken === SANDBOX_SESSION_TOKEN) {
// Create session for a test user (e.g. external_user_id: 'sandbox')
// Then redirect or return success
return createTestSessionAndRespond(req, res)
}
const response = await fetch('https://api.trymellonauth.com/v1/sessions/validate', {
method: 'GET',
headers: { Authorization: `Bearer ${sessionToken}` },
})
if (!response.ok) {
return res.status(401).json({ error: 'Invalid session' })
}
const { data } = await response.json()
// data.external_user_id, data.tenant_id, data.app_id
// Create your own session (e.g. set cookie), then redirect or return success
Validate in your language
The example above uses Node/Express. Below are equivalent snippets for other backends.
Node.js (Express)
See the Example (Node/Express-style) above.
Python (requests)
import requests
TRYMELLON_API = "https://api.trymellonauth.com"
def validate_session(session_token: str) -> dict:
"""Validate a TryMellon session token. Call this on your login callback."""
response = requests.get(
f"{TRYMELLON_API}/v1/sessions/validate",
headers={"Authorization": f"Bearer {session_token}"},
timeout=5,
)
if response.status_code != 200:
raise ValueError(f"Invalid session: {response.status_code}")
body = response.json()
data = body["data"]
if not data.get("valid"):
raise ValueError("Session not valid")
return data # { valid, user_id, external_user_id, tenant_id, app_id }
Go
package auth
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
type SessionData struct {
Valid bool `json:"valid"`
UserID string `json:"user_id"`
ExternalUserID string `json:"external_user_id"`
TenantID string `json:"tenant_id"`
AppID string `json:"app_id"`
}
type ValidateResponse struct {
Ok bool `json:"ok"`
Data SessionData `json:"data"`
}
func ValidateSession(sessionToken string) (*SessionData, error) {
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET",
"https://api.trymellonauth.com/v1/sessions/validate", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+sessionToken)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("invalid session: status %d", resp.StatusCode)
}
var body ValidateResponse
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return nil, err
}
if !body.Data.Valid {
return nil, fmt.Errorf("session not valid")
}
return &body.Data, nil
}
Java (HttpClient)
import java.net.URI;
import java.net.http.*;
import com.google.gson.Gson;
public class TryMellonValidator {
private static final String API = "https://api.trymellonauth.com";
private static final HttpClient client = HttpClient.newHttpClient();
private static final Gson gson = new Gson();
public record SessionData(
boolean valid, String user_id, String external_user_id,
String tenant_id, String app_id
) {}
public record ValidateResponse(boolean ok, SessionData data) {}
public static SessionData validate(String sessionToken) throws Exception {
var request = HttpRequest.newBuilder()
.uri(URI.create(API + "/v1/sessions/validate"))
.header("Authorization", "Bearer " + sessionToken)
.timeout(java.time.Duration.ofSeconds(5))
.GET()
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200)
throw new RuntimeException("Invalid session: " + response.statusCode());
var body = gson.fromJson(response.body(), ValidateResponse.class);
if (!body.data().valid())
throw new RuntimeException("Session not valid");
return body.data();
}
}
Ruby (net/http)
require 'net/http'
require 'json'
require 'uri'
TRYMELLON_API = "https://api.trymellonauth.com"
def validate_session(session_token)
uri = URI("#{TRYMELLON_API}/v1/sessions/validate")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{session_token}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true,
open_timeout: 5, read_timeout: 5) { |http|
http.request(req)
}
raise "Invalid session: #{res.code}" unless res.code == '200'
body = JSON.parse(res.body)
data = body['data']
raise "Session not valid" unless data['valid']
data # { "valid", "user_id", "external_user_id", "tenant_id", "app_id" }
end