Connect flow
This page covers the Connect protocol: the browser-mediated Sudomimus flow used when your application wants direct control over the login round-trip. Connect speaks JSON over HTTPS, so any backend language with an HTTP client works; examples below are in curl, Node.js, Python, and Go.
For production code, prefer an official SDK where one exists. Start with the SDK overview or jump to the TypeScript SDK for @sudomimus/connect.
If you’re building a native client (desktop, game, CLI), see Native clients. For OIDC, see OIDC relying parties.
Tabs are synchronised across the page: pick your language once and every block below switches with it.
Protocol at a glance
Section titled “Protocol at a glance”| Phase | Initiator | Endpoint | Result |
|---|---|---|---|
| 1. Establish | Application backend | connect-api POST /establish | { exposureKey, hiddenKey } |
| 2. Authenticate | Browser | via.sudomimus.com | The user completes an allowed challenge |
| 3. Redeem | Application backend | connect-api POST /redeem | { accessToken, refreshToken } |
| 4. Refresh | Application backend | session-api POST /refresh | A new access token and rotated refresh token |
Three parties split responsibility:
- Your backend signs
/establish, storeshiddenKey, redeems the completed inquiry, and verifies the resulting tokens. - The browser carries
exposureKeyto the hosted authentication UI but never seeshiddenKey. via.sudomimus.comruns the passkey, email OTP, OAuth, or federation challenge and createsconfirmationKeyonly after authentication succeeds.
The first three phases are specific to Connect. OIDC uses authorization code + PKCE, while native direct-issue exchanges a Steam ticket or AccessKey in one request. After any ordinary application flow has a refresh token, the shared Session API owns refresh, introspection, logout, and revocation.
1. Establish — start a session
Section titled “1. Establish — start a session”Your backend asks Connect to open an authentication session. The response gives you an exposure key (passed to the browser) and a hidden key (kept on the server).
curl -X POST https://connect-api.sudomimus.com/establish \ -H "Content-Type: application/json" \ -H "Authorization: SudomimusClientJWT $SUDOMIMUS_CLIENT_AUTH_JWT" \ -d '{ "applicationAnchor": "your-application", "returnMethods": [ { "type": "CALLBACK", "payload": { "callbackUrl": "https://your-app.com/auth/callback" } } ] }'const res = await fetch("https://connect-api.sudomimus.com/establish", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `SudomimusClientJWT ${await signEstablishJwt(body)}`, }, body: JSON.stringify({ applicationAnchor: process.env.SUDOMIMUS_APPLICATION_ANCHOR, returnMethods: [ { type: "CALLBACK", payload: { callbackUrl: "https://your-app.com/auth/callback" }, }, ], }),});
const { exposureKey, hiddenKey } = await res.json();import os, requests
res = requests.post( "https://connect-api.sudomimus.com/establish", headers={ "Authorization": f"SudomimusClientJWT {sign_establish_jwt(body)}", }, json={ "applicationAnchor": os.environ["SUDOMIMUS_APPLICATION_ANCHOR"], "returnMethods": [ { "type": "CALLBACK", "payload": {"callbackUrl": "https://your-app.com/auth/callback"}, }, ], },)
data = res.json()exposure_key = data["exposureKey"]hidden_key = data["hiddenKey"]body, _ := json.Marshal(map[string]any{ "applicationAnchor": os.Getenv("SUDOMIMUS_APPLICATION_ANCHOR"), "returnMethods": []map[string]any{{ "type": "CALLBACK", "payload": map[string]any{ "callbackUrl": "https://your-app.com/auth/callback", }, }},})
req, _ := http.NewRequest( "POST", "https://connect-api.sudomimus.com/establish", bytes.NewReader(body),)req.Header.Set("Content-Type", "application/json")req.Header.Set("Authorization", "SudomimusClientJWT "+signEstablishJwt(body))
res, err := http.DefaultClient.Do(req)
var data struct { ExposureKey string `json:"exposureKey"` HiddenKey string `json:"hiddenKey"`}json.NewDecoder(res.Body).Decode(&data)Store hiddenKey against the user’s pending session (e.g. in a server-side store). Send the user to via.sudomimus.com with the exposureKey in the URL.
2. Authenticate — hand off to via.sudomimus.com
Section titled “2. Authenticate — hand off to via.sudomimus.com”Redirect the user’s browser to via.sudomimus.com with the exposure key. The user completes the passkey or email-OTP challenge there.
# No HTTP call — this is a 302 redirect from your application:Location: https://via.sudomimus.com/?exposure-key=<exposureKey>const authUrl = new URL("https://via.sudomimus.com/");authUrl.searchParams.set("exposure-key", exposureKey);
return Response.redirect(authUrl.toString(), 302);from urllib.parse import urlencodefrom flask import redirect
return redirect( "https://via.sudomimus.com/?" + urlencode({"exposure-key": exposure_key}), code=302,)http.Redirect( w, r, "https://via.sudomimus.com/?exposure-key="+url.QueryEscape(exposureKey), http.StatusFound,)When the user finishes, via.sudomimus.com redirects the browser to your
concrete callbackUrl with exposure-key and confirmation-key appended as
query parameters. Existing query parameters and fragments are preserved. If
the URL already contains either reserved parameter, Connect overwrites it with
the current Inquiry value; do not put key templates in the callback URL.
3. Redeem — exchange for a token
Section titled “3. Redeem — exchange for a token”In your callback handler, combine the three keys and exchange them at Connect for an access token plus a refresh token.
curl -X POST https://connect-api.sudomimus.com/redeem \ -H "Content-Type: application/json" \ -d '{ "exposureKey": "...", "hiddenKey": "...", "confirmationKey": "..." }'// inside GET /auth/callback?exposure-key=...&confirmation-key=...const { exposureKey, hiddenKey } = await loadPendingSession(req);const confirmationKey = req.query["confirmation-key"];
const res = await fetch("https://connect-api.sudomimus.com/redeem", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ exposureKey, hiddenKey, confirmationKey }),});
const { accessToken, refreshToken } = await res.json();# inside GET /auth/callback?exposure-key=...&confirmation-key=...exposure_key, hidden_key = load_pending_session(request)confirmation_key = request.args["confirmation-key"]
res = requests.post( "https://connect-api.sudomimus.com/redeem", json={ "exposureKey": exposure_key, "hiddenKey": hidden_key, "confirmationKey": confirmation_key, },)
data = res.json()access_token = data["accessToken"]refresh_token = data["refreshToken"]// inside GET /auth/callback?exposure-key=...&confirmation-key=...exposureKey, hiddenKey := loadPendingSession(r)confirmationKey := r.URL.Query().Get("confirmation-key")
body, _ := json.Marshal(map[string]string{ "exposureKey": exposureKey, "hiddenKey": hiddenKey, "confirmationKey": confirmationKey,})
res, _ := http.Post( "https://connect-api.sudomimus.com/redeem", "application/json", bytes.NewReader(body),)
var data struct { AccessToken string `json:"accessToken"` RefreshToken string `json:"refreshToken"`}json.NewDecoder(res.Body).Decode(&data)The access token is a signed JWT. Read its kid, select that key from the application’s Session JWKS at GET /applications/{applicationAnchor}/jwks.json, verify the signature, and only then trust its claims. See Tokens and verification for the full recipe, including cache refresh and the kty: "Access" header check.
4. Refresh — keep the session alive
Section titled “4. Refresh — keep the session alive”Before the access token expires, exchange the refresh token at Session API for a fresh access token and a new refresh token. /refresh does not require a client-auth JWT. Refresh tokens are rotated — the token you present is consumed, and the response returns its replacement. Persist the new refreshToken and use it for the next refresh; re-using a spent one revokes the whole session. Near-simultaneous concurrent refreshes of the same token (e.g. multiple tabs) are tolerated and converge on one session; only reuse after the replacement has been issued revokes it.
curl -X POST https://session-api.sudomimus.com/refresh \ -H "Content-Type: application/json" \ -d '{ "refreshToken": "..." }'const res = await fetch("https://session-api.sudomimus.com/refresh", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }),});
// The refresh token is rotated — capture the new one and persist it,// replacing the token you just sent.const { accessToken, refreshToken: newRefreshToken } = await res.json();await store.saveRefreshToken(newRefreshToken);res = requests.post( "https://session-api.sudomimus.com/refresh", json={"refreshToken": refresh_token},)
data = res.json()access_token = data["accessToken"]# The refresh token is rotated — persist the new one, replacing the old.store.save_refresh_token(data["refreshToken"])body, _ := json.Marshal(map[string]string{"refreshToken": refreshToken})
res, _ := http.Post( "https://session-api.sudomimus.com/refresh", "application/json", bytes.NewReader(body),)
var data struct { AccessToken string `json:"accessToken"` RefreshToken string `json:"refreshToken"`}json.NewDecoder(res.Body).Decode(&data)
// The refresh token is rotated — persist data.RefreshToken, replacing the old one.store.SaveRefreshToken(data.RefreshToken)For introspection, logout, and account-wide revocation, see Managing sessions.
Looking up application metadata
Section titled “Looking up application metadata”POST /info returns the localized public profile of an application given its anchor. It does not require a client-auth JWT, so it is safe to call from browsers and untrusted contexts. Signing keys deliberately live on the Session JWKS endpoint, not this metadata route.
curl -X POST https://connect-api.sudomimus.com/info \ -H "Content-Type: application/json" \ -d '{ "applicationAnchor": "your-application", "locale": "en-US" }'const res = await fetch("https://connect-api.sudomimus.com/info", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ applicationAnchor, locale: "en-US" }),});
const { applicationAnchor: anchor, applicationName } = await res.json();res = requests.post( "https://connect-api.sudomimus.com/info", json={"applicationAnchor": application_anchor, "locale": "en-US"},)
info = res.json()body, _ := json.Marshal(map[string]string{ "applicationAnchor": applicationAnchor, "locale": "en-US",})
res, _ := http.Post( "https://connect-api.sudomimus.com/info", "application/json", bytes.NewReader(body),)Use GET https://session-api.sudomimus.com/applications/{applicationAnchor}/jwks.json for token verification keys. Cache that response according to Cache-Control, select the JWT’s exact kid, and refresh once when an unknown kid appears.