Settings Flow

The Settings flow lets your end-users update their credentials and profile traits directly within your application. The entire interaction stays in your UI — no redirects to Verne-hosted pages.

CapabilitySupported
Change passwordYes
Update profile traits (email, display name, etc.)Yes
Link / unlink social providers (OIDC)Yes
Enrol an authenticator app (TOTP)Yes — when the tenant has MFA enabled
Generate backup codesYes — same condition

How It Works

Settings flow requires an active user session. The session token is obtained after a successful login or recovery.

Your backend                  Edge Gateway              Kratos
     │                              │                      │
     │── GET /v1/gate/auth/settings ─▶                     │
     │   Authorization: Bearer <api_key>                   │
     │   X-Session-Token: <kratos_session_token>           │
     │                              │── GET /self-service/settings/api ─▶
     │                              │   X-Session-Token: ...
     │                              │◀─ 200 flow JSON ──────
     │◀── 200 flow JSON ────────────│                      │
     │                              │                      │
     │── POST /v1/gate/auth/settings/submit?flow=<id> ─▶   │
     │   X-Session-Token: <kratos_session_token>           │
     │   { "method": "password", "password": "..." }       │
     │                              │── POST /self-service/settings?flow=<id> ─▶
     │                              │◀─ 200 updated identity ──
     │◀── 200 updated identity ─────│                      │

Start the flow with your key. The settings flow of an end user has to be started with your tenant key (GET /v1/gate/auth/settings). That is how Gate knows which tenant the user belongs to when they change their email address. A flow started any other way — straight on /auth/self-service/settings/api, for example — is refused when a profile change is submitted to it: the answer is 400 with message 4000002 in the flow's ui.messages, and nothing is saved. The refusal text is Settings must be started with a tenant's Gate key (GET /v1/gate/auth/settings).

Security: Your Gate API key never reaches the browser. The Kratos session token is user-scoped — it cannot access other users' data. Before it proxies anything to Kratos, the Edge Gateway verifies that the session belongs to one of your tenant's end users and that the settings flow belongs to your tenant.


API Reference

Initiate Settings Flow

GET /v1/gate/auth/settings

Creates a Kratos settings flow for the authenticated user and returns the flow descriptor with all available UI nodes.

Required headers

HeaderValue
AuthorizationBearer vrn_9f8a7... — your tenant API key
X-Session-TokenThe user's Kratos session token from a prior login

Response (200 OK)

Returns the Kratos flow object. The ui.nodes array contains input nodes grouped by method:

GroupContents
defaultCSRF token
passwordNew password input + submit button
profileEditable trait inputs (email, name, etc.) + submit button
oidcSocial provider link/unlink buttons (only for enabled providers)
{
  "id": "flow_abc123",
  "type": "api",
  "expires_at": "2026-04-11T14:00:00Z",
  "ui": {
    "action": "https://...",
    "method": "POST",
    "nodes": [
      { "group": "default",  "attributes": { "name": "csrf_token", ... } },
      { "group": "password", "attributes": { "name": "password",   ... } },
      { "group": "profile",  "attributes": { "name": "traits.email", ... } }
    ]
  }
}

Example

curl https://api.vernesoft.com/v1/gate/auth/settings \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'X-Session-Token: st_xyzabc...'

Submit Settings Flow

POST /v1/gate/auth/settings/submit?flow={flow_id}

Submits a settings change to Kratos. The flow_id must match the ID returned by the initiation step above.

Required headers

HeaderValue
AuthorizationBearer vrn_9f8a7... — your tenant API key
X-Session-TokenThe same session token used to initiate the flow
Content-Typeapplication/json

Request body

Change password:

{
  "method": "password",
  "password": "new-secure-password-123!"
}

Update profile traits:

{
  "method": "profile",
  "traits": {
    "email": "new-email@example.com"
  }
}

Changing the email address changes the address the user signs in with, and the one verification and recovery mail go to. The same limits apply as when you create a user: an address that cannot be used is refused with 400 and message 4000010 in ui.messages, with the text listed under Create Identity. If the new address belongs to another tenant's user, that is no obstacle: it is a different person.

The address can be sent nested, as above, or flat ("traits.email": "new-email@example.com"). Use one spelling per request.

Response (200 OK)

On success Kratos returns the updated identity and session state.

{
  "identity": {
    "id": "identity_123",
    "traits": { "email": "new-email@example.com", "tenant_id": "ten_001" }
  },
  "session": { ... }
}

If the submitted value fails validation (e.g. password too weak), Kratos returns 400 or 422 with the flow object containing error messages in the relevant UI nodes.

Example

curl -X POST 'https://api.vernesoft.com/v1/gate/auth/settings/submit?flow=flow_abc123' \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'X-Session-Token: st_xyzabc...' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "password", "password": "NewPassword456!" }'

Integration Guide

A typical "change password" screen in your application:

// ─── 1. Backend ───────────────────────────────────────────────────────────────
// Both calls must be made server-side — the API key must not leave your backend.

const GATE_API = 'https://api.vernesoft.com';
const GATE_KEY = 'vrn_9f8a7...'; // Dashboard → API Keys, with Users & sign-in ticked

// Store the session token in your session after the user logs in.
// It comes back as `session_token` when your app submits the user's credentials
// to the login flow's own `ui.action` URL — see "Where Does the Session Token
// Come From?" below.

async function initSettingsFlow(sessionToken) {
  const res = await fetch(`${GATE_API}/v1/gate/auth/settings`, {
    headers: {
      Authorization: `Bearer ${GATE_KEY}`,
      'X-Session-Token': sessionToken,
    },
  });
  if (!res.ok) throw new Error(`Gate error: ${res.status}`);
  return res.json();
}

async function submitSettingsFlow(flowId, sessionToken, body) {
  const res = await fetch(
    `${GATE_API}/v1/gate/auth/settings/submit?flow=${flowId}`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${GATE_KEY}`,
        'X-Session-Token': sessionToken,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    }
  );
  return { status: res.status, data: await res.json() };
}

// ─── Example: Express route handlers ──────────────────────────────────────────

// GET /account/settings/flow
app.get('/account/settings/flow', requireAuth, async (req, res) => {
  const flow = await initSettingsFlow(req.session.kratosToken);
  res.json(flow);
});

// POST /account/settings/password
app.post('/account/settings/password', requireAuth, async (req, res) => {
  const flow = await initSettingsFlow(req.session.kratosToken);
  const result = await submitSettingsFlow(flow.id, req.session.kratosToken, {
    method: 'password',
    password: req.body.new_password,
  });
  res.status(result.status).json(result.data);
});
// ─── 2. Frontend ──────────────────────────────────────────────────────────────
// Your change-password form — fetch the flow from your server, submit to server.

async function changePassword(newPassword) {
  // 1. Get flow from your backend
  const flow = await fetch('/account/settings/flow').then(r => r.json());

  // 2. Submit the password change through your backend
  const result = await fetch('/account/settings/password', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ new_password: newPassword }),
  }).then(r => r.json());

  if (result.ui?.messages?.some(m => m.type === 'error')) {
    // Show validation errors from Kratos
    const msg = result.ui.messages.map(m => m.text).join(', ');
    throw new Error(msg);
  }

  return result; // success
}

Where Does the Session Token Come From?

The Kratos session token is returned when a user successfully logs in or recovers their account:

  • Login flow — start it with GET /v1/gate/auth/login, then submit the user's credentials to the ui.action URL the flow carries (a Kratos public route under /auth/*). There is no /v1/gate/auth/login/submit endpoint — the action URL is the submit target, and its successful response holds session_token at the top level, next to the session object. The MFA guide shows the full exchange.
  • Recovery flow — a recovery that ends with a correct code answers 200 with a continue_with list; the item set_ory_session_token carries the session token (ory_session_token) that opens a settings flow, so a user who has just recovered their account can be taken straight to a password change from your backend.

Store the session token server-side (e.g. in your session store). Do not expose it to the browser.


Error Handling

HTTP statusMeaning
400Missing or malformed X-Session-Token header. Also, with message 4000002 in ui.messages: a profile change submitted to a flow that was not started with your key; with 4000010: a new email address that cannot be used
401unauthorized, "That session is not active." — the session token is expired, revoked or invalid, or it is not a session of one of your tenant's end users (another tenant's user, a dashboard account). The two cases are answered identically on purpose. The user must log in again
403Flow does not belong to this tenant, or has expired (TTL: 1 hour)
422Validation error (e.g. password too short) — check ui.messages in the response body
502upstream_unavailable — Kratos (or the lookup of whose session it is) did not answer; transient, retry with back-off. Never a statement about the token