Skip to content

Native flows

View as Markdown

The Connect browser flow assumes you can redirect a browser to via.sudomimus.com and receive a callback. That works for web applications but is awkward for desktop apps, games, and headless tools.

A native client has three flows available, depending on what it can do. Two are Native API (native-api.sudomimus.com) one-shot direct-issue endpoints — Steam direct-issue and AccessKey direct-issue. The third, browser polling, is a Connect API flow (/establish/status-poll/redeem) that a native client drives itself; it does not run on Native API.

If your client is public and should not receive an application client-auth private key or a pre-issued AccessKey secret, use Device authorization instead. Device authorization is the code-confirmation flow for CLIs, launchers, and similar public clients.

FlowWhen to useRound-tripsEndpoint
Browser pollingAny native client with a system browser available3+ (establish + N×poll + redeem)connect POST /establish/status-poll/redeem
Steam direct-issueGame client running inside Steam, with access to Steamworks SDK1native-api POST /direct-issue/steam-ticket
AccessKey direct-issueCLI / headless service / launcher pre-provisioned with an application-scoped credential for a known existing account1native-api POST /direct-issue/access-key

When your native client can open the user’s system browser but cannot easily receive a callback URL, use the polling flow:

  1. The client backend calls connect POST /establish (signed with the application’s client-auth JWT) declaring a STATUS_POLL return method, and receives { exposureKey, hiddenKey }.
  2. The client opens the system browser pointed at https://via.sudomimus.com/?exposure-key=<exposureKey>.
  3. The user completes the passkey or email-OTP challenge in the browser.
  4. The client polls connect POST /status-poll every few seconds with { exposureKey, hiddenKey }. Once the user finishes, the poll returns { status: "REALIZED", confirmationKey }.
  5. The client then redeems the three keys at connect POST /redeem for { 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:

Terminal window
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:

Terminal window
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).

For games shipped through Steam, Sudomimus supports a silent login that does not open a browser at all. The user does not see a login prompt; their Steam identity is exchanged directly for a Sudomimus session.

Terminal window
curl -X POST https://native-api.sudomimus.com/direct-issue/steam-ticket \
-H "Content-Type: application/json" \
-d '{
"applicationAnchor": "my-game",
"steamTicketHex": "<hex-encoded ticket from Steamworks GetAuthTicketForWebApi>",
"steamAppId": 480
}'

The flow is:

  1. The game calls Steamworks ISteamUser::GetAuthTicketForWebApi("sudomimus")not GetAuthSessionTicket. The two are different ticket types and are not interchangeable. The identity string must be exactly "sudomimus" (case-sensitive); other values are rejected.
  2. The game waits for the GetTicketForWebApiResponse_t callback before using the ticket.
  3. The ticket bytes are hex-encoded and sent as steamTicketHex to POST /direct-issue/steam-ticket, together with applicationAnchor and steamAppId.
  4. Sudomimus verifies the ticket with Steam, looks up or creates the account, and — on the happy path — returns { accessToken, refreshToken } in one round trip. If the application requires consent or profile data the Steam account has not provided, this returns a 403 with an Errand handoff instead — see When direct-issue needs consent or profile data.
  5. The game calls Steamworks.CancelAuthTicket(handle) after receiving the tokens.

The user never leaves the game. The Steam account is the source of identity.

The application must have:

  • Layer 1: a STEAM_TICKET AuthenticationRule with allowedSteamAppIds: number[] containing this game’s Steam App ID.
  • Layer 2: a rule that will match — typically STEAM_ID with allowedSteamIds: ["*"] for any verified Steam account, or a list of specific SteamID64 strings.
  • Layer 3: a DIRECT_ISSUE ReturnRule.

A Steam-first account that has never linked an email needs a STEAM_ID (or ACCOUNT_ALIAS / SECTOR_SUBJECT) Layer 2 rule; an EMAIL-only Layer 2 will reject it.

This endpoint does not require a client-auth JWT — the Steam ticket itself attests both the user and the binary’s right to talk to the application.

Web counterpart. The browser-side “Sign in with Steam” button uses the STEAM_OPENID Layer 1 method instead of STEAM_TICKET. Both paths land on the same per-user Steam identity, so a user who first signed in through a game can subsequently sign in through the web button (and vice versa) without any account-linking step. See Authentication rules for the STEAM_OPENID rule shape.

For environments without a Steam ticket but where the target Sudomimus account is already known — CLI tools, custom launchers, headless services, automated test rigs. The “proof” is a Sudomimus-issued credential pair (accessKeyIdentifier + accessKeySecret) generated from the developer portal and handed out-of-band to the operator.

Terminal window
curl -X POST https://native-api.sudomimus.com/direct-issue/access-key \
-H "Content-Type: application/json" \
-d '{
"applicationAnchor": "my-cli-tool",
"accessKeyIdentifier": "acs_k_<uuidv4>",
"accessKeySecret": "acs_t_<64-char lowercase hex>"
}'

Both credential strings carry mandatory prefixes:

  • acs_k_ — the public identifier, followed by a UUIDv4.
  • acs_t_ — the secret half, followed by 64 lowercase hex characters (shown exactly once at creation; unrecoverable afterwards).

The prefixes are part of the canonical form. They make the two halves visually distinguishable and let secret scanners match accidentally-committed credentials by literal substring.

The application must have:

  • Layer 1: an ACCESS_KEY_DIRECT AuthenticationRule (empty payload). Default-deny unless explicitly added.
  • Layer 2: a rule matching the target account — EMAIL, STEAM_ID, ACCOUNT_ALIAS, or SECTOR_SUBJECT.
  • Layer 3: a DIRECT_ISSUE ReturnRule.

AccessKey credentials cannot create new accounts. The credential is issued against an existing Sudomimus account; if that account is deleted, every credential bound to it is rejected at login time.

Credentials are managed from with.sudomimus.com on the application detail page → Access keys tab. Revocation is a soft delete (revokedAt timestamp); rotation = revoke + reissue. Expired credentials are not auto-evicted but are rejected at the handler.

This endpoint does not require a client-auth JWT either — the access-key secret is itself the credential. Embedding the client-auth private key in a distributable CLI would be reversible by any operator anyway.

Section titled “When direct-issue needs consent or profile data”

Both direct-issue endpoints are one-shot readers — they cannot pop a consent screen or ask the user to type in an email. So when an application requires a claim the user has not granted, or requires data the account does not have yet (a Steam account with no email, for instance), the call cannot just succeed. Instead it returns a 403 carrying an Errand — a short-lived browser side-trip where the user completes that work:

{
"reason": "ClaimConsentRequired",
"claims": {
"email": { "requirement": "REQUIRED", "state": "UNKNOWN" },
"firstName": { "requirement": "OPTIONAL", "state": "UNKNOWN" },
"lastName": { "requirement": "OFF", "state": "UNKNOWN" }
},
"errand": {
"errandKey": "ernd_...",
"url": "https://via.sudomimus.com/errand?key=ernd_...",
"expiresAt": "2026-06-10T12:30:00Z"
}
}

The reason is one of ClaimConsentRequired (the user must agree to share a required claim) or RequiredClaimDataMissing (consent is there, but the account data is not). Both Steam and AccessKey direct-issue behave identically here. To recover:

  1. Open errand.url in the user’s system browser. The page walks the user through any sign-in, data entry, and consent that is owed.
  2. Poll GET /errand/{errandKey}/status (native-api) every ~2 seconds until it reports COMPLETED — or just let the user tell your UI they’re done.
  3. Retry the same direct-issue call once. It now succeeds with { accessToken, refreshToken }.
Terminal window
curl https://native-api.sudomimus.com/errand/ernd_.../status
# → { "status": "PENDING" } user still working in the browser
# → { "status": "COMPLETED" } done — retry the direct-issue
# → { "status": "EXPIRED" } expired/consumed/unknown — re-run direct-issue for a fresh errand

A 200 from either endpoint also carries a claims block (the same shape as in the 403), so even on success you can see which optional claims were shared and which were withheld. The full contract — the 30-minute lifetime, why retries reuse the same errandKey, and the two security tiers (consent-only vs. sign-in-required) — is in The Errand.

Your client is…Use
A web applicationConnect + via.sudomimus.com (regular flow)
A desktop app / Electron / mobile with a browser availableConnect + via.sudomimus.com via system browser + connect /status-poll
A public CLI / launcher without a client secretDevice authorization
A Steam-distributed gamenative-api POST /direct-issue/steam-ticket (silent, one round trip)
A CLI / headless service tied to a known account by a pre-issued credentialnative-api POST /direct-issue/access-key (one round trip with an AccessKey)

In all cases the token format returned at the end is the same — your application only ever deals with the same access-token shape, regardless of how the user authenticated.