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.
Before starting login, configure the rules and credentials for your chosen flow, then have an organization OWNER take the application live. New applications remain DRAFT until explicitly activated; login requires ACTIVE and available parent organization and sector.
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 |
The sequence below shows the standard CALLBACK path used by the examples on this page:
sequenceDiagram
autonumber
participant App as Application backend
participant Browser as User browser
participant Connect as Connect API
participant Via as via.sudomimus.com
participant Session as Session API
App->>Connect: POST /establish<br/>client-auth JWT + return method
Connect-->>App: exposureKey + hiddenKey
Note over App: Keep hiddenKey server-side
App-->>Browser: 302 redirect with exposureKey
Browser->>Via: Open hosted authentication UI
Note over Browser,Via: User completes an allowed challenge
Via-->>Browser: 302 to application callback<br/>exposureKey + confirmationKey
Browser->>App: GET application callback
App->>Connect: POST /redeem<br/>exposureKey + hiddenKey + confirmationKey
Connect-->>App: accessToken + refreshToken
App->>Session: GET application JWKS
Session-->>App: Public verification keys
Note over App: Verify the access token locally
loop Before the access token expires
App->>Session: POST /refresh<br/>current refreshToken
Session-->>App: new accessToken + rotated refreshToken
end
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, AccessKey, or PublicKey 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 body = JSON.stringify({ applicationAnchor: process.env.SUDOMIMUS_APPLICATION_ANCHOR, 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,});
const { exposureKey, hiddenKey } = await res.json();import json, os, requests
body = json.dumps({ "applicationAnchor": os.environ["SUDOMIMUS_APPLICATION_ANCHOR"], "returnMethods": [ { "type": "CALLBACK", "payload": {"callbackUrl": "https://your-app.com/auth/callback"}, }, ],}, separators=(",", ":")).encode("utf-8")
res = requests.post( "https://connect-api.sudomimus.com/establish", headers={ "Content-Type": "application/json", "Authorization": f"SudomimusClientJWT {sign_establish_jwt(body)}", }, data=body,)
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. Require typ: "vnd.sudomimus.application-access+jwt" for Account-only endpoints. If an endpoint intentionally admits Agent or Automation tokens, also accept typ: "vnd.sudomimus.workload-access+jwt" and handle its act.sub actor. See Tokens and verification for the full verification and cache-refresh recipe.
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.
Browser polling
Section titled “Browser polling”When your native client can open the user’s system browser but cannot easily receive a callback URL, use the polling flow:
- The client backend calls
connect POST /establish(signed with the application’s client-auth JWT) declaring aSTATUS_POLLreturn method, and receives{ exposureKey, hiddenKey }. - The client opens the system browser pointed at
https://via.sudomimus.com/?exposure-key=<exposureKey>. - The user completes the passkey or email-OTP challenge in the browser.
- The client polls
connect POST /status-pollevery few seconds with{ exposureKey, hiddenKey }. Once the user finishes, the poll returns{ status: "REALIZED", confirmationKey }. - The client then redeems the three keys at
connect POST /redeemfor{ accessToken, refreshToken }.
This works on any platform with a default browser — Windows, macOS, Linux desktop apps, Electron, etc. The application’s Layer 3 rules must allow STATUS_POLL.
The /establish call is the standard client-auth-signed Connect request — see Web applications for the full shape — except the return method is STATUS_POLL:
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": "STATUS_POLL", "payload": {} } ] }'# → { "exposureKey": "exp_...", "hiddenKey": "hid_..." }Then poll /status-poll with those two keys every few seconds. The poll carries no client-auth JWT — possession of the hiddenKey is what authorizes it:
curl -X POST https://connect-api.sudomimus.com/status-poll \ -H "Content-Type: application/json" \ -d '{ "exposureKey": "exp_...", "hiddenKey": "hid_..." }'
# While the user is still authenticating in the browser:# { "status": "PENDING" }# Once they finish:# { "status": "REALIZED", "confirmationKey": "cnf_..." }When the poll returns REALIZED, redeem the three keys at connect POST /redeem for the access and refresh tokens (same /redeem call as the web flow).