---
title: Connect flow
description: The Connect protocol from Establish through Authenticate and
  Redeem, plus the Session API refresh step for web applications.
editUrl: true
head: []
template: doc
sidebar:
  order: 1
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

import { Tabs, TabItem } from "@astrojs/starlight/components";

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. [Choose a framework or API SDK](/en-us/sdk/overview/), or see the [TypeScript API SDK](/en-us/sdk/typescript/) for `@sudomimus/connect`.

If you're building a native client (desktop, game, CLI), see [Native clients](/en-us/native/overview/). For OIDC, see [OIDC relying parties](/en-us/oidc/flow/).

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](/en-us/with-portal/application-lifecycle/). New applications remain `DRAFT` until explicitly activated; login requires `ACTIVE` and available parent organization and sector.

## 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:

```mermaid
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`, stores `hiddenKey`, redeems the completed inquiry, and verifies the resulting tokens.
- **The browser** carries `exposureKey` to the hosted authentication UI but never sees `hiddenKey`.
- **`via.sudomimus.com`** runs the passkey, email OTP, OAuth, or federation challenge and creates `confirmationKey` only 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

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

:::caution[`/establish` requires a signed client-auth JWT]
Every `/establish` request must carry `Authorization: SudomimusClientJWT <jwt>`, where `<jwt>` is an RS256 JWT signed with your application's client-auth private key. Required claims: `iss = applicationAnchor`, `aud = "sudomimus-connect"`, `iat`, `exp` (≤ 60s from `iat`), `jti` (UUID, replay-protected), `body_sha256` (base64 SHA-256 of the raw HTTP body).

Hand-rolling this in 4 lines of curl is impractical. The examples below assume `$SUDOMIMUS_CLIENT_AUTH_JWT` is produced by your signing code; in real integrations, use [`@sudomimus/connect`](/en-us/sdk/typescript/) which signs internally. Unsigned requests are rejected with HTTP 401.
:::

:::note[`applicationAnchor` format]
The anchor is the stable identifier you chose when creating the application at [`with.sudomimus.com`](https://with.sudomimus.com). It is lowercase kebab-case (`[a-z][a-z0-9-]*`, 3–64 characters, no leading/trailing/consecutive hyphens) and globally unique — for example `my-app` or `acme-checkout`.
:::

<Tabs syncKey="lang">
<TabItem label="curl">
```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": "CALLBACK",
        "payload": { "callbackUrl": "https://your-app.com/auth/callback" }
      }
    ]
  }'
```
</TabItem>
<TabItem label="Node.js">
{/* CONNECT_ESTABLISH_NODE_EXAMPLE_START */}
```js
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();
```
{/* CONNECT_ESTABLISH_NODE_EXAMPLE_END */}
</TabItem>
<TabItem label="Python">
{/* CONNECT_ESTABLISH_PYTHON_EXAMPLE_START */}
```python
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"]
```
{/* CONNECT_ESTABLISH_PYTHON_EXAMPLE_END */}
</TabItem>
<TabItem label="Go">
```go
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)
```
</TabItem>
</Tabs>

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`

Redirect the user's browser to `via.sudomimus.com` with the exposure key. The user completes the passkey or email-OTP challenge there.

<Tabs syncKey="lang">
<TabItem label="curl">
```text
# No HTTP call — this is a 302 redirect from your application:
Location: https://via.sudomimus.com/?exposure-key=<exposureKey>
```
</TabItem>
<TabItem label="Node.js">
```js
const authUrl = new URL("https://via.sudomimus.com/");
authUrl.searchParams.set("exposure-key", exposureKey);

return Response.redirect(authUrl.toString(), 302);
```
</TabItem>
<TabItem label="Python">
```python
from urllib.parse import urlencode
from flask import redirect

return redirect(
    "https://via.sudomimus.com/?" + urlencode({"exposure-key": exposure_key}),
    code=302,
)
```
</TabItem>
<TabItem label="Go">
```go
http.Redirect(
    w, r,
    "https://via.sudomimus.com/?exposure-key="+url.QueryEscape(exposureKey),
    http.StatusFound,
)
```
</TabItem>
</Tabs>

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.

:::note[URL params are kebab-case]
Query parameters travelling through the browser URL use kebab-case (`exposure-key`, `confirmation-key`). The JSON fields on API request/response bodies use camelCase (`exposureKey`, `confirmationKey`).
:::

## 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.

<Tabs syncKey="lang">
<TabItem label="curl">
```bash
curl -X POST https://connect-api.sudomimus.com/redeem \
  -H "Content-Type: application/json" \
  -d '{
    "exposureKey": "...",
    "hiddenKey": "...",
    "confirmationKey": "..."
  }'
```
</TabItem>
<TabItem label="Node.js">
```js
// 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();
```
</TabItem>
<TabItem label="Python">
```python
# 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"]
```
</TabItem>
<TabItem label="Go">
```go
// 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)
```
</TabItem>
</Tabs>

Treat the returned tokens as opaque credentials until they have passed the complete [token verification procedure](/en-us/concepts/tokens-and-verification/#verification), including `typ` and actor-shape checks.

## 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.

<Tabs syncKey="lang">
<TabItem label="curl">
```bash
curl -X POST https://session-api.sudomimus.com/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "..." }'
```
</TabItem>
<TabItem label="Node.js">
```js
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);
```
</TabItem>
<TabItem label="Python">
```python
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"])
```
</TabItem>
<TabItem label="Go">
```go
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)
```
</TabItem>
</Tabs>

For introspection, logout, and account-wide revocation, see [Managing sessions](/en-us/guides/managing-sessions/).

## 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.

<Tabs syncKey="lang">
<TabItem label="curl">
```bash
curl -X POST https://connect-api.sudomimus.com/info \
  -H "Content-Type: application/json" \
  -d '{ "applicationAnchor": "your-application", "locale": "en-US" }'
```
</TabItem>
<TabItem label="Node.js">
```js
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();
```
</TabItem>
<TabItem label="Python">
```python
res = requests.post(
    "https://connect-api.sudomimus.com/info",
    json={"applicationAnchor": application_anchor, "locale": "en-US"},
)

info = res.json()
```
</TabItem>
<TabItem label="Go">
```go
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),
)
```
</TabItem>
</Tabs>

Use the application-specific Session JWKS described in [Tokens and verification](/en-us/concepts/tokens-and-verification/#verification); that page owns the cache and unknown-`kid` behavior.

## 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).