Skip to content

Native flows

View as Markdown

Native API provides three direct-issue methods. Choose Steam for a Steam game, AccessKey for an issued secret pair, or PublicKey for an Ed25519 key you control. AccessKey and PublicKey can authenticate an Account, Agent, or Automation.

Credential Endpoint
Steam ticket POST /direct-issue/steam-ticket
AccessKey POST /direct-issue/access-key
PublicKey POST /direct-issue/public-key

Configure all three rule layers and credentials, then have an organization OWNER take the application live. The application must be ACTIVE, with available parent organization and sector. Every direct-issue method requires a DIRECT_ISSUE ReturnRule and an applicable Layer 2 rule for the account.

Layer 1 must allow the exact method below. Native derives it from the credential’s bound principal; callers cannot choose another principal kind. These rules have empty payloads.

Principal AccessKey PublicKey
Account ACCESS_KEY_DIRECT PUBLIC_KEY_DIRECT
Agent AGENT_ACCESS_KEY_DIRECT AGENT_PUBLIC_KEY_DIRECT
Automation AUTOMATION_ACCESS_KEY_DIRECT AUTOMATION_PUBLIC_KEY_DIRECT

For a confidential backend that can sign /establish, use Connect browser polling. Public clients without that backend should use Device authorization.

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 Steam account is the source of identity; required consent or missing profile data may still need the browser Errand.

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. It is returned by the create operation and an exact retry for up to ten minutes, but never by ordinary key reads.

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: the exact AccessKey AuthenticationRule for the credential principal from the table above (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 under Programmatic access → Access keys; see Manage access keys. 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.

Register an Ed25519 public key under Programmatic access → Public keys, choosing the Account, Agent, or Automation and its application/sector coverage. Keep the private key in your own system.

  1. Serialize the request body, for example {"applicationAnchor":"my-app"}.
  2. Sign an EdDSA JWT with header alg: "EdDSA", typ: "vnd.sudomimus.public-key-assertion+jwt", and kid equal to the registered pky_... credential identifier.
  3. Set iss equal to kid, aud to sudomimus-native-public-key, current iat, exp at most 60 seconds later, a fresh random 128-bit base64url jti, and requestHash to the base64url SHA-256 digest of the exact request body bytes.
  4. POST those same bytes to https://native-api.sudomimus.com/direct-issue/public-key with Content-Type: application/json and Authorization: SudomimusPublicKeyJWT <assertion>.

Every new attempt requires a fresh assertion and jti. The private key authenticates issuance; the resulting access token remains a bearer token. Exact request and error shapes are in the Native API reference.

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

Direct-issue endpoints authenticate a credential in one request — 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" },
"staticAvatar": { "requirement": "SYNTHETIC_ONLY", "state": "UNKNOWN" },
"animatedAvatar": { "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). Steam, AccessKey, and PublicKey direct-issue can return this handoff, but their retry credentials differ. 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 direct-issue once with the appropriate proof: Steam must obtain a fresh ticket from GetAuthTicketForWebApi; AccessKey may reuse its still-valid identifier and secret; PublicKey must sign a fresh assertion with a new jti. Never replay the original Steam ticket, even if it returned an Errand. Current credential, application, rule, and claim checks still apply.
Terminal window
curl https://native-api.sudomimus.com/errand/ernd_.../status
# → { "status": "PENDING" } user still working in the browser
# → { "status": "COMPLETED" } done — retry with the credential-specific proof
# → { "status": "EXPIRED" } expired/consumed/unknown — re-run with the credential-specific proof

A 200 from a direct-issue 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, when an eligible retry reuses the same errandKey, and the two security tiers (consent-only vs. sign-in-required) — is in The Errand.

Account access tokens use typ: vnd.sudomimus.application-access+jwt. Agent and Automation access tokens use typ: vnd.sudomimus.workload-access+jwt and add act.sub, the pairwise actor subject. In both cases sub identifies the owner account within the sector. Applications must explicitly admit the appropriate token type and enforce their own business permissions; credential scope does not grant those permissions.

Refresh tokens use vnd.sudomimus.application-refresh+jwt and contain neither owner nor actor subject. Fetch current owner profile claims from Session UserInfo. Follow Tokens and verification for signature and authority checks.