---
title: Connect flow
description: The Connect protocol from Establish through Authenticate, Redeem,
  and Refresh, with end-to-end examples 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.

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.

## 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 | `connect-api POST /refresh` | A new access token and rotated refresh token |

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.

This lifecycle is specific to Connect. OIDC uses authorization code + PKCE, while native direct-issue exchanges a Steam ticket or AccessKey in one request.

## 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/reference/sdks/) (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">
```js
const res = await fetch("https://connect-api.sudomimus.com/establish", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Authorization": `SudomimusClientJWT ${await signEstablishJwt(body)}`,
    },
    body: JSON.stringify({
        applicationAnchor: process.env.SUDOMIMUS_APPLICATION_ANCHOR,
        returnMethods: [
            {
                type: "CALLBACK",
                payload: { callbackUrl: "https://your-app.com/auth/callback" },
            },
        ],
    }),
});

const { exposureKey, hiddenKey } = await res.json();
```
</TabItem>
<TabItem label="Python">
```python
import os, requests

res = requests.post(
    "https://connect-api.sudomimus.com/establish",
    headers={
        "Authorization": f"SudomimusClientJWT {sign_establish_jwt(body)}",
    },
    json={
        "applicationAnchor": os.environ["SUDOMIMUS_APPLICATION_ANCHOR"],
        "returnMethods": [
            {
                "type": "CALLBACK",
                "payload": {"callbackUrl": "https://your-app.com/auth/callback"},
            },
        ],
    },
)

data = res.json()
exposure_key = data["exposureKey"]
hidden_key = data["hiddenKey"]
```
</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 `callbackUrl` with `exposure-key` and `confirmation-key` appended as query parameters.

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

The access token is a signed JWT. Verify it using your application's token-signing public key — fetched once from `POST /info` and cached — then trust its claims. See [Tokens and verification](/en-us/concepts/tokens-and-verification/) for the full verification recipe, including the `kty: "Access"` header check.

## 4. Refresh — keep the session alive

Before the access token expires, exchange the refresh token 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://connect-api.sudomimus.com/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "..." }'
```
</TabItem>
<TabItem label="Node.js">
```js
const res = await fetch("https://connect-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://connect-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://connect-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 public profile of an application (name, public key, localized name) given its anchor. It does **not** require a client-auth JWT, so it is safe to call from browsers and untrusted contexts.

<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, applicationPublicKey } = 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>

The `applicationPublicKey` returned by `/info` is the key your backend uses to verify access tokens for this application. Cache it; refetch only after key rotation.