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.
| Capability | Supported |
|---|---|
| Change password | Yes |
| 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 codes | Yes — 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
| Header | Value |
|---|---|
Authorization | Bearer vrn_9f8a7... — your tenant API key |
X-Session-Token | The 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:
| Group | Contents |
|---|---|
default | CSRF token |
password | New password input + submit button |
profile | Editable trait inputs (email, name, etc.) + submit button |
oidc | Social 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
| Header | Value |
|---|---|
Authorization | Bearer vrn_9f8a7... — your tenant API key |
X-Session-Token | The same session token used to initiate the flow |
Content-Type | application/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 theui.actionURL the flow carries (a Kratos public route under/auth/*). There is no/v1/gate/auth/login/submitendpoint — the action URL is the submit target, and its successful response holdssession_tokenat the top level, next to thesessionobject. The MFA guide shows the full exchange. - Recovery flow — a recovery that ends with a
correct code answers
200with acontinue_withlist; the itemset_ory_session_tokencarries 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 status | Meaning |
|---|---|
400 | Missing 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 |
401 | unauthorized, "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 |
403 | Flow does not belong to this tenant, or has expired (TTL: 1 hour) |
422 | Validation error (e.g. password too short) — check ui.messages in the response body |
502 | upstream_unavailable — Kratos (or the lookup of whose session it is) did not answer; transient, retry with back-off. Never a statement about the token |