---
title: Native flows
description: Choose browser-polling Connect, Steam ticket direct-issue, or
  AccessKey direct-issue for desktop apps, games, CLIs, and services.
editUrl: true
head: []
template: doc
sidebar:
  order: 1
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

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](/en-us/device/flow/) instead. Device authorization is the code-confirmation flow for CLIs, launchers, and similar public clients.

| Flow | When to use | Round-trips | Endpoint |
|---|---|---|---|
| **Browser polling** | Any native client with a system browser available | 3+ (establish + N×poll + redeem) | `connect POST /establish` → `/status-poll` → `/redeem` |
| **Steam direct-issue** | Game client running inside Steam, with access to Steamworks SDK | 1 | `native-api POST /direct-issue/steam-ticket` |
| **AccessKey direct-issue** | CLI / headless service / launcher pre-provisioned with an application-scoped credential for a known existing account | 1 | `native-api POST /direct-issue/access-key` |

## Browser polling

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](/en-us/connect/flow/) for the full shape — except the return method is `STATUS_POLL`:

```bash
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:

```bash
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).

## Steam direct-issue

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.

```bash
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](#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.

### Application configuration for Steam

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](/en-us/application-rules/authentication-rules/) for the `STEAM_OPENID` rule shape.

## AccessKey direct-issue

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.

```bash
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.

### Application configuration for AccessKey

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`](https://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.

## 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](/en-us/concepts/identity-claims/) 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](/en-us/native/claims-and-errand/)** — a short-lived browser side-trip where the user completes that work:

```json
{
  "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 }`.

```bash
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](/en-us/native/claims-and-errand/).

## When to use which

| Your client is… | Use |
|---|---|
| A web application | Connect + `via.sudomimus.com` (regular flow) |
| A desktop app / Electron / mobile with a browser available | Connect + `via.sudomimus.com` via system browser + `connect /status-poll` |
| A public CLI / launcher without a client secret | [Device authorization](/en-us/device/flow/) |
| A Steam-distributed game | `native-api POST /direct-issue/steam-ticket` (silent, one round trip) |
| A CLI / headless service tied to a known account by a pre-issued credential | `native-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.