+{% endblock footer %}
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..280d411b
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,178 @@
+import ast
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath("../src"))
+sys.path.insert(0, os.path.abspath("."))
+
+# -- Project info -------------------------------------------------------
+
+project = "auth0-server-python"
+author = "Auth0"
+copyright = "2024, Auth0"
+release = "1.0.0b18"
+
+# -- Extensions ---------------------------------------------------------
+
+extensions = [
+ "autoapi.extension",
+ "sphinx.ext.napoleon",
+ "sphinx.ext.linkcode",
+ "mintlify_export",
+]
+
+# -- AutoAPI ------------------------------------------------------------
+
+autoapi_dirs = ["../src"]
+autoapi_type = "python"
+autoapi_root = "reference"
+autoapi_ignore = ["**/tests/**", "**/test_*.py"]
+
+autoapi_options = [
+ "members",
+ "undoc-members",
+ "show-inheritance",
+]
+autoapi_member_order = "groupwise"
+autoapi_keep_files = True
+autoapi_template_dir = "_templates/autoapi"
+autoapi_python_class_content = "class"
+
+# Top-level import package, shown in each module's import block.
+IMPORT_ROOT = "auth0_server_python"
+
+
+def autoapi_prepare_jinja_env(jinja_env):
+ jinja_env.globals["import_root"] = IMPORT_ROOT
+
+
+
+def autoapi_skip_member(app, what, name, obj, skip, options):
+ if what == "class" and name.endswith(".Config"):
+ return True
+ return skip
+
+
+# -- Signatures ---------------------------------------------------------
+
+# Show `Name(...)` not `module.Name(...)`. The import block gives the full path.
+add_module_names = False
+
+# -- Source links -------------------------------------------------------
+
+REPO_ROOT = os.path.abspath("..")
+GITHUB_BLOB_URL = "https://github.com/auth0/auth0-server-python/blob/main"
+
+
+def _definition_line(path, fullname):
+ """Line where ``fullname`` (``Class.method``) is defined as a class or function, or None."""
+ with open(path) as f:
+ nodes = ast.parse(f.read()).body
+ node = None
+ for name in fullname.split("."):
+ node = next(
+ (
+ n
+ for n in nodes
+ if isinstance(n, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef))
+ and n.name == name
+ ),
+ None,
+ )
+ if node is None:
+ return None
+ nodes = node.body
+ return node.lineno if node else None
+
+
+def linkcode_resolve(domain, info):
+ if domain != "py" or not info.get("module"):
+ return None
+ base = os.path.join(REPO_ROOT, "src", IMPORT_ROOT, *info["module"].split("."))
+ path = base + ".py" if os.path.isfile(base + ".py") else os.path.join(base, "__init__.py")
+ if not os.path.isfile(path):
+ return None
+ # Attributes and fields have no definition of their own, so they get no link.
+ line = _definition_line(path, info.get("fullname", ""))
+ if line is None:
+ return None
+ return f"{GITHUB_BLOB_URL}/{os.path.relpath(path, REPO_ROOT)}#L{line}"
+
+
+# -- Napoleon (docstring style) -----------------------------------------
+
+napoleon_google_docstring = True
+napoleon_use_param = True
+napoleon_use_rtype = True
+napoleon_preprocess_types = True
+
+# -- General ------------------------------------------------------------
+
+templates_path = ["_templates"]
+
+exclude_patterns = [
+ "_build",
+ "_templates",
+ "Thumbs.db",
+ ".DS_Store",
+ "reference/index.rst",
+ "reference/auth_schemes/index.rst",
+ "reference/auth_server/index.rst",
+ "reference/auth_server/anonymous/index.rst",
+ "reference/auth_server/anonymous/helpers/index.rst",
+ "reference/encryption/index.rst",
+ "reference/store/index.rst",
+ "reference/utils/index.rst",
+]
+
+# -- Docs site export (see mintlify_export/) ---------------------------
+
+SITE_DIR = "docs/sdk/python/server"
+
+mintlify_site_dir = SITE_DIR
+mintlify_github_blob_url = GITHUB_BLOB_URL
+mintlify_guides_root = REPO_ROOT
+
+mintlify_remove_pages = []
+
+# -- Guides (README, CONTRIBUTING, examples/*.md) -----------------------
+
+EXAMPLE_PAGES = sorted(
+ f[:-3] for f in os.listdir(os.path.join(mintlify_guides_root, "examples")) if f.endswith(".md")
+)
+
+# Output page -> (Markdown source relative to the repo root, fixed title or "").
+mintlify_guides = {
+ "overview": ("README.md", "Overview"),
+ "CONTRIBUTING": ("CONTRIBUTING.md", ""),
+ **{f"examples/{name}": (f"examples/{name}.md", "") for name in EXAMPLE_PAGES},
+}
+
+# -- Navigation ---------------------------------------------------------
+
+# Guides starts folded and API Reference starts open. Nested groups are
+# what the docs site renders as foldable. "{reference}" is replaced by the
+# API reference groups, which are built from the generated pages.
+mintlify_navigation = [
+ {
+ "group": "auth0-server-python",
+ "pages": [
+ f"{SITE_DIR}/overview",
+ {
+ "group": "Guides",
+ "expanded": False,
+ "pages": [f"{SITE_DIR}/examples/{name}" for name in EXAMPLE_PAGES],
+ },
+ {
+ "group": "API Reference",
+ "expanded": True,
+ "pages": ["{reference}"],
+ },
+ f"{SITE_DIR}/CONTRIBUTING",
+ ],
+ },
+]
+
+
+def setup(app):
+ app.connect("autoapi-skip-member", autoapi_skip_member)
diff --git a/docs/dist/CONTRIBUTING.mdx b/docs/dist/CONTRIBUTING.mdx
new file mode 100644
index 00000000..9eae78d0
--- /dev/null
+++ b/docs/dist/CONTRIBUTING.mdx
@@ -0,0 +1,35 @@
+---
+title: "Contribution"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Please read [Auth0's contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md).
+
+## Environment setup
+
+- Make sure you have node and npm installed
+- Run `poetry install` to install dependencies
+- Follow the local development steps below to get started
+
+## Local development
+
+- `poetry install`: install dependencies
+- `poetry run pytest`: run unit tests
+- `poetry build`: compile the package
+
+## Testing
+
+### Adding tests
+
+Every change should be accompanied by a test.
+
+### Running tests
+
+Run unit tests before opening a PR:
+
+```bash
+poetry run pytest
+```
+
+Also include any information about essential manual tests.
diff --git a/docs/dist/examples/AnonymousSessions.mdx b/docs/dist/examples/AnonymousSessions.mdx
new file mode 100644
index 00000000..b7c6a57d
--- /dev/null
+++ b/docs/dist/examples/AnonymousSessions.mdx
@@ -0,0 +1,257 @@
+---
+title: "Anonymous Sessions"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Anonymous Sessions give a visitor an Auth0 identity **before they log in**. Each visitor gets a persistent `anon@` subject plus an access token, with up to 1 KB of key/value metadata attached at creation. At login, the SDK carries the session to Auth0 as a short-lived transfer ticket so Post-Login / Pre-User-Registration Actions can read the anonymous data via `event.anonymous_session`. Nothing migrates onto the real user profile automatically. The Action author decides what to persist.
+
+## Table of Contents
+
+- [Anonymous Sessions](#anonymous-sessions)
+ - [Table of Contents](#table-of-contents)
+ - [Setup](#setup)
+ - [Creating a Session](#creating-a-session)
+ - [Getting a Token (Renewal Ladder)](#getting-a-token-renewal-ladder)
+ - [Reading the Session](#reading-the-session)
+ - [Logging Out](#logging-out)
+ - [Login Injection](#login-injection)
+ - [Rate-Limiting `get_token()`](#rate-limiting-get_token)
+ - [Error Handling](#error-handling)
+ - [Additional Resources](#additional-resources)
+
+## Setup
+
+Before using the anonymous sessions API, the `anonymous_sessions_enabled` flag must be turned on for your tenant, and the application/client must be enabled for the feature.
+
+Pass an `anonymous_store` to `ServerClient`, alongside your existing `state_store` and `transaction_store`:
+
+```python
+server_client = ServerClient(
+ domain="your-tenant.auth0.com",
+ client_id="...",
+ client_secret="...",
+ secret="...",
+ state_store=my_state_store,
+ transaction_store=my_transaction_store,
+ anonymous_store=my_anonymous_store, # its own store instance, not state_store
+)
+```
+
+Give `anonymous_store` its own store instance, not `state_store` with a different identifier. If you omit it, `create_session()`, `get_token()`, and `logout()` raise `ConfigurationError` before any write. `get_session()` returns `None`.
+
+The anonymous store implements the same `StateStore` ABC as your existing session store. A minimal cookie-backed example:
+
+```python
+from auth0_server_python.store import StateStore
+
+class AnonymousSessionStore(StateStore):
+ def __init__(self, secret: str):
+ super().__init__({"secret": secret})
+
+ async def set(self, identifier, state, remove_if_expires=False, options=None):
+ # Encrypt and write to a cookie or server-side store.
+ # The SDK passes a plain dict; encryption is your responsibility.
+ encrypted = self.encrypt(identifier, state)
+ options["response"].set_cookie("_a0_anon", encrypted, httponly=True, samesite="lax")
+
+ async def get(self, identifier, options=None):
+ value = options["request"].cookies.get("_a0_anon")
+ if not value:
+ return None
+ return self.decrypt(identifier, value)
+
+ async def delete(self, identifier, options=None):
+ options["response"].delete_cookie("_a0_anon")
+```
+
+`self.encrypt` / `self.decrypt` are helpers from the `StateStore` base class that derive a key from your `secret` and the store identifier. The cookie name `_a0_anon` must be distinct from the cookie name used by your `state_store` - a shared name will silently overwrite the authenticated session. See `examples/ConfigureStore.md` for the full store configuration reference.
+
+
+
+**Encryption is your responsibility.** The SDK writes the anonymous session as a plain dict with no encryption applied. If your `anonymous_store` is cookie-backed or otherwise persists data outside a trusted server boundary, you must encrypt the payload before writing and decrypt it on read. This is the same responsibility you already have for `state_store`.
+
+
+
+## Creating a Session
+
+```python
+session = await server_client.anonymous.create_session(
+ audience="https://api.example.com",
+ scope="read:cart write:cart",
+ metadata={"cart_id": "cart_456"},
+ store_options=store_options,
+)
+```
+
+`metadata` is **set once, at creation, and never updated**. Any JSON-serializable value is accepted, ≤1 KB total (UTF-8 JSON byte length). Oversized or non-JSON-serializable metadata is rejected client-side before any network call.
+
+`AnonymousSession` returns `access_token`, `expires_at`, `session_expires_at`, `metadata`, `sub`, and `scope`.
+
+## Getting a Token (Renewal Ladder)
+
+```python
+token = await server_client.anonymous.get_token(store_options=store_options)
+```
+
+Renewal logic, in order:
+
+1. Cached access token still fresh, returned with no network call.
+2. Expired, re-minted using the stored session token (not a refresh-token grant, since anonymous sessions never issue refresh tokens).
+3. Session token expired, raises `AnonymousSessionTokenError` with code `session_expired` and clears the stored session. Token structurally invalid raises with code `invalid_session_token` and also clears. Call `create_session()` to start a new session in either case.
+4. Any other error, raised as a typed exception. No swallow, no auto-retry.
+
+
+
+**On `session_expired` or `invalid_session_token`, the previous anonymous identity is gone.** The SDK clears the stored session before raising, so a subsequent `get_session()` returns `None`. Neither error surfaces the previous identity (its `sub` or `metadata`). Call `create_session()` to start fresh.
+
+Do any identity-dependent work, such as cart or data migration, at **login time**, not on expiry. Read `get_session().sub` before calling `complete_interactive_login()`. That is the normal migration path and is unaffected by the expiry clear. A session expiring before the visitor ever logs in is rare given the session lifetime, and the correct response is simply to create a new one.
+
+
+
+## Reading the Session
+
+```python
+data = await server_client.anonymous.get_session(store_options=store_options)
+```
+
+Returns the stored anonymous identity without calling Auth0, or `None` when there is no session, the stored record is unreadable, or the stored domain does not match the current tenant. Use it to check whether a visitor already has an anonymous identity before deciding to call `create_session()`.
+
+`AnonymousSessionData` carries `sub`, `metadata`, `created_at`, `session_expires_at`, and `domain` - the identity fields only. It never exposes the session token or an access token. To obtain a usable access token, call `get_token()`.
+
+Unlike the other `.anonymous.*` methods, `get_session()` returns `None` rather than raising when no `anonymous_store` is configured.
+
+## Logging Out
+
+```python
+await server_client.anonymous.logout(store_options=store_options)
+```
+
+
+
+**`logout()` does not revoke.** There is no server-side anonymous session store to revoke against, this clears only the locally-held session context. Any access token already issued for this anonymous session remains valid until its natural expiry.
+
+
+
+Authenticated (OIDC) logout also ends an active anonymous session. When you call `ServerClient.logout()` and an anonymous store is configured, the SDK clears the locally-held anonymous session before returning the logout URL. This is a local clear only, with no remote call (consistent with `anonymous.logout()`, which also does not revoke server-side). It prevents the next visitor on a shared device from having the previous visitor's anonymous identity re-linked at their login. If no anonymous session is active, nothing is cleared.
+
+## Login Injection
+
+When an anonymous session is active, `start_interactive_login()` automatically injects an `anon_transfer_token` transfer ticket into the `/authorize` URL, no code change needed at your call site. If no anonymous session exists, behavior is unchanged.
+
+The raw `session_token` never goes on the URL. At the moment the `/authorize` URL is built, the SDK exchanges the stored session token for a short-lived (30s) single-use ticket (`anon_transfer_token`) via `POST /anonymous/token`, and forwards only that ticket as the `anon_transfer_token` query parameter. The raw session token stays inside the SDK's store and the ticket is never persisted. The ticket is short-lived and grants no authorization on its own, but you should still set `Referrer-Policy: no-referrer` on your login pages and never log the authorize URL.
+
+If the exchange errors (network failure, a non-200, or an unparseable response), login proceeds with no ticket and no linking, and never aborts the login.
+
+After `complete_interactive_login` succeeds, the SDK clears the anonymous session by default. To keep it active across the login boundary:
+
+```python
+server_client = ServerClient(
+ ...
+ clear_anonymous_session_on_login=False,
+)
+```
+
+## Rate-Limiting `get_token()`
+
+`get_token()` makes at most one upstream Auth0 call per invocation. It does not protect against an attacker calling your route repeatedly. `POST /anonymous/token` is an unauthenticated, token-issuing endpoint. **You must rate-limit any route in your application that calls `get_token()` on an anonymous session**, the same way you would rate-limit any other unauthenticated token-issuing path. The SDK has no request-level context to do this itself.
+
+## Error Handling
+
+All anonymous session errors subclass `AnonymousSessionError`, carrying a `.code` you can branch on:
+
+```python
+from auth0_server_python.error import (
+ AnonymousSessionCreateError,
+ AnonymousSessionTokenError,
+)
+
+try:
+ session = await server_client.anonymous.create_session(audience="...", scope="...")
+except AnonymousSessionCreateError as e:
+ if e.code == "feature_not_enabled":
+ ...
+```
+
+### Error Hierarchy
+
+```
+AnonymousSessionError base class, never raised directly
+ AnonymousSessionCreateError raised by create_session()
+ AnonymousSessionTokenError raised by get_token()
+```
+
+### `AnonymousSessionCreateError` codes
+
+| `.code` | When |
+|---------|------|
+| `"feature_not_enabled"` | anonymous sessions not enabled on this tenant/client (server-returned code, passed through unchanged) |
+| `"anonymous_create_error"` | generic platform error on the create path |
+| `"missing_session_token"` | platform response omitted the session token (misconfigured tenant) |
+| `"invalid_metadata"` | metadata is not a dict or contains non-JSON-serializable values |
+| `"metadata_too_large"` | metadata exceeds the 1 KB limit |
+| `"invalid_options"` | unrecognised key in `create_session()` options |
+
+The platform may return other codes (e.g. `"insufficient_scope"`) and these are passed through on `.code` unchanged.
+
+### `AnonymousSessionTokenError` codes
+
+| `.code` | When |
+|---------|------|
+| `"session_expired"` | session token has expired - call `create_session()` to start a new session |
+| `"invalid_session_token"` | session token is structurally invalid (e.g. rotated or revoked) - call `create_session()` to recover |
+| `"invalid_session_state"` | stored session data is corrupt or unreadable - call `create_session()` to recover |
+| `"anonymous_token_error"` | no active session, network error, parse error, or generic platform error on the renewal path |
+
+> **Note on naming.** The SDK spec names this class `AnonymousSessionTokenExpiredError`. This SDK uses `AnonymousSessionTokenError` - a deliberate broadening, since the class covers all `get_token()` failures, not just expiry. The `.code` values are stable and safe to branch on.
+
+### Handling expired and invalid session tokens
+
+Both `session_expired` and `invalid_session_token` mean the stored session is permanently unusable. The SDK clears it before raising, so `get_session()` returns `None` immediately after. The correct response is to start a fresh session:
+
+```python
+from auth0_server_python.error import AnonymousSessionTokenError
+
+try:
+ token = await server_client.anonymous.get_token(store_options=store_options)
+except AnonymousSessionTokenError as e:
+ if e.code == "session_expired":
+ # The session lifetime elapsed. Start a fresh one.
+ session = await server_client.anonymous.create_session(
+ audience="https://api.example.com",
+ scope="read:cart write:cart",
+ store_options=store_options,
+ )
+ token = await server_client.anonymous.get_token(store_options=store_options)
+ elif e.code == "invalid_session_token":
+ # The token is structurally invalid (e.g. rotated or revoked).
+ session = await server_client.anonymous.create_session(
+ audience="https://api.example.com",
+ scope="read:cart write:cart",
+ store_options=store_options,
+ )
+ token = await server_client.anonymous.get_token(store_options=store_options)
+ else:
+ raise
+```
+
+If your application treats both cases identically, you can handle them together:
+
+```python
+from auth0_server_python.error import AnonymousSessionTokenError
+
+try:
+ token = await server_client.anonymous.get_token(store_options=store_options)
+except AnonymousSessionTokenError as e:
+ if e.code in ("session_expired", "invalid_session_token"):
+ session = await server_client.anonymous.create_session(
+ audience="https://api.example.com",
+ scope="read:cart write:cart",
+ store_options=store_options,
+ )
+ token = await server_client.anonymous.get_token(store_options=store_options)
+ else:
+ raise
+```
+
+The two codes are kept distinct so callers that need to differentiate (for example, to log a metric or alert on unexpected token invalidation) can do so without parsing the message string.
diff --git a/docs/dist/examples/ClientInitiatedBackChannelLogin.mdx b/docs/dist/examples/ClientInitiatedBackChannelLogin.mdx
new file mode 100644
index 00000000..490f4b5e
--- /dev/null
+++ b/docs/dist/examples/ClientInitiatedBackChannelLogin.mdx
@@ -0,0 +1,146 @@
+---
+title: "Backchannel Authentication Examples"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Client-Initiated Backchannel Authentication (CIBA) enables applications to authenticate users via a separate channel or device, without requiring browser-based redirects. This is especially useful for IoT devices, call center authentication, and scenarios where the authentication device is different from the requesting device.
+
+## Prerequisites
+
+Before using backchannel authentication:
+
+1. Enable CIBA in your Auth0 dashboard (Authentication → Flows → Client-Initiated Backchannel Authentication)
+2. Ensure you have the proper scopes configured for your application
+3. Set up your Auth0 SDK with the required credentials
+
+## Basic Usage
+
+### Initiating Backchannel Authentication
+
+```python
+from auth0_server_python.auth_server import ServerClient
+
+# Initialize the Auth0 client
+auth0 = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+)
+
+async def authenticate_via_backchannel():
+ # Configure backchannel options
+ options = {
+ "login_hint": {"sub": "user-id"}, # The user identifier
+ "binding_message": "Login to Example App", # Message displayed to the user
+ "authorization_params": {
+ "requested_expiry": 300, # Optional: Token expiry time in seconds
+ }
+ }
+
+ # Initiate backchannel authentication
+ try:
+ result = await auth0.login_backchannel(options)
+
+ # Authentication successful
+ print(f"User authenticated successfully")
+ print(f"Token type: {result.get('token_type')}")
+ print(f"Expires in: {result.get('expires_in')} seconds")
+
+ # You can now use the session for API calls
+ return result
+ except Exception as e:
+ print(f"Authentication failed: {str(e)}")
+ return None
+```
+
+- `binding_message`: An optional, human-readable message to be displayed at the consumption device and authentication device. This allows the user to ensure the transaction initiated by the consumption device is the same that triggers the action on the authentication device.
+- `login_hint['sub']`: The `sub` claim of the user that is trying to login using Client-Initiated Backchannel Authentication, and to which a push notification to authorize the login will be sent.
+- `requested_expiry`: The requested lifetime, in seconds, of the authentication request. The default value on Auth0 is 300 seconds.
+
+
+
+- Using Client-Initiated Backchannel Authentication requires the feature to be enabled in the Auth0 dashboard.
+- Read [the Auth0 docs](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow) to learn more about Client-Initiated Backchannel Authentication.
+
+
+
+### Using Rich Authorization Requests
+When using Client-Initiated Backchannel Authentication, you can also use Rich Authorization Requests (RAR) by setting `authorizationParams.authorization_details`:
+
+
+```python
+
+result = await auth0.login_backchannel({
+ "binding_message": "",
+ "login_hint": {
+ "sub": "auth0|123456789"
+ },
+ "authorization_params": {
+ "authorization_details": json.dumps([{
+ "type": "",
+ # additional fields here
+ }])
+ }
+})
+```
+
+
+
+- Audience must be explicitly specified when `authorization_details` parameter is passed
+- Using Client-Initiated Backchannel Authentication with Rich Authorization Requests (RAR) requires the feature to be enabled in the Auth0 dashboard.
+- Read [the Auth0 docs](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow) to learn more about Client-Initiated Backchannel Authentication.
+
+
+
+### Passing Store Options
+
+Just like most methods, `login_backchannel()` accept a second argument that is used to pass to the configured Transaction and State Store:
+
+```python
+store_options = { /* ... */ }
+await auth0.login_backchannel({}, store_options)
+```
+
+Read more above in [Configuring the Store](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+### Example (FastAPI)
+
+```python
+
+from fastapi import FastAPI, Request
+from fastapi.responses import JSONResponse
+from auth0_server_python.auth_server import ServerClient
+
+app = FastAPI()
+
+# Initialize Auth0 client
+auth0 = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+)
+
+@app.post("/api/auth/backchannel")
+async def backchannel_login(request: Request):
+ try:
+ result = await auth0.login_backchannel({
+ "binding_message": "Login to Example App",
+ "login_hint": {
+ "sub": "auth0|123456789"
+ }
+ })
+
+ # Successfully authenticated
+ return JSONResponse(content={
+ "status": "success",
+ "message": "User authenticated via backchannel"
+ })
+ except Exception as e:
+ return JSONResponse(content={
+ "status": "error",
+ "message": str(e)
+ })
+```
diff --git a/docs/dist/examples/ConfigureStore.mdx b/docs/dist/examples/ConfigureStore.mdx
new file mode 100644
index 00000000..d0c05e36
--- /dev/null
+++ b/docs/dist/examples/ConfigureStore.mdx
@@ -0,0 +1,438 @@
+---
+title: "Configuring the Store"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+The `auth0-server-python` library provides abstract base classes for storing session data (via a `State Store`) and for storing short-lived transaction data (via a `Transaction Store`).
+
+However, it doesn’t force a particular storage mechanism – you can pick cookies, a server-side database/cache, or anything else that suits your application.
+
+Your `auth0-fastapi` layer (or your custom integration) is responsible for instantiating these stores and passing them to the `ServerClient` (in **auth0-server-python**) or to the `AuthClient` wrapper (in **auth0-fastapi**). This guide shows how to do exactly that.
+
+## 1.Store Configuration Basics
+
+### Abstract Base Classes
+
+In `auth0-server-python`, you have:
+
+1. **TransactionStore** – holds short-lived data for ongoing Auth0 flows (PKCE code_verifier, state param, etc.).
+
+2. **StateStore** – holds user “session” data over longer periods (ID token, refresh token, user profile, etc.).
+
+Each store implements `set`, `get`, and `delete`. The `StateStore` adds an optional `delete_by_logout_token` for backchannel logout scenarios.
+
+### Passing Store Options
+
+When you **start** or **complete** an Auth0 flow, you can pass a `store_options` dictionary with extra data (like the FastAPI `Request` and `Response`) that the store can use for reading/writing cookies. For example:
+
+```python
+store_options = {"request": request, "response": response}
+await auth_client.start_login(app_state={"return_to": "/profile"}, store_options=store_options)
+```
+## 2.Stateless Store (All Data in Cookies)
+### When to Use It
+A **stateless** store puts **all** the session data directly in the cookie – typically encrypted. It’s easy to set up but limited by cookie size constraints and overhead on each request. For small to medium session data, it can be sufficient.
+
+### Example: `StatelessStateStore` and `CookieTransactionStore` in FastAPI
+If you’re using `auth0-fastapi`, you already have:
+
+1. **StatelessStateStore** – stores the session data (ID tokens, refresh tokens, etc.) in an encrypted cookie.
+2. **CookieTransactionStore** – stores short-lived transaction data (PKCE code_verifier) in another encrypted cookie.
+
+```python
+from auth0_server_python.store import StateStore, TransactionStore
+
+class StatelessStateStore(StateStore):
+ def __init__(self, secret: str, cookie_name: str = "_a0_session"):
+ super().__init__({"secret": secret})
+ self.cookie_name = cookie_name
+
+ async def set(self, identifier, state, remove_if_expires=False, options=None):
+ # 1. Use self.encrypt to encrypt `state`.
+ # 2. Store in a cookie named self.cookie_name
+ # typically using options["response"].set_cookie(...)
+ pass
+
+ async def get(self, identifier, options=None):
+ # 1. Retrieve cookie from options["request"].cookies
+ # 2. Decrypt with self.decrypt(...)
+ pass
+
+ async def delete(self, identifier, options=None):
+ # Clear or expire the cookie
+ pass
+
+ # delete_by_logout_token can be no-op or raise NotImplementedError if purely stateless
+
+```
+**Transaction store** is similar, just specialized for short-lived data.
+
+### Usage
+```python
+from auth0_fastapi.auth.auth_client import AuthClient
+from auth0_fastapi.stores.stateless_state_store import StatelessStateStore
+from auth0_fastapi.stores.cookie_transaction_store import CookieTransactionStore
+
+stateless_store = StatelessStateStore(secret="YOUR_SECRET")
+cookie_tx_store = CookieTransactionStore(secret="YOUR_SECRET")
+
+auth_client = AuthClient(
+ config=...,
+ state_store=stateless_store,
+ transaction_store=cookie_tx_store
+)
+```
+When users log in:
+```python
+@app.get("/auth/login")
+async def login(request: Request, response: Response):
+ store_options = {"request": request, "response": response}
+ redirect_url = await request.app.state.auth_client.start_login(
+ store_options=store_options
+ )
+ return RedirectResponse(url=redirect_url)
+```
+All session data is now **fully** in cookies, so there’s no need for a server-side session DB.
+
+## 3.Stateful Store (Server-Side Sessions)
+### When to Use It
+A **stateful** approach stores only a **session ID** in the cookie, while the actual user/session data (ID tokens, refresh tokens) lives in a server-based store (e.g., Redis, Postgres). This avoids cookie size limits and can handle large amounts of data.
+
+### 3.1. Redis-Based Example
+Let’s walk through a `RedisStateStore` that inherits from `StateStore`:
+```python
+import json
+import aioredis
+from typing import Any, Dict, Optional
+from auth0_server_python.store import StateStore
+
+class RedisStateStore(StateStore):
+ """
+ A stateful store that uses Redis to store session data.
+ The cookie holds just a session_id; the rest of the data is in Redis.
+ """
+
+ def __init__(self, secret: str, redis_url: str = "redis://localhost:6379", cookie_name: str = "_a0_session"):
+ super().__init__({"secret": secret})
+ self.cookie_name = cookie_name
+ self.redis_client = aioredis.from_url(redis_url)
+
+ async def set(
+ self,
+ identifier: str,
+ state: Dict[str, Any],
+ options: Optional[Dict[str, Any]] = None
+ ) -> None:
+ if not options or "response" not in options:
+ raise ValueError("Response object is required for setting a cookie in RedisStateStore.")
+
+ response = options["response"]
+
+ # Generate or reuse a session_id. If remove_if_expires is True, you might want to re-generate.
+ # For simplicity, let's re-generate a new session_id each time. You could also store it once.
+ session_id = identifier # or use secrets.token_urlsafe(), etc.
+
+ # Store the actual user data in Redis under `session_id`.
+ # If you want encryption at rest, you can do:
+ # encrypted_data = self.encrypt(session_id, state)
+ # await self.redis_client.set(session_id, encrypted_data)
+ # For demo, let's store it as JSON without encryption:
+ await self.redis_client.set(session_id, json.dumps(state))
+
+ # Now set a cookie in the response with just the session_id
+ # (You can choose an expiration or not).
+ response.set_cookie(
+ key=self.cookie_name,
+ value=session_id,
+ httponly=True,
+ samesite="Lax",
+ max_age=3600, # 1 hour example
+ path="/"
+ )
+
+ async def get(
+ self,
+ identifier: str,
+ options: Optional[Dict[str, Any]] = None
+ ) -> Optional[Dict[str, Any]]:
+ if not options or "request" not in options:
+ raise ValueError("Request object is required to read cookies in RedisStateStore.")
+
+ request = options["request"]
+ session_id = request.cookies.get(self.cookie_name)
+ if not session_id:
+ return None
+
+ # Retrieve from Redis
+ raw_data = await self.redis_client.get(session_id)
+ if not raw_data:
+ return None
+
+ # If you used self.encrypt(...) on set, call self.decrypt(...) here.
+ return json.loads(raw_data)
+
+ async def delete(
+ self,
+ identifier: str,
+ options: Optional[Dict[str, Any]] = None
+ ) -> None:
+ if not options or "response" not in options:
+ raise ValueError("Response object is required to clear cookies in RedisStateStore.")
+
+ response = options["response"]
+ request = options["request"]
+ session_id = request.cookies.get(self.cookie_name)
+
+ # Remove from Redis
+ if session_id:
+ await self.redis_client.delete(session_id)
+
+ # Clear the cookie
+ response.delete_cookie(self.cookie_name)
+
+ async def delete_by_logout_token(
+ self,
+ claims: Dict[str, Any],
+ options: Optional[Dict[str, Any]] = None
+ ) -> None:
+ """
+ For backchannel logout, look up and remove sessions that match `sid` or `sub`.
+ This might require scanning your Redis store or having an index that maps sid -> session_id.
+ """
+ # Example: if you store 'sid' inside the session data, you could do:
+ # 1) iterate over all keys or maintain a mapping. We'll do a naive approach:
+ # 2) parse each session for matching sub/sid.
+ # This is a simple approach; for large scale, add indexing or a specialized lookup.
+
+ keys = await self.redis_client.keys("*")
+ for k in keys:
+ raw_data = await self.redis_client.get(k)
+ if raw_data:
+ session_data = json.loads(raw_data)
+ # If your session_data stores an internal dict with `sid` or `sub`
+ internal = session_data.get("internal", {})
+ if internal.get("sid") == claims.get("sid") and session_data.get("user", {}).get("sub") == claims.get("sub"):
+ await self.redis_client.delete(k)
+
+```
+#### Usage (FastAPI)
+```python
+from auth0_fastapi.auth.auth_client import AuthClient
+from your_module.redis_state_store import RedisStateStore
+from auth0_fastapi.stores.cookie_transaction_store import CookieTransactionStore
+
+redis_state_store = RedisStateStore(
+ secret="YOUR_APP_SECRET",
+ redis_url="redis://localhost:6379",
+ cookie_name="_a0_session"
+)
+
+transaction_store = CookieTransactionStore(secret="YOUR_APP_SECRET")
+
+auth_client = AuthClient(
+ config=config, # your Auth0Config
+ state_store=redis_state_store,
+ transaction_store=transaction_store
+)
+```
+Now your user’s session data is in **Redis**, and only a minimal session ID is in the cookie.
+
+### 3.2. Postgres-Based Example
+If you prefer a **SQL database** for session data, here’s a `PostgresStateStore` example using [asyncpg](https://github.com/MagicStack/asyncpg).
+
+```python
+import json
+import asyncpg
+from typing import Any, Dict, Optional
+from auth0_server_python.store import StateStore
+
+class PostgresStateStore(StateStore):
+ """
+ A stateful store that uses Postgres to store session data.
+ The cookie holds just a session_id; the rest of the data is in Postgres.
+ """
+
+ def __init__(self, secret: str, dsn: str, cookie_name: str = "_a0_session"):
+ super().__init__({"secret": secret})
+ self.dsn = dsn # "postgresql://user:password@host:port/dbname"
+ self.cookie_name = cookie_name
+ self.pool = None # We'll initialize a connection pool later.
+
+ async def init_pool(self):
+ """Create a connection pool. Call this once on startup."""
+ if not self.pool:
+ self.pool = await asyncpg.create_pool(dsn=self.dsn)
+
+ async def set(
+ self,
+ identifier: str,
+ state: Dict[str, Any],
+ options: Optional[Dict[str, Any]] = None
+ ) -> None:
+ if not options or "response" not in options:
+ raise ValueError("Response object is required for setting a cookie in PostgresStateStore.")
+
+ response = options["response"]
+ session_id = identifier # Or generate a random ID (e.g. secrets.token_urlsafe())
+
+ async with self.pool.acquire() as conn:
+ # Optionally encrypt `state`:
+ # encrypted_data = self.encrypt(session_id, state)
+ # For simplicity, store as JSON
+ data_json = json.dumps(state)
+
+ # Insert or update the session
+ await conn.execute(
+ """
+ INSERT INTO auth_sessions(session_id, session_data)
+ VALUES($1, $2)
+ ON CONFLICT (session_id) DO UPDATE SET session_data=excluded.session_data
+ """,
+ session_id,
+ data_json
+ )
+
+ # Set the cookie containing session_id
+ response.set_cookie(
+ key=self.cookie_name,
+ value=session_id,
+ httponly=True,
+ samesite="Lax",
+ max_age=86400, # 1 day example
+ path="/"
+ )
+
+ async def get(
+ self,
+ identifier: str,
+ options: Optional[Dict[str, Any]] = None
+ ) -> Optional[Dict[str, Any]]:
+ if not options or "request" not in options:
+ raise ValueError("Request object is required to read cookies in PostgresStateStore.")
+
+ request = options["request"]
+ session_id = request.cookies.get(self.cookie_name)
+ if not session_id:
+ return None
+
+ async with self.pool.acquire() as conn:
+ row = await conn.fetchrow(
+ "SELECT session_data FROM auth_sessions WHERE session_id = $1",
+ session_id
+ )
+ if not row:
+ return None
+ # If you used encryption, do self.decrypt(...)
+ return json.loads(row["session_data"])
+
+ async def delete(
+ self,
+ identifier: str,
+ options: Optional[Dict[str, Any]] = None
+ ) -> None:
+ if not options or "response" not in options:
+ raise ValueError("Response object is required to clear cookies in PostgresStateStore.")
+
+ response = options["response"]
+ request = options["request"]
+ session_id = request.cookies.get(self.cookie_name)
+
+ if session_id:
+ async with self.pool.acquire() as conn:
+ await conn.execute("DELETE FROM auth_sessions WHERE session_id = $1", session_id)
+
+ response.delete_cookie(self.cookie_name)
+
+ async def delete_by_logout_token(
+ self,
+ claims: Dict[str, Any],
+ options: Optional[Dict[str, Any]] = None
+ ) -> None:
+ """
+ Backchannel logout: find sessions that match sub/sid and remove them.
+ This might require storing sub/sid in the DB row for quick lookups.
+ """
+ async with self.pool.acquire() as conn:
+ # If you store 'sid' or 'user.sub' in a separate column, you can do:
+ await conn.execute(
+ """
+ DELETE FROM auth_sessions
+ WHERE (session_data->'internal'->>'sid') = $1
+ AND (session_data->'user'->>'sub') = $2
+ """,
+ claims.get("sid"),
+ claims.get("sub")
+ )
+```
+#### Usage (FastAPI)
+```python
+from auth0_fastapi.auth.auth_client import AuthClient
+from your_module.postgres_state_store import PostgresStateStore
+from auth0_fastapi.stores.cookie_transaction_store import CookieTransactionStore
+
+pg_store = PostgresStateStore(
+ secret="YOUR_SECRET",
+ dsn="postgresql://user:pass@localhost:5432/mydb",
+ cookie_name="_a0_session"
+)
+
+await pg_store.init_pool() # do this in an @app.on_event("startup") if using FastAPI
+
+transaction_store = CookieTransactionStore(secret="YOUR_APP_SECRET")
+
+auth_client = AuthClient(
+ config=config,
+ state_store=pg_store,
+ transaction_store=transaction_store
+)
+
+```
+## 4. Passing Store Options (FastAPI Endpoint)
+
+Often you need `request` and `response` objects to set or clear cookies. In your route:
+
+```python
+@app.get("/auth/login")
+async def login(request: Request, response: Response):
+ store_options = {"request": request, "response": response}
+ auth_url = await auth_client.start_login(
+ store_options=store_options
+ )
+ return RedirectResponse(auth_url)
+```
+Likewise for logout or completing the login callback:
+```python
+@app.get("/auth/callback")
+async def callback(request: Request, response: Response):
+ store_options = {"request": request, "response": response}
+ session_data = await auth_client.complete_login(
+ str(request.url),
+ store_options=store_options
+ )
+ ...
+```
+The store sees `store_options["request"]` and `store_options["response"]`—it can handle cookies or read HTTP headers as needed.
+
+## 5. Handling `delete_by_logout_token` (Backchannel Logout)
+
+If you want to support **OIDC Backchannel Logout**:
+
+- For **stateless** stores, you might just remove the cookie or do nothing.
+- For **stateful** stores, you often need to find all sessions referencing a sub or sid from the logout token and remove them. That’s what the `delete_by_logout_token` method in `StateStore` is for.
+
+```python
+@app.post("/auth/backchannel-logout")
+async def backchannel_logout(request: Request):
+ body = await request.json()
+ logout_token = body.get("logout_token")
+ await auth_client.handle_backchannel_logout(logout_token)
+ return Response(status_code=204)
+
+```
+The store’s `delete_by_logout_token` method is where you’d do a DB lookup by `sid` or `sub` to invalidate sessions.
+
+## 6. Best Practices
+1. **Choose Stateless** for simplicity (everything in cookies) or **Stateful** for more flexibility (cookies store an ID; actual data in a DB).
+2. **Encrypt** your data if you store tokens or personal info in cookies (stateless). If you store them in a DB, ensure your DB environment is secure.
+3. **Large Data** or **multiple tokens** often suggests **stateful** storage—avoid hitting cookie size limits or overhead.
diff --git a/docs/dist/examples/ConnectedAccounts.mdx b/docs/dist/examples/ConnectedAccounts.mdx
new file mode 100644
index 00000000..4ff42b1c
--- /dev/null
+++ b/docs/dist/examples/ConnectedAccounts.mdx
@@ -0,0 +1,251 @@
+---
+title: "Connect Accounts for using Token Vault"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+The Connect Accounts feature uses the Auth0 My Account API to allow users to link multiple third party accounts to a single Auth0 user profile. In order to use this feature, [My Account API](https://auth0.com/docs/manage-users/my-account-api) must be activated on your Auth0 tenant.
+
+
+
+DPoP sender token constraining is not yet supported in this SDK. My Account API can be configured to support it (default behaviour) but must not be configured to require it.
+
+
+
+
+When using [Connected Accounts for Token Vault](https://auth0.com/docs/secure/tokens/token-vault/connected-accounts-for-token-vault), Auth0 acquires tokens from upstream Identity Providers (like Google) and stores them in a secure [Token Vault](https://auth0.com/docs/secure/tokens/token-vault). These tokens can then be used to access third-party APIs (like Google Calendar) on behalf of the user.
+
+The tokens in the Token Vault are then accessible to [Applications](https://auth0.com/docs/get-started/applications) configured in Auth0. The application can issue requests to Auth0 to retrieve the tokens from the Token Vault and use them to access the third-party APIs.
+
+This is particularly useful for applications that require access to different resources on behalf of a user, like AI Agents.
+
+## Pre-requisites
+Connected Account functionality makes use of the Auth0 [My Account API] (https://auth0.com/docs/manage-users/my-account-api) and [Multiple Resource Refresh Tokens (MRRT)](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token). To use this functionality in this SDK you must first ensure that you first [activate My Account API](https://auth0.com/docs/manage-users/my-account-api#activate-the-my-account-api) on your Auth0 tenant and enable access for you client Application. You must also [configure MRRT](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token/configure-and-implement-multi-resource-refresh-token#configure-applications-for-mrrt) on your client application's refresh token policies to allow access to the My Account audience (`https://{yourDomain}/me/`) for the `create:me:connected_accounts` scope as well as any other APIs and scopes you intend to access in your application.
+
+## Configure the SDK
+
+```python
+server_client = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+
+)
+```
+
+`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+## Login to the application
+
+Use the login methods to authenticate to the application and get a refresh token in order to use MRRT to call the My Account API. If you are also intending to authorize access to any custom API, you may optionally specify an audience and any relevant scopes but you must at a minimum include the `offline_access` scope to ensure you obtain a refresh token.
+
+```python
+# Login to authenticate your aplication, optionally specifying any scopes for your Auth0 API
+authorization_url = await server_client.start_interactive_login(
+ {
+ "redirect_uri": "CALLBACK_URL",
+ "authorization_params": {
+ # optional
+ "audience": "https://custom-api.com",
+ # must include at least offline_access to obtain a refresh token
+ "scope": "offline_access openid profile email read:foo"
+ }
+ },
+ store_options={"request": request, "response": response}
+)
+```
+Redirect user to the provided url, after authenticating with Auth0, the user will be returned to the provided `CALLBACK_URL` with additional `state` and `code` query parameters. In the handler for this callback, complete the interactive login passing the full callback url (including any query parameters).
+
+```python
+# handle redirect
+result = await server_client.complete_interactive_login(
+ full_callback_url,
+ store_options={"request": request, "response": response}
+)
+```
+
+## Connect to a third-party account
+
+Start the flow using the `start_connect_account` method to redirect the user to the third-party Identity Provider to connect their account.
+
+The `authorization_params` is used to pass additional parameters required by the third-party IdP.
+
+The `scopes` parameter is used to override the scopes requested from the third-party IdP. By default, if no `scopes` parameter is provided, Auth0 will request any scopes configured in the connection settings through the Auth0 Management API/Dashboard.
+
+The `app_state` parameter allows you to pass custom state (for example, a return URL) that is later available when the connect process completes.
+
+```python
+
+connect_url = await self.client.start_connect_account(
+ ConnectAccountOptions(
+ connection="google-oauth2",
+ redirect_uri="CALLBACK_URL"
+ app_state= {
+ "returnUrl":"SOME_URL"
+ }
+ scopes= [
+ # scopes to passed to the third-party IdP
+ "openid",
+ "https://www.googleapis.com/auth/calendar.freebusy",
+ "https://www.googleapis.com/auth/calendar.readonly"
+ ]
+ authorization_params= {
+ # additional auth parameters to be sent to the third-party IdP
+ "login_hint": "jdoe@example.org"
+ }
+ ),
+ store_options={"request": request, "response": response}
+)
+```
+
+Using the url returned, redirect the user to the third-party Identity Provider to complete any required authorization. Once authorized, the user will be redirected back to the provided `CALLBACK_URL` with a `connect_code` and `state` parameter. The `CALLBACK_URL` provided may be the same that is used for the interative login or it may be distinct. However if the same endpoint is used, the handler should be able distinguish between a login callback vs a connect accounts callback (typically via the presence of the `code` and `connect_code` parameter respectively).
+
+## Complete the account connection
+
+In the callback handler, call the `complete_connect_account` method using the full callback url returned from the third-party IdP to complete the connected account flow. This method extracts the connect_code from the URL, completes the connection, and returns the response data (including any `app_state` you passed originally).
+
+```python
+complete_response = await self.client.complete_connect_account(
+ url= connect_account_full_callback_url,
+ store_options={"request": request, "response": response}
+)
+```
+
+
+
+The `callback_url` must include the necessary parameters (`state` and `connect_code`) that Auth0 sends upon successful authorization.
+
+
+
+You can now call the API with your access token and the API can use [Access Token Exchange with Token Vault](https://auth0.com/docs/secure/tokens/token-vault/access-token-exchange-with-token-vault) to get tokens from the Token Vault to access third-party APIs on behalf of the user.
+
+```python
+access_token_for_google = await server_client.get_access_token_for_connection(
+ { "connection": "google-oauth2" },
+ store_options={"request": request, "response": response}
+)
+```
+
+## Managing Connected Accounts
+
+`ServerClient` exposes three methods for managing a user's connected accounts
+
+### List Available Connections
+
+This method provides a list of connections that have been enabled for use with Connected Accounts for Token Vault that the user may use to connect accounts.
+
+This method requires the My Account `read:me:connected_accounts` scope to be enabled for your application and configured for MRRT.
+
+This method supports paging via optional the use of `take` parameter. Without this parameters, a default page size of 10 is used. Subsequent pages can be retrieved by also passing the `from_param` parameter with the token returned in the `next` property of the response
+
+```python
+available_connections = await client.list_connected_account_connections(
+ take= 5, # optional
+ from_param= "NEXT_VALUE_FROM_PREVIOUS_RESPONSE", # optional
+ store_options= {"request": request, "response": response}
+)
+```
+
+### List Connected Accounts
+
+This method provides a list of accounts that you have already connected.
+
+This method requires the My Account `read:me:connected_accounts` scope to be enabled for your application and configured for MRRT.
+
+An optional `connection` parameter can be used to filter the connected accounts for a specific connection, otherwise all connected accounts will be returns
+
+This method supports paging via optional the use of `take` parameter. Without this parameters, a default page size of 10 is used. Subsequent pages can be retrieved by also passing the `from_param` parameter with the token returned in the `next` property of the response
+
+```python
+connected_accounts = await client.list_connected_accounts(
+ connection= "google-oauth2", # optional
+ take= 5, # optional
+ from_param= "NEXT_VALUE_FROM_PREVIOUS_RESPONSE", # optional
+ store_options= {"request": request, "response": response}
+)
+```
+
+### Delete Connected Account
+
+This method removes a connected account for the user.
+
+This method requires the My Account `delete:me:connected_accounts` scope to be enabled for your application and configured for MRRT.
+
+This method takes a `connected_account_id` parameter which can be obtained from `list_connected_accounts`.
+
+```python
+connected_accounts = await client.delete_connected_account(
+ connected_account_id= "CONNECTED_ACCOUNT_ID",
+ store_options= {"request": request, "response": response}
+)
+```
+
+## Error Handling
+
+All SDK errors inherit from `Auth0Error`. For most cases, catch `Auth0Error` to handle all errors uniformly. Only catch specific error types when you need to take different actions based on the error.
+
+### Basic Error Handling (Recommended)
+
+```python
+from auth0_server_python.error import Auth0Error
+
+try:
+ connect_url = await client.start_connect_account(
+ ConnectAccountOptions(connection="google-oauth2", redirect_uri="https://example.com/callback"),
+ store_options={"request": request, "response": response}
+ )
+except Auth0Error as e:
+ print(f"Error: {str(e)}")
+ return {"error": "Failed to connect account"}
+```
+
+### Advanced Error Handling (When Specific Actions Required)
+
+Catch specific types only when you need to handle different error conditions differently:
+
+```python
+from auth0_server_python.error import Auth0Error, MyAccountApiError
+
+try:
+ connect_url = await client.start_connect_account(
+ ConnectAccountOptions(connection="google-oauth2", redirect_uri="https://example.com/callback"),
+ store_options={"request": request, "response": response}
+ )
+except MyAccountApiError as e:
+ if e.status == 401:
+ return redirect_to_login() # Token expired
+ elif e.status == 403:
+ return {"error": "Missing required permission"} # Missing scope
+ elif e.status == 400 and e.validation_errors:
+ return {"error": "Validation failed", "details": e.validation_errors}
+ raise # Re-raise other cases
+except Auth0Error as e:
+ return {"error": str(e)}
+```
+
+### Common Error Types
+
+- **`Auth0Error`** (base): Catch this for general error handling
+- **`MyAccountApiError`**: My Account API errors with `status`, `detail`, and optional `validation_errors`
+- **`InvalidArgumentError`**: Invalid parameter value
+- **`MissingRequiredArgumentError`**: Required parameter not provided
+
+## A note about scopes
+
+If multiple pieces of Connected Account functionality are intended to be used, it is recommended that you set the default `scope` for the My Account audience when creating you `ServerClient`. This will avoid multiple token requests as without it a new token will be requested for each scope used. This can be done by configuring the `scope` dictionary in the `authorization_params` when configuring the SDK. Each value in the dictionary corresponds to an `audience` and sets the `default` requested scopes for that audience.
+
+```python
+server_client = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ authorization_params={
+ "scope" {
+ "https://YOUR_AUTH0_DOMAIN/me/": "create:me:connected_accounts read:me:connected_accounts delete:me:connected_accounts", # scopes required for the My Account audience
+ # default scopes for custom API audiences can also be defined
+ }
+ }
+)
+```
diff --git a/docs/dist/examples/CustomTokenExchange.mdx b/docs/dist/examples/CustomTokenExchange.mdx
new file mode 100644
index 00000000..13146e42
--- /dev/null
+++ b/docs/dist/examples/CustomTokenExchange.mdx
@@ -0,0 +1,284 @@
+---
+title: "Custom Token Exchange"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Custom Token Exchange allows you to exchange tokens from external identity providers or legacy authentication systems for Auth0 tokens without browser redirects. This implements **OAuth 2.0 Token Exchange** ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)).
+
+> **NOTE**: For complete documentation on Custom Token Exchange, configuration requirements, and detailed use cases, see the [official Auth0 documentation](https://auth0.com/docs/authenticate/custom-token-exchange).
+
+## 1. Basic Token Exchange
+
+Exchange a custom token for Auth0 tokens without creating a user session.
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+from auth0_server_python.auth_types import CustomTokenExchangeOptions
+
+# Initialize the client
+auth0 = ServerClient(
+ domain="",
+ client_id="",
+ client_secret="",
+ secret=""
+)
+
+# Exchange a custom token
+response = await auth0.custom_token_exchange(
+ CustomTokenExchangeOptions(
+ subject_token="custom-token-from-external-system",
+ subject_token_type="urn:acme:mcp-token",
+ audience="https://api.example.com",
+ scope="read:data write:data"
+ )
+)
+
+# Access the exchanged tokens
+print(f"Access Token: {response.access_token}")
+print(f"Expires In: {response.expires_in} seconds")
+if response.id_token:
+ print(f"ID Token: {response.id_token}")
+```
+
+`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+## 2. Login with Token Exchange
+
+Exchange a custom token AND establish a user session.
+
+```python
+from auth0_server_python.auth_types import LoginWithCustomTokenExchangeOptions
+from fastapi import Request, Response
+
+# Exchange token and create session
+result = await auth0.login_with_custom_token_exchange(
+ LoginWithCustomTokenExchangeOptions(
+ subject_token="custom-token-from-external-system",
+ subject_token_type="urn:acme:mcp-token",
+ audience="https://api.example.com"
+ ),
+ store_options={"request": request, "response": response}
+)
+
+# User is now logged in
+user = result.state_data["user"]
+print(f"User logged in: {user['sub']}")
+```
+
+> **TIP**: Use `login_with_custom_token_exchange()` when you need both token exchange and session management (e.g., user migration flows). Use `custom_token_exchange()` for pure token exchange scenarios (e.g., service-to-service authentication).
+
+## 3. Actor Tokens (Delegation)
+
+Enable delegation scenarios where one party acts on behalf of a user. The acting party is supplied via `actor_token`, and Auth0 records it in the [`act` claim](https://datatracker.ietf.org/doc/html/rfc8693#section-4.1) on the issued tokens.
+
+```python
+# Service acting on behalf of a user
+response = await auth0.custom_token_exchange(
+ CustomTokenExchangeOptions(
+ subject_token="user-access-token",
+ subject_token_type="urn:ietf:params:oauth:token-type:access_token",
+ actor_token="service-access-token",
+ actor_token_type="urn:ietf:params:oauth:token-type:access_token",
+ audience="https://api.example.com"
+ )
+)
+
+# The actor claim is surfaced on the response. It may nest for delegation chains.
+if response.act:
+ print(f"Acting party: {response.act['sub']}")
+```
+
+> **NOTE**: `response.act` is read from the ID token. Auth0 writes the same `act` claim onto the issued access token as well, so they reflect the same acting party. The access token may be opaque, in which case `act` cannot be read off it directly - the ID token is where you read it.
+
+When you establish a session with `login_with_custom_token_exchange()`, the `act` claim is persisted on the session user and can be read back later via `get_user()`:
+
+```python
+result = await auth0.login_with_custom_token_exchange(
+ LoginWithCustomTokenExchangeOptions(
+ subject_token="user-access-token",
+ subject_token_type="urn:ietf:params:oauth:token-type:access_token",
+ actor_token="service-access-token",
+ actor_token_type="urn:ietf:params:oauth:token-type:access_token",
+ ),
+ store_options={"request": request, "response": response}
+)
+
+user = result.state_data["user"]
+if user.get("act"):
+ print(f"Acting party: {user['act']['sub']}")
+```
+
+> **NOTE**: When an `actor_token` is present, Auth0 does not issue a refresh token (the `offline_access` scope is dropped). A subsequent refresh-token grant therefore cannot re-emit the `act` claim, so the acting party is fixed at exchange time.
+
+## 4. Custom Authorization Parameters
+
+Pass additional parameters to the token endpoint.
+
+```python
+response = await auth0.custom_token_exchange(
+ CustomTokenExchangeOptions(
+ subject_token="custom-token",
+ subject_token_type="urn:acme:mcp-token",
+ audience="https://api.example.com",
+ authorization_params={
+ "custom_field": "custom_value"
+ }
+ )
+)
+```
+
+> **NOTE**: Critical parameters (`grant_type`, `client_id`, `subject_token`, `subject_token_type`) cannot be overridden via `authorization_params` for security reasons.
+
+## 5. Organization Support
+
+Specify an organization when exchanging tokens.
+
+```python
+response = await auth0.custom_token_exchange(
+ CustomTokenExchangeOptions(
+ subject_token="custom-token",
+ subject_token_type="urn:acme:mcp-token",
+ audience="https://api.example.com",
+ organization="org_abc1234"
+ )
+)
+```
+
+## 6. Error Handling
+
+```python
+from auth0_server_python.error import CustomTokenExchangeError
+
+try:
+ response = await auth0.custom_token_exchange(
+ CustomTokenExchangeOptions(
+ subject_token="token",
+ subject_token_type="urn:acme:mcp-token"
+ )
+ )
+except CustomTokenExchangeError as e:
+ print(f"Exchange failed: {e.code} - {e.message}")
+```
+
+### Common Error Codes
+
+- `INVALID_TOKEN_FORMAT`: Token is empty, whitespace-only, or has "Bearer " prefix
+- `MISSING_ACTOR_TOKEN_TYPE`: `actor_token` provided without `actor_token_type`
+- `MISSING_ACTOR_TOKEN`: `actor_token_type` provided without `actor_token`
+- `TOKEN_EXCHANGE_FAILED`: General token exchange failure
+- `INVALID_RESPONSE`: Auth0 returned a non-JSON response
+
+## 7. Token Type URIs
+
+Use standard URNs when possible:
+
+```python
+# Standard token types
+"urn:ietf:params:oauth:token-type:jwt" # JWT tokens
+"urn:ietf:params:oauth:token-type:access_token" # OAuth access tokens
+"urn:ietf:params:oauth:token-type:id_token" # OpenID Connect ID tokens
+"urn:ietf:params:oauth:token-type:refresh_token" # OAuth refresh tokens
+
+# Custom token types (use your own namespace)
+"urn:acme:mcp-token"
+"urn:company:legacy-token"
+```
+
+## 8. Impersonation via Session Transfer (STT)
+
+Custom Token Exchange can also mint a **Session Transfer Token (STT)** instead of an API access token. An STT lets an initiator app (for example a support console) log an agent into a target web app **as** a customer, with the agent recorded in the `act` claim - so a support engineer can reproduce a customer's exact experience without their password.
+
+This is a two-role, two-hop flow:
+
+- **Initiator** (the agent's app) mints the STT and redirects with it. This is where the new SDK methods live.
+- **Target** (the customer's app) forwards the STT to `/authorize` on a normal interactive login, which establishes the impersonated session.
+
+The STT is opaque, single-use, and short-lived (~60s). The SDK requests it and helps build the redirect - it never decodes or stores it.
+
+### Initiator: request an STT and build the redirect
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+from auth0_server_python.error import CustomTokenExchangeError
+
+# Mint the STT. The audience (urn:{domain}:session_transfer), grant type, and the actor are
+# set by the SDK - the actor is sourced from the logged-in agent's session.
+result = await auth0.request_session_transfer_token(
+ subject_token=subject_token, # your proof of which customer to impersonate
+ subject_token_type="urn:acme:customer-subject",
+ organization=None, # optional, sent on the mint request (separate from the redirect)
+ store_options={"request": request, "response": None},
+)
+
+# result.session_transfer_token is the opaque, one-shot STT (~60s). Never store it.
+redirect_url = auth0.build_session_transfer_redirect(
+ "https://customer-app.example.com/auth/login", result, organization=None
+)
+return RedirectResponse(redirect_url) # your framework performs the redirect
+```
+
+`SessionTransferTokenResult` carries `session_transfer_token`, `issued_token_type` (the session-transfer URN - the field to branch on), `expires_in`, and an informational `token_type` (`N_A`). There is no `act` on this result; `act` appears later, on the target session.
+
+> **NOTE**: An actor is mandatory - an STT is only issued when the Action set one. By default the SDK sources the actor from the logged-in agent's session ID token, refreshing it when expired. If the agent is not logged in (no usable session ID token and none can be refreshed), the call fails client-side with `ACTOR_UNAVAILABLE` before any network request.
+
+> **NOTE**: To use your own actor token instead of the session, pass `actor_token` (and optionally `actor_token_type`, which defaults to the ID token URN). An explicit `actor_token` takes precedence and the session is not read at all. When `actor_token_type` is the ID token URN (the default), Auth0 validates the token, so it must be:
+>
+> - Signed with RS256 or PS256 (HS256 is rejected, it uses a shared secret).
+> - Unexpired, and carrying `sub`, `iss`, `exp`, and `iat`.
+> - Issued to the same client making the exchange (its `aud` must be that client's ID).
+> - Belonging to a user who still exists and is not blocked.
+>
+> An Auth0 ID token from the agent's own session on this client satisfies all of these. A token that fails any of them is rejected by the server.
+>
+> ```python
+> result = await auth0.request_session_transfer_token(
+> subject_token=subject_token,
+> subject_token_type="urn:acme:customer-subject",
+> actor_token=agent_id_token, # explicit override - session is not used
+> store_options={"request": request, "response": None},
+> )
+> ```
+
+### Target: forward the STT to `/authorize`
+
+On the target, the STT rides through your normal login. `start_interactive_login` forwards arbitrary authorization parameters to `/authorize`, so your login route just passes `session_transfer_token` (and `organization`, when you want the target login org-scoped) straight through:
+
+```python
+from auth0_server_python.auth_types import StartInteractiveLoginOptions
+
+url = await auth0.start_interactive_login(
+ StartInteractiveLoginOptions(authorization_params={
+ "session_transfer_token": request.query_params["session_transfer_token"],
+ # "organization": org, # when you want the target login org-scoped
+ }),
+ store_options={"request": request, "response": None},
+)
+return RedirectResponse(url)
+```
+
+After the callback completes, read the acting party off the session user - the same way as the [Actor Tokens (Delegation)](#3-actor-tokens-delegation) section above:
+
+```python
+session = await auth0.get_session(store_options={"request": request, "response": None})
+act = (session or {}).get("user", {}).get("act")
+if act:
+ print(f"Impersonated by: {act['sub']}") # drive an impersonation banner, etc.
+```
+
+> **NOTE**: Both clients need one-time configuration through the Auth0 Dashboard or Management API. The issuing (initiator) client must be allowed to create session transfer tokens. The redeeming (target) client must be allowed to accept delegated-access sessions and to receive the token as a query parameter. See the [Auth0 documentation](https://auth0.com/docs/authenticate/custom-token-exchange) for the exact client settings.
+
+> **NOTE**: `build_session_transfer_redirect` attaches a single-use credential to `target_login_url`, so that URL must be a trusted, app-controlled value - never one derived from untrusted input (such as a user-supplied `returnTo`), which could leak the token to an attacker host.
+
+> **NOTE**: The impersonation session is hard-capped at 2 hours and cannot mint a refresh token (`offline_access` is dropped when an actor is present). To continue past that, re-run the flow.
+
+### STT error codes
+
+- `ACTOR_UNAVAILABLE`: no usable actor token (client-side; raised before any network call)
+- `SETACTOR_REQUIRED`: an STT was requested but the Action did not call `setActor` (server 400)
+- `SESSION_TRANSFER_DISABLED`: the session-transfer feature is not enabled for the tenant/client (server 400)
+
+## Additional Resources
+
+- [Auth0 Custom Token Exchange Documentation](https://auth0.com/docs/authenticate/custom-token-exchange)
+- [RFC 8693 - OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)
diff --git a/docs/dist/examples/EnterpriseConnect.mdx b/docs/dist/examples/EnterpriseConnect.mdx
new file mode 100644
index 00000000..4a00ee99
--- /dev/null
+++ b/docs/dist/examples/EnterpriseConnect.mdx
@@ -0,0 +1,292 @@
+---
+title: "Enterprise Connect (Embedded Login)"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Enterprise Connect lets your application sign users in through their company's identity provider while **your app owns the session**. Auth0 acts as a pure SSO relay: it authenticates the user and returns verified claims, but issues no refresh token and holds no session on your behalf. This guide covers the `enterprise_connect` mode on `ServerClient`, email-domain discovery, the callback contract, and federated logout.
+
+
+
+Enterprise Connect is an Early Access feature. Tenant setup (entitlements, connection type, and the exact claims a token carries) depends on your Auth0 configuration and may change. Treat the tenant-side steps below as a starting point and confirm them against your tenant. The SDK behavior described here is stable.
+
+
+
+
+
+These flows are for confidential server-side applications. The verified claims and access token are handed to your server, which creates and owns the user session. The browser should only ever receive your application's own session cookie or opaque session reference, never an Auth0 token.
+
+
+
+## Table of Contents
+
+- [How the flow works](#how-the-flow-works)
+- [Prerequisites](#prerequisites)
+- [1. Configure the client](#1-configure-the-client)
+- [2. Discover and start the login](#2-discover-and-start-the-login)
+- [3. Complete the callback](#3-complete-the-callback)
+- [4. Protect your routes](#4-protect-your-routes)
+- [5. Organizations and multi-tenant apps](#5-organizations-and-multi-tenant-apps)
+- [6. Logout](#6-logout)
+- [Available and blocked methods](#available-and-blocked-methods)
+- [Error Handling](#error-handling)
+
+## How the flow works
+
+1. The user enters their email. Your app calls `start_enterprise_login()`, which runs [WebFinger](https://datatracker.ietf.org/doc/html/rfc7033) discovery on the email domain.
+2. If the domain is managed by Auth0 for enterprise SSO, the SDK returns an authorization URL with the email as `login_hint` so Auth0 can resolve the connection and organization. If it is not managed, the method returns `None` and your app falls back to its own login.
+3. The user authenticates at their identity provider and is redirected back to your callback.
+4. Your app calls `complete_interactive_login()`. The SDK exchanges the code, verifies the ID token's signature and issuer, and returns the claims from it. It persists **nothing** and issues no refresh token.
+5. Your app creates its own first-party session from the returned claims.
+
+The contract is inverted from a normal login: the SDK does not store a session, so the session-reading methods (`get_session`, `get_access_token`) are not available in this mode.
+
+## Prerequisites
+
+Enterprise Connect requires a **Regular Web Application** with a client secret. The tenant and connection must be provisioned for Enterprise Connect (Early Access), and WebFinger discovery must be enabled on the tenant. Work with your Auth0 contact to confirm entitlements for your tenant.
+
+Do not request `offline_access` and do not set a static `organization` on the client. Enterprise Connect issues no refresh token, and the organization is resolved from the login email at Auth0. The SDK warns at construction if either is set.
+
+## 1. Configure the client
+
+Opt in with `enterprise_connect=True`. Supply a `transaction_store` (used to protect the callback with `state` and PKCE). A `state_store` is not needed because the SDK persists no session.
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+
+server_client = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ redirect_uri="https://app.example.com/auth/callback",
+ authorization_params={"scope": "openid profile email"},
+ enterprise_connect=True,
+)
+```
+
+For apps using request/response-backed stores or multiple custom domains, pass `store_options={"request": request, "response": response}` to each method that reads or writes transaction state.
+
+
+
+The SDK writes its transaction cookie onto the `response` you pass in `store_options`: the `state` and PKCE values when a login starts, and a deletion of that cookie when the callback completes. Return that same `response` object, with your own redirect and session cookie set on it. Do not return a fresh `redirect(url)` built after the SDK call. A new response object carries none of the SDK's `Set-Cookie` headers, so the transaction cookie never reaches the browser and the callback finds no transaction to match. The examples below create the response first, thread it through `store_options`, then set its `Location` last. The pattern is framework-neutral: `redirect(...)`, `.set_cookie(...)`, and `.headers["Location"]` map onto the equivalent calls in your web framework.
+
+
+
+## 2. Discover and start the login
+
+Your app must serve a login page that collects the user's work email. Pass it to `start_enterprise_login()`, which runs WebFinger discovery and returns the authorization URL when the domain is managed, or `None` when it is not.
+
+```python
+from auth0_server_python.auth_types import StartEnterpriseLoginOptions
+from auth0_server_python.error import MissingRequiredArgumentError, InvalidArgumentError
+
+response = redirect("/") # placeholder target, overwritten once discovery returns
+
+try:
+ auth_url = await server_client.start_enterprise_login(
+ StartEnterpriseLoginOptions(
+ email=user_email,
+ app_state={"return_to": "/dashboard"},
+ ),
+ store_options={"request": request, "response": response},
+ )
+except (MissingRequiredArgumentError, InvalidArgumentError):
+ auth_url = None
+
+response.headers["Location"] = auth_url or "/login/password"
+return response
+```
+
+
+
+Discovery is a routing hint, not an authorization decision. It fails closed to "not managed" on any error, so a discovery failure routes the user to your fallback login rather than granting access. It never, on its own, signs anyone in - the callback must still complete.
+
+
+
+If you only need the discovery signal (for example, to decide which login button to show), call the standalone helper. It takes no client instance and is stateless.
+
+```python
+from auth0_server_python.auth_server import is_federated_domain
+
+managed = await is_federated_domain("YOUR_AUTH0_DOMAIN", "acme.example")
+```
+
+## 3. Complete the callback
+
+When Auth0 redirects back, call `complete_interactive_login()`. In Enterprise Connect mode it returns the verified claims and an access token instead of a session record.
+
+```python
+response = redirect("/") # placeholder target, overwritten once app_state is known
+
+result = await server_client.complete_interactive_login(
+ str(request.url),
+ store_options={"request": request, "response": response},
+)
+
+user = result["user"]
+
+response.set_cookie(
+ "app_session",
+ sign_session({"sub": user.get("sub"), "email": user.get("email"), "org_id": user.get("org_id") or ""}),
+ httponly=True,
+ secure=True,
+ samesite="lax",
+)
+
+app_state = result.get("app_state") or {}
+response.headers["Location"] = app_state.get("return_to", "/")
+return response
+```
+
+The returned dict contains:
+
+- `user` - the verified claims from the ID token, as a dict
+- `token_set` - `audience`, `access_token`, `scope`, and `expires_at`
+- `id_token` - the raw ID token, for your own use
+- `domain` - the Auth0 domain the login came from
+- `app_state` - present only when you passed `app_state` at `start_enterprise_login()`
+
+`result` and `user` are both plain dicts. Read claims with `user.get("sub")` so a claim the connection did not return yields `None` instead of raising `KeyError`. `sub` is always present, the optional claims (`email`, `org_id`, `name`) may not be.
+
+The SDK verifies the ID token's signature and issuer, and derives the returned claims from it, before returning. It does not write a session store record and retains no refresh token.
+
+
+
+The access token expires and cannot be renewed. Enterprise Connect issues no refresh token, so when it expires the user must re-authenticate. `token_set["expires_at"]` is a Unix timestamp in seconds. Compare it with `time.time()` before using the access token. For API authorization, issue your own tokens from the callback claims rather than relying on the Auth0 access token long-term.
+
+
+
+### Sign the session cookie
+
+The session cookie is your trust boundary now, not Auth0's. A signed-in user controls their own cookies, so an unsigned session lets them rewrite `sub`, `email`, or `org_id` to impersonate another user or forge membership in an organization they were denied. Sign the cookie with a server-side secret and verify the signature in constant time before trusting any claim in it.
+
+```python
+import base64
+import hashlib
+import hmac
+import json
+from typing import Optional
+
+_SESSION_SECRET = b"YOUR_SESSION_SECRET"
+
+def sign_session(claims: dict) -> str:
+ body = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip("=")
+ sig = hmac.new(_SESSION_SECRET, body.encode(), hashlib.sha256).hexdigest()
+ return f"{body}.{sig}"
+
+def read_session(cookie: Optional[str]) -> Optional[dict]:
+ if not cookie or "." not in cookie:
+ return None
+ body, _, sig = cookie.partition(".")
+ expected = hmac.new(_SESSION_SECRET, body.encode(), hashlib.sha256).hexdigest()
+ if not hmac.compare_digest(sig, expected):
+ return None
+ padded = body + "=" * (-len(body) % 4)
+ try:
+ return json.loads(base64.urlsafe_b64decode(padded).decode())
+ except ValueError:
+ return None
+```
+
+
+
+Hold `_SESSION_SECRET` server-side only (Tier 0) and never ship it to the browser. Use a dedicated secret rather than reusing one for another purpose. The `hmac.compare_digest` check is what makes a forged cookie fail closed - a plain `==` comparison leaks the signature through timing.
+
+
+
+## 4. Protect your routes
+
+Your app owns the session - the SDK holds nothing. Check your session store at the start of any route that requires authentication and redirect to your login page when the session is absent.
+
+```python
+session = read_session(request.cookies.get("app_session"))
+if not session:
+ return redirect("/login")
+
+your_template.render(user=session)
+```
+
+
+
+Do not store an Auth0 access or ID token in your session. Store only the claims you need (for example `sub`, `email`, `org_id`). The browser must never see an Auth0 token.
+
+
+
+## 5. Organizations and multi-tenant apps
+
+Auth0 stamps the resolved organization into the token as `org_id`, available on `result["user"]`. The SDK surfaces it but does not enforce it.
+
+## 6. Logout
+
+Clear your own application session first, then send the user to the Auth0 logout URL. For Enterprise Connect, always request federated logout with `federated=True`.
+
+```python
+from auth0_server_python.auth_types import LogoutOptions
+
+response = redirect("/") # placeholder target, overwritten once logout returns
+clear_app_session(response) # delete your session cookie on the response you return
+
+logout_url = await server_client.logout(
+ LogoutOptions(return_to="https://app.example.com/login", federated=True),
+ store_options={"request": request, "response": response},
+)
+
+response.headers["Location"] = logout_url
+return response
+```
+
+Enterprise Connect signs the user in through their enterprise identity provider and keeps no refresh token, so the identity provider session is the durable credential, not anything your app or Auth0 holds. A non-federated logout clears your app session and the Auth0 session but leaves that identity provider session alive, so the next `start_enterprise_login()` signs the user straight back in without a prompt. On a shared device that is not a real logout, which is why Enterprise Connect logout must be federated.
+
+
+
+Federated logout also ends the user's session at other applications that share the same enterprise SSO, not just yours. That is the intended reach of an enterprise sign-out. It depends on the identity provider supporting federated logout, so confirm that on the connection.
+
+
+
+
+
+`return_to` must be an absolute URL on your tenant's Allowed Logout URLs list. Auth0 rejects a URL that is not allow-listed.
+
+
+
+## Available and blocked methods
+
+These members work in Enterprise Connect mode:
+
+| Member | Notes |
+|---|---|
+| `start_enterprise_login()` | EC login entry point |
+| `start_interactive_login()` | Writes the transaction store only |
+| `complete_interactive_login()` | Returns verified claims without persisting a session |
+| `logout()` | Returns the Auth0 logout URL. No session or transaction state to clear in this mode |
+| `custom_token_exchange()` | Works once, while the callback access token is valid. No refresh after it expires |
+| `handle_backchannel_logout()` | No-op. The SDK holds no session to revoke |
+
+Everything else raises `EnterpriseConnectError` with `code` `enterprise_connect_not_supported`. The message names the member that was called. For a session or access token, read them from `complete_interactive_login()` instead.
+
+Own the session and any token refresh in your app.
+
+## Error Handling
+
+```python
+from auth0_server_python.error import ApiError, EnterpriseConnectError
+
+try:
+ result = await server_client.complete_interactive_login(
+ str(request.url),
+ store_options={"request": request, "response": response},
+ )
+except ApiError as e:
+ return {"error": e.code}
+
+try:
+ await server_client.get_access_token()
+except EnterpriseConnectError as e:
+ return {"error": e.code}
+```
+
+Errors you may see:
+
+- `EnterpriseConnectError` - a session or refresh-dependent member is not available in this mode. Its `code` is always `enterprise_connect_not_supported`. The message names the member that was called
+- `ApiError` - the token exchange failed, or the login returned no verifiable claims (`invalid_response`)
diff --git a/docs/dist/examples/InteractiveLogin.mdx b/docs/dist/examples/InteractiveLogin.mdx
new file mode 100644
index 00000000..2a00841a
--- /dev/null
+++ b/docs/dist/examples/InteractiveLogin.mdx
@@ -0,0 +1,202 @@
+---
+title: "Interactive Login"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Interactive login in `auth0‑server‑python` is a two‑step process. First, you start the login flow by obtaining an authorization URL; then, after the user authenticates at Auth0 and is redirected back, you complete the login flow to exchange the authorization code for tokens.
+
+This guide covers how to customize the authorization parameters, pass custom app state, enable **Pushed Authorization Requests (PAR)** and **Rich Authorization Requests (RAR)**, supply store options, and log in to an organization. For organization-specific flows, see [OrganizationLogin.md](/docs/sdk/python/server/examples/OrganizationLogin) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/OrganizationLogin.md)).
+
+## 1. Starting Interactive Login
+
+### Configuring the Redirect URI
+
+Interactive login begins by configuring a redirect_uri—the URL Auth0 will use to send the user back after authentication. For example, when instantiating your core `ServerClient`:
+
+```python
+from auth_server.server_client import ServerClient
+
+server_client = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ authorization_params={
+ "redirect_uri":"http://localhost:3000/auth/callback",
+ }
+)
+```
+
+`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+Now call `start_interactive_login()` to obtain the authorization URL and redirect the user:
+```python
+authorization_url = await server_client.start_interactive_login()
+```
+## 2. Passing Authorization Params
+You can customize the parameters sent to Auth0's `/authorize` endpoint in two ways:
+
+### Global Configuration
+
+When creating your ServerClient, you can specify default parameters:
+
+```python
+server_client = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ redirect_uri="http://localhost:3000/auth/callback",
+ secret="YOUR_SECRET",
+ authorization_params={
+ "scope": " your scopes",
+ "audience": "urn:custom:api",
+ }
+)
+```
+### Dynamic Configuration Per Call
+You can also override or add parameters when calling `start_interactive_login()`:
+```python
+authorization_url = await server_client.start_interactive_login({
+ "authorization_params": {
+ "scope": "openid profile email",
+ "audience": "urn:custom:api",
+ "foo": "bar" # arbitrary custom parameter
+ }
+})
+```
+
+
+Any parameter specified here will override the corresponding global configuration.
+
+
+
+### Experiment Center Overrides
+Auth0 Experiment Center runs A/B tests on your login flows, and by default Auth0 assigns each user to a variation automatically. To force a specific variation for a single login (for example while reproducing a variation during debugging), pass `experiment_id` and `variation_id` as authorization params on the login call. Both IDs come from your Auth0 Dashboard or the Management API:
+```python
+from auth0_server_python.auth_types import StartInteractiveLoginOptions
+
+authorization_url = await server_client.start_interactive_login(
+ StartInteractiveLoginOptions(
+ authorization_params={
+ "experiment_id": "exp_123",
+ "variation_id": "var_456",
+ }
+ )
+)
+```
+When the experiment uses segment targeting, also pass `segment_id`:
+```python
+authorization_url = await server_client.start_interactive_login(
+ StartInteractiveLoginOptions(
+ authorization_params={
+ "experiment_id": "exp_123",
+ "variation_id": "var_456",
+ "segment_id": "seg_789",
+ }
+ )
+)
+```
+The override applies to this login request only.
+
+
+
+Pass these per call, not in the client-level `authorization_params` at construction. A construction-time value pins every login to the same variation and defeats the experiment.
+
+Experiment Center is an Enterprise feature. Refer to the [Experiment Center documentation](https://auth0.com/docs/customize/experiment-center/overview) for more information and setup.
+
+
+
+## 3. Passing App State to Track State During Login
+
+The `app_state` parameter allows you to pass custom state (for example, a return URL) that is later available when the login process completes.
+
+```python
+# Start interactive login with custom app state:
+authorize_url = await server_client.start_interactive_login({
+ "app_state": {"returnTo": "http://localhost:3000/dashboard"}
+})
+
+# Later, after completing login:
+result = await server_client.complete_interactive_login(callback_url)
+print(result.get("app_state").get("returnTo")) # Should output: http://localhost:3000/dashboard
+```
+
+
+- `authorize_url` is the URL for Auth0's /authorize endpoint (or a URL built from PAR, if enabled).
+- `callback_url` is the URL Auth0 redirects back to after authentication.
+
+
+
+## 4. Using Pushed Authorization Requests (PAR)
+
+To enable PAR, simply set the flag in your interactive login options. When enabled, the SDK will send an HTTP POST request with the authorization parameters to the PAR endpoint (retrieved from OIDC metadata) and use the returned `request_uri` to build the final authorization URL.
+```python
+# Enable PAR dynamically for a login call:
+authorization_url = await server_client.start_interactive_login({
+ "pushed_authorization_requests": True
+})
+```
+
+
+Using PAR requires that your Auth0 tenant is configured to support it. Refer to Auth0's documentation for details.
+
+
+
+## 5. Using Pushed Authorization Requests and Rich Authorization Requests (RAR)
+
+When using PAR, you can also supply Rich Authorization Request details by including an `authorization_details` field in the `authorization_params`:
+```python
+import json
+
+authorization_url = await server_client.start_interactive_login({
+ "pushed_authorization_requests": True,
+ "authorization_params": {
+ "authorization_details": json.dumps([{
+ "type": "your_type",
+ "additional_field": "value"
+ }])
+ }
+})
+```
+After completing the interactive login, the SDK will expose the `authorization_details` in the result:
+```python
+import json
+
+authorization_url = await server_client.start_interactive_login({
+result = await server_client.complete_interactive_login(callback_url)
+print(result.get("authorization_details"))
+```
+
+
+Both PAR and RAR require that these features are enabled in your Auth0 dashboard.
+
+
+
+## 6. Passing Store Options
+Most methods in the SDK accept a second argument called `store_options`. This dictionary should include the HTTP Request and Response objects (or equivalent) that the store uses to manage cookies and session data.
+```python
+store_options = {"request": request, "response": response}
+authorization_url = await server_client.start_interactive_login({}, store_options=store_options)
+```
+This enables the SDK to correctly read and set cookies for session management.
+
+## 7. Completing Interactive Login
+
+After the user is redirected back to your callback URL from Auth0, you call `complete_interactive_login()` to finalize the authentication process. This method extracts the authorization code from the URL, exchanges it for tokens, and returns session data (including any app_state you passed originally, and—if using RAR—the `authorization_details`).
+```python
+result = await server_client.complete_interactive_login(callback_url, store_options={"request": request, "response": response})
+print(result.get("app_state").get("returnTo")) # Custom app state
+print(result.get("authorization_details")) # Rich Authorization Requests details (if any)
+
+```
+
+
+
+The `callback_url` must include the necessary parameters (`state` and `code`) that Auth0 sends upon successful authentication.
+
+
+
+## 8. Organizations
+
+For dedicated-org and multi-org login patterns, accepting invitations, handling organization errors, and reading org data from the session, see [OrganizationLogin.md](/docs/sdk/python/server/examples/OrganizationLogin) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/OrganizationLogin.md)).
diff --git a/docs/dist/examples/MFA.mdx b/docs/dist/examples/MFA.mdx
new file mode 100644
index 00000000..d7413834
--- /dev/null
+++ b/docs/dist/examples/MFA.mdx
@@ -0,0 +1,891 @@
+---
+title: "Multi-Factor Authentication (MFA)"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+The Auth0 MFA API allows you to manage multi-factor authentication for users in server-side applications. This guide covers how to handle MFA requirements, enroll authenticators, and verify MFA challenges.
+
+
+
+Multi-Factor Authentication support for server SDKs is in Early Access. For detailed information, refer to the [Auth0 MFA documentation](https://auth0.com/docs/secure/multi-factor-authentication).
+
+
+
+## Table of Contents
+
+- [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa)
+ - [Table of Contents](#table-of-contents)
+ - [Setup](#setup)
+ - [Understanding MFA Responses](#understanding-mfa-responses)
+ - [Challenge Flow Response](#challenge-flow-response)
+ - [Enroll Flow Response](#enroll-flow-response)
+ - [Handling MFA Required Errors](#handling-mfa-required-errors)
+ - [MFA Token Encryption Design](#mfa-token-encryption-design)
+ - [Getting Authenticators](#getting-authenticators)
+ - [Response Structure](#response-structure)
+ - [Getting Enrollment Factors](#getting-enrollment-factors)
+ - [Enrollment](#enrollment)
+ - [Enrolling OTP (Authenticator App)](#enrolling-otp-authenticator-app)
+ - [Enrolling SMS](#enrolling-sms)
+ - [Enrolling Voice](#enrolling-voice)
+ - [Enrolling Email](#enrolling-email)
+ - [Enrolling Push Notification (Auth0 Guardian)](#enrolling-push-notification-auth0-guardian)
+ - [Challenge](#challenge)
+ - [Challenge with SMS](#challenge-with-sms)
+ - [Challenge with Email](#challenge-with-email)
+ - [Challenge with OTP](#challenge-with-otp)
+ - [Challenge with Push Notification](#challenge-with-push-notification)
+ - [Verify](#verify)
+ - [Verify with OOB (SMS or Email)](#verify-with-oob-sms-or-email)
+ - [Verify with OTP](#verify-with-otp)
+ - [Verify with Recovery Code](#verify-with-recovery-code)
+ - [Verify with Push Notification (Polling)](#verify-with-push-notification-polling)
+ - [Verify with DPoP (sender-constrained tokens)](#verify-with-dpop-sender-constrained-tokens)
+ - [Session Persistence](#session-persistence)
+ - [Automatic Session Update](#automatic-session-update)
+ - [Manual Session Update](#manual-session-update)
+ - [Complete MFA Flow Examples](#complete-mfa-flow-examples)
+ - [Enrollment Flow](#enrollment-flow)
+ - [Challenge Flow](#challenge-flow)
+ - [Complete Login with MFA](#complete-login-with-mfa)
+ - [Error Handling](#error-handling)
+ - [Common Error Scenarios](#common-error-scenarios)
+ - [Additional Resources](#additional-resources)
+
+## Setup
+
+Before using the MFA API, ensure MFA is configured in your [Auth0 Dashboard](https://manage.auth0.com) under **Security** > **Multi-factor Auth**. For detailed configuration, see the [Auth0 MFA documentation](https://auth0.com/docs/secure/multi-factor-authentication/customize-mfa/customize-mfa-enrollments-universal-login).
+
+
+
+**Multiple Custom Domains (MCD)**: All MFA methods accept an optional `store_options` parameter (as a top-level argument, not inside `options`). When using a callable domain resolver, pass `store_options={"request": request, "response": response}` so the resolver can determine the correct tenant domain from the incoming request.
+
+
+
+## Understanding MFA Responses
+
+When MFA is required during authentication, the error response contains an `mfa_requirements` object that indicates either:
+
+1. **Challenge Flow** (user has enrolled authenticators)
+2. **Enroll Flow** (user needs to set up MFA)
+
+### Challenge Flow Response
+
+```json
+{
+ "error": "mfa_required",
+ "error_description": "Multifactor authentication required",
+ "mfa_token": "Fe26.2*...",
+ "mfa_requirements": {
+ "challenge": [
+ { "type": "otp" },
+ { "type": "sms" },
+ { "type": "email" },
+ { "type": "auth0" }
+ ]
+ }
+}
+```
+
+**Interpretation**: User has enrolled authenticators → proceed with **Get Authenticators → Challenge → Verify** flow
+
+### Enroll Flow Response
+
+```json
+{
+ "error": "mfa_required",
+ "error_description": "Multifactor authentication required",
+ "mfa_token": "Fe26.2*...",
+ "mfa_requirements": {
+ "enroll": [
+ { "type": "otp" },
+ { "type": "sms" },
+ { "type": "email" },
+ { "type": "auth0" }
+ ]
+ }
+}
+```
+
+**Interpretation**: User needs to set up MFA → proceed with **Get Enrollment Factors → Enroll → Verify** flow
+
+## Handling MFA Required Errors
+
+When `get_access_token()` encounters an MFA requirement, it raises an `MfaRequiredError` with the `mfa_token` context:
+
+```python
+from auth0_server_python.error import MfaRequiredError
+from auth0_server_python.auth_server import ServerClient
+
+server_client = ServerClient(
+ domain="",
+ client_id="",
+ client_secret=""
+)
+
+try:
+ access_token = await server_client.get_access_token()
+except MfaRequiredError as error:
+ print(f"MFA Required: {error.error_description}")
+
+ # Decrypt the encrypted mfa_token before using MFA methods
+ context = server_client.mfa.decrypt_mfa_token(error.mfa_token)
+ mfa_token = context.mfa_token # Raw token for MFA API calls
+```
+
+`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+
+
+`get_access_token()` is not the only origin of `MfaRequiredError`. A passkey login (`signin_with_passkey`) raises the same error when a second factor is required — but there is **no session yet** at that point, which changes how you complete and persist the flow. See [Passkeys.md → Completing MFA on a passkey login](/docs/sdk/python/server/examples/Passkeys#completing-mfa-on-a-passkey-login-and-where-the-session-comes-from) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/Passkeys.md#completing-mfa-on-a-passkey-login-and-where-the-session-comes-from)).
+
+
+
+### MFA Token Encryption Design
+
+The MFA token returned by `MfaRequiredError` from `get_access_token()` is **encrypted** by `ServerClient` before it reaches the caller. This prevents token tampering and replay when the token is stored client-side (e.g. in a cookie or hidden form field).
+
+All `MfaClient` API methods (`list_authenticators`, `enroll_authenticator`, `challenge_authenticator`, `verify`) operate on **raw** MFA tokens. The caller must decrypt the token using `decrypt_mfa_token()` before passing it to any MFA method:
+
+```python
+except MfaRequiredError as error:
+ encrypted_mfa_token = error.mfa_token # Encrypted by ServerClient
+
+ # Decrypt before calling MFA methods
+ context = server_client.mfa.decrypt_mfa_token(encrypted_mfa_token)
+ raw_mfa_token = context.mfa_token
+
+ authenticators = await server_client.mfa.list_authenticators(
+ {"mfa_token": raw_mfa_token}
+ )
+```
+
+
+
+**Chained MFA**: If `verify()` raises `MfaRequiredError` (additional factor required), the `mfa_token` in that error is **raw** (not encrypted). This is by design — `MfaClient` is a transport-layer client that does not encrypt tokens. Encryption is the responsibility of the calling layer (`ServerClient` or the framework SDK like auth0-fastapi). Framework SDKs should encrypt the chained MFA token before returning it to the client.
+
+
+
+## Getting Authenticators
+
+Retrieve the list of authenticators the user has already enrolled:
+
+```python
+try:
+ authenticators = await server_client.mfa.list_authenticators(
+ {"mfa_token": mfa_token},
+ store_options={"request": request, "response": response} # Optional — required for MCD
+ )
+
+ for auth in authenticators:
+ print(f"Authenticator: {auth.id}")
+ print(f" Type: {auth.authenticator_type}")
+ print(f" Created: {auth.created_at}")
+
+except Exception as error:
+ print(f"Error retrieving authenticators: {error}")
+```
+
+### Response Structure
+
+Each authenticator is an `AuthenticatorResponse` object with:
+- `id`: Authenticator identifier (e.g., `otp|dev_xxx`)
+- `authenticator_type`: Type of authenticator (`otp`, `oob`, `recovery-code`)
+- `created_at`: Creation timestamp
+- `active`: Boolean indicating if authenticator is active
+- `oob_channel`: For OOB authenticators, the channel used (`sms`, `voice`, `email`, `auth0`)
+- `phone_number`: For SMS/Voice authenticators, the enrolled phone number
+- `last_auth`: Last authentication timestamp (if available)
+
+## Getting Enrollment Factors
+
+Check what MFA factors are available for enrollment:
+
+```python
+try:
+ factors = await server_client.mfa.list_authenticators({
+ "mfa_token": mfa_token
+ })
+
+ if len(factors) > 0:
+ print("Available enrollment factors:")
+ for factor in factors:
+ print(f" - {factor.authenticator_type}")
+ else:
+ print("User already has all available authenticators enrolled")
+
+except Exception as error:
+ print(f"Error retrieving enrollment factors: {error}")
+```
+
+## Enrollment
+
+### Enrolling OTP (Authenticator App)
+
+Enroll an OTP authenticator (Google Authenticator, Microsoft Authenticator, etc.):
+
+```python
+try:
+ enrollment = await server_client.mfa.enroll_authenticator({
+ "mfa_token": mfa_token,
+ "factor_type": "otp"
+ })
+
+ # Display QR code to user
+ print(f"QR Code URI: {enrollment.barcode_uri}") # otpauth://totp/...
+ print(f"Secret Key: {enrollment.secret}") # Base32 secret for manual entry
+
+except Exception as error:
+ print(f"Enrollment failed: {error}")
+```
+
+### Enrolling SMS
+
+Enroll an SMS authenticator:
+
+```python
+try:
+ enrollment = await server_client.mfa.enroll_authenticator({
+ "mfa_token": mfa_token,
+ "factor_type": "sms",
+ "phone_number": "+12025551234" # E.164 format
+ })
+
+ # Save oobCode for enrollment verification
+ print(f"OOB Code: {enrollment.oob_code}")
+ print("SMS sent to user's phone number")
+
+except Exception as error:
+ print(f"SMS enrollment failed: {error}")
+```
+
+### Enrolling Voice
+
+Enroll a voice call authenticator:
+
+```python
+try:
+ enrollment = await server_client.mfa.enroll_authenticator({
+ "mfa_token": mfa_token,
+ "factor_type": "voice",
+ "phone_number": "+12025551234" # E.164 format
+ })
+
+ print(f"OOB Code: {enrollment.oob_code}")
+ print("Voice call initiated to user's phone number")
+
+except Exception as error:
+ print(f"Voice enrollment failed: {error}")
+```
+
+### Enrolling Email
+
+Enroll an email authenticator:
+
+```python
+try:
+ enrollment = await server_client.mfa.enroll_authenticator({
+ "mfa_token": mfa_token,
+ "factor_type": "email",
+ "email": "user@example.com"
+ })
+
+ print(f"OOB Code: {enrollment.oob_code}")
+ print("Verification email sent to user")
+
+except Exception as error:
+ print(f"Email enrollment failed: {error}")
+```
+
+### Enrolling Push Notification (Auth0 Guardian)
+
+Enroll a push notification authenticator using Auth0 Guardian:
+
+```python
+try:
+ enrollment = await server_client.mfa.enroll_authenticator({
+ "mfa_token": mfa_token,
+ "factor_type": "auth0"
+ })
+
+ # Display Guardian QR code to user — they scan it with the Auth0 Guardian app
+ print(f"QR Code URI: {enrollment.barcode_uri}") # otpauth://... for Guardian app
+ print(f"OOB Code: {enrollment.oob_code}") # Used for polling verification status
+
+ # After user scans the QR code, poll for approval (see Verify with Push Notification)
+
+except Exception as error:
+ print(f"Push enrollment failed: {error}")
+```
+
+
+
+The `barcode_uri` is returned in the `OobEnrollmentResponse` and should be rendered as a QR code for the user to scan with the Auth0 Guardian app. After scanning, poll for verification using the `oob_code`.
+
+
+
+## Challenge
+
+After enrolling an authenticator, or when the user has existing authenticators, initiate a challenge:
+
+### Challenge with SMS
+
+```python
+try:
+ challenge = await server_client.mfa.challenge_authenticator({
+ "mfa_token": mfa_token,
+ "factor_type": "sms",
+ "authenticator_id": "sms|dev_xxx"
+ })
+
+ print(f"OOB Code: {challenge.oob_code}")
+ print(f"Challenge Expires In: {challenge.expires_in} seconds")
+ print("User will receive SMS with verification code")
+
+except Exception as error:
+ print(f"Challenge failed: {error}")
+```
+
+### Challenge with Email
+
+```python
+try:
+ challenge = await server_client.mfa.challenge_authenticator({
+ "mfa_token": mfa_token,
+ "factor_type": "email",
+ "authenticator_id": "email|dev_xxx"
+ })
+
+ print(f"OOB Code: {challenge.oob_code}")
+ print("User will receive verification email")
+
+except Exception as error:
+ print(f"Challenge failed: {error}")
+```
+
+### Challenge with OTP
+
+
+
+For OTP authenticators, you do not need to explicitly call challenge. The code is generated automatically by the user's authenticator app. Simply prompt the user to open their app and provide the 6-digit code.
+
+
+
+```python
+try:
+ challenge = await server_client.mfa.challenge_authenticator({
+ "mfa_token": mfa_token,
+ "factor_type": "otp",
+ "authenticator_id": "otp|dev_xxx"
+ })
+
+ print("User should open their authenticator app and provide the code")
+
+except Exception as error:
+ print(f"Challenge failed: {error}")
+```
+
+### Challenge with Push Notification
+
+```python
+try:
+ challenge = await server_client.mfa.challenge_authenticator({
+ "mfa_token": mfa_token,
+ "factor_type": "auth0",
+ "authenticator_id": "auth0|dev_xxx"
+ })
+
+ print(f"OOB Code: {challenge.oob_code}")
+ print(f"Challenge Expires In: {challenge.expires_in} seconds")
+ print("Push notification sent to user's device — poll for approval")
+
+except Exception as error:
+ print(f"Challenge failed: {error}")
+```
+
+
+
+Push notification challenges do not require a `binding_code` from the user. Instead, the user approves the notification on their device, and you poll the verify endpoint to check if they have approved.
+
+
+
+## Verify
+
+Complete MFA verification with the challenge response:
+
+### Verify with OOB (SMS or Email)
+
+```python
+try:
+ verify_response = await server_client.mfa.verify({
+ "mfa_token": mfa_token,
+ "oob_code": challenge.oob_code,
+ "binding_code": "123456", # Code user received via SMS/Email
+ "persist": True, # Persist tokens to session store
+ "audience": "https://api.example.com", # Required when persist=True
+ "scope": "openid profile email" # Optional scope
+ })
+
+ access_token = verify_response.access_token
+ id_token = verify_response.id_token
+
+ # Check for recovery code (returned on first-time enrollment)
+ if verify_response.recovery_code:
+ print(f"Recovery Code: {verify_response.recovery_code}")
+ print("Save this recovery code securely — it will only be shown once!")
+
+ print(f"MFA verification successful!")
+ print(f"Access Token: {access_token}")
+ print(f"ID Token: {id_token}")
+ print("Tokens have been persisted to session store")
+
+except Exception as error:
+ print(f"Verification failed: {error}")
+```
+
+
+
+Setting `persist=True` automatically updates the session store with the new tokens, eliminating the need for manual token management after MFA verification.
+
+
+
+
+
+The `verify()` response may include a `recovery_code` field. This is returned when a user completes their first MFA enrollment, or when they verify using a recovery code (a new one is generated to replace the used code). Always check for this field and display it to the user.
+
+
+
+### Verify with OTP
+
+```python
+try:
+ verify_response = await server_client.mfa.verify({
+ "mfa_token": mfa_token,
+ "otp": "123456", # 6-digit code from authenticator app
+ "persist": True, # Persist tokens to session store
+ "audience": "https://api.example.com", # Required when persist=True
+ "scope": "openid profile email"
+ })
+
+ access_token = verify_response.access_token
+
+ print("MFA verification successful!")
+ print("Tokens have been persisted to session store")
+
+except Exception as error:
+ print(f"Invalid OTP code: {error}")
+```
+
+### Verify with Recovery Code
+
+Recovery codes can be used to complete MFA verification without initiating a challenge:
+
+```python
+try:
+ verify_response = await server_client.mfa.verify({
+ "mfa_token": mfa_token,
+ "recovery_code": "XXXX-XXXX-XXXX", # One of the recovery codes
+ "persist": True, # Persist tokens to session store
+ "audience": "https://api.example.com" # Required when persist=True
+ })
+
+ access_token = verify_response.access_token
+
+ # A new recovery code is generated after using one
+ if verify_response.recovery_code:
+ print(f"New Recovery Code: {verify_response.recovery_code}")
+ print("Save this new recovery code — the old one has been invalidated!")
+
+ print("MFA verification successful using recovery code!")
+ print("Tokens have been persisted to session store")
+
+except Exception as error:
+ print(f"Verification failed: {error}")
+```
+
+### Verify with Push Notification (Polling)
+
+Push notification verification uses polling — the user approves on their device, and you repeatedly call `verify()` until approval or timeout:
+
+```python
+import asyncio
+
+async def poll_push_verification(server_client, mfa_token, oob_code, timeout=60):
+ """Poll for push notification approval."""
+ elapsed = 0
+ interval = 3 # Poll every 3 seconds
+
+ while elapsed < timeout:
+ try:
+ verify_response = await server_client.mfa.verify({
+ "mfa_token": mfa_token,
+ "oob_code": oob_code
+ # No binding_code needed for push notifications
+ })
+
+ # Success — user approved on their device
+ print("Push notification approved!")
+
+ if verify_response.recovery_code:
+ print(f"Recovery Code: {verify_response.recovery_code}")
+
+ return verify_response
+
+ except Exception as error:
+ error_msg = str(error)
+ if "authorization_pending" in error_msg:
+ # User hasn't responded yet — keep polling
+ await asyncio.sleep(interval)
+ elapsed += interval
+ elif "slow_down" in error_msg:
+ # Rate limited — increase interval
+ interval = min(interval + 2, 10)
+ await asyncio.sleep(interval)
+ elapsed += interval
+ else:
+ # Actual error (expired, denied, etc.)
+ raise
+
+ raise TimeoutError("Push notification timed out")
+```
+
+
+
+When polling for push notification approval, the API returns an `authorization_pending` error until the user approves or denies the request. A `slow_down` error indicates you should increase the polling interval.
+
+
+
+### Verify with DPoP (sender-constrained tokens)
+
+When the login that triggered MFA was DPoP-bound (for example a `signin_with_passkey(dpop_key=...)` that returned `MfaRequiredError`), pass the **same** `dpop_key` to `verify()` so the token minted by the MFA step-up stays sender-constrained:
+
+```python
+verify_response = await server_client.mfa.verify(
+ {
+ "mfa_token": mfa_token,
+ "otp": "123456",
+ },
+ dpop_key=dpop_key, # the same EC P-256 key the login was bound to
+)
+
+assert verify_response.token_type == "DPoP"
+```
+
+The SDK does not store your private key, so you must re-supply it on the `verify()` call. It attaches a token-endpoint DPoP proof, transparently handles the server-nonce challenge, and **rejects a Bearer downgrade** — if `dpop_key` was supplied but the server returned an unbound token (or vice versa), `verify()` raises `MfaVerifyError` instead of silently dropping the sender constraint.
+
+
+
+The `dpop_key` is a **Tier 0 secret**. Keep it in your secret store, never log it, use one key per user/session, and use **EC P-256 only**.
+
+
+
+## Session Persistence
+
+By default, `verify()` returns tokens without persisting them to the session store. However, you can automatically persist tokens by setting `persist=True`.
+
+
+
+`persist=True` updates an existing session when one is present. For first login MFA flows where the SDK has not created a session yet, `ServerClient.mfa` can create the initial session from the final MFA token response when that response includes an ID token.
+
+
+
+### Automatic Session Update
+
+When you set `persist=True`, the SDK will:
+1. Update an existing session, or create the initial session when the MFA flow completed a first login
+2. Persist the `access_token` for the specified audience
+3. Persist the `id_token` if present
+4. Add the token to the `token_sets` array with expiration information
+
+```python
+verify_response = await server_client.mfa.verify(
+ {
+ "mfa_token": mfa_token,
+ "otp": "123456",
+ "persist": True, # Enable automatic persistence
+ "audience": "https://api.example.com", # Required when persist=True
+ "scope": "openid profile email" # Optional
+ },
+ store_options={"request": request, "response": response} # Required for session persistence and MCD
+)
+
+# Tokens are now available in the session store
+# User can call server_client.get_user() to access updated session
+user = await server_client.get_user()
+```
+
+### Manual Session Update
+
+If you prefer to manage session updates yourself:
+
+```python
+verify_response = await server_client.mfa.verify(
+ {
+ "mfa_token": mfa_token,
+ "otp": "123456"
+ # persist=False (default)
+ },
+ store_options={"request": request, "response": response} # Optional — required for MCD
+)
+
+# Handle token storage manually if needed
+access_token = verify_response.access_token
+id_token = verify_response.id_token
+
+# Store tokens in your application's session management
+await my_session_store.update_tokens(access_token, id_token)
+```
+
+## Complete MFA Flow Examples
+
+### Enrollment Flow
+
+When a user needs to set up MFA for the first time:
+
+```python
+async def handle_mfa_enrollment_flow(server_client, mfa_token, store_options=None):
+ try:
+ # Get available enrollment factors
+ factors = await server_client.mfa.list_authenticators(
+ {"mfa_token": mfa_token},
+ store_options=store_options
+ )
+
+ print("Available MFA options:")
+ for factor in factors:
+ print(f" - {factor.authenticator_type}")
+
+ # User selects OTP
+ enrollment = await server_client.mfa.enroll_authenticator(
+ {"mfa_token": mfa_token, "factor_type": "otp"},
+ store_options=store_options
+ )
+
+ # Display QR code to user
+ print(f"QR Code: {enrollment.barcode_uri}")
+
+ # Wait for user to scan and enter verification code
+ user_code = input("Enter 6-digit code from authenticator: ")
+
+ # Verify enrollment
+ verify_response = await server_client.mfa.verify(
+ {
+ "mfa_token": mfa_token,
+ "otp": user_code,
+ "persist": True,
+ "audience": "https://api.example.com",
+ "scope": "openid profile email"
+ },
+ store_options=store_options
+ )
+
+ print("MFA enrollment successful!")
+ print("Tokens have been persisted to session store")
+ return verify_response.access_token
+
+ except Exception as error:
+ print(f"Enrollment flow failed: {error}")
+ raise
+```
+
+### Challenge Flow
+
+When a user with existing authenticators needs to verify:
+
+```python
+async def handle_mfa_challenge_flow(server_client, mfa_token, store_options=None):
+ try:
+ # Get user's enrolled authenticators
+ authenticators = await server_client.mfa.list_authenticators(
+ {"mfa_token": mfa_token},
+ store_options=store_options
+ )
+
+ print("Select an authenticator:")
+ for i, auth in enumerate(authenticators):
+ print(f" {i + 1}. {auth.authenticator_type} ({auth.id})")
+
+ # User selects authenticator
+ selected_index = int(input("Selection: ")) - 1
+ selected_auth = authenticators[selected_index]
+
+ # Determine the correct factor_type for challenge
+ # OOB authenticators use their oob_channel as the factor_type
+ if selected_auth.authenticator_type == "oob":
+ factor_type = selected_auth.oob_channel # "sms", "email", "voice", "auth0"
+ else:
+ factor_type = selected_auth.authenticator_type # "otp", "recovery-code"
+
+ # Initiate challenge
+ challenge = await server_client.mfa.challenge_authenticator(
+ {
+ "mfa_token": mfa_token,
+ "factor_type": factor_type,
+ "authenticator_id": selected_auth.id
+ },
+ store_options=store_options
+ )
+
+ # Get verification code from user
+ if selected_auth.authenticator_type == "otp":
+ user_code = input("Enter 6-digit code from authenticator: ")
+ verify_response = await server_client.mfa.verify(
+ {
+ "mfa_token": mfa_token,
+ "otp": user_code,
+ "persist": True,
+ "audience": "https://api.example.com",
+ "scope": "openid profile email"
+ },
+ store_options=store_options
+ )
+ elif factor_type == "auth0":
+ # Push notification — poll for approval
+ verify_response = await poll_push_verification(
+ server_client, mfa_token, challenge.oob_code
+ )
+ else:
+ user_code = input(f"Enter code from {factor_type}: ")
+ verify_response = await server_client.mfa.verify(
+ {
+ "mfa_token": mfa_token,
+ "oob_code": challenge.oob_code,
+ "binding_code": user_code,
+ "persist": True,
+ "audience": "https://api.example.com",
+ "scope": "openid profile email"
+ },
+ store_options=store_options
+ )
+
+ print("MFA verification successful!")
+ print("Tokens have been persisted to session store")
+ return verify_response.access_token
+
+ except Exception as error:
+ print(f"Challenge flow failed: {error}")
+ raise
+```
+
+### Complete Login with MFA
+
+```python
+from auth0_server_python.error import MfaRequiredError
+
+async def login_with_mfa(server_client, store_options=None):
+ try:
+ # Attempt to get access token
+ access_token = await server_client.get_access_token(
+ store_options=store_options
+ )
+ return access_token
+
+ except MfaRequiredError as mfa_error:
+ # Decrypt the encrypted mfa_token from get_access_token()
+ context = server_client.mfa.decrypt_mfa_token(mfa_error.mfa_token)
+ mfa_token = context.mfa_token
+
+ # Determine flow: check if user needs to enroll or has authenticators
+ authenticators = await server_client.mfa.list_authenticators(
+ {"mfa_token": mfa_token},
+ store_options=store_options
+ )
+
+ if len(authenticators) == 0:
+ # User needs to enroll
+ print("MFA enrollment required")
+ access_token = await handle_mfa_enrollment_flow(
+ server_client, mfa_token, store_options=store_options
+ )
+ else:
+ # User has authenticators, proceed with challenge
+ print("MFA verification required")
+ access_token = await handle_mfa_challenge_flow(
+ server_client, mfa_token, store_options=store_options
+ )
+
+ return access_token
+
+ except Exception as error:
+ print(f"Login failed: {error}")
+ raise
+```
+
+## Error Handling
+
+Each MFA operation has specific error handling:
+
+```python
+from auth0_server_python.error import MfaRequiredError, ApiError
+
+async def handle_mfa_with_error_handling(server_client, store_options=None):
+ try:
+ # Attempt token exchange
+ access_token = await server_client.get_access_token(
+ store_options=store_options
+ )
+
+ except MfaRequiredError as error:
+ print(f"MFA Required: {error.error_description}")
+
+ # Decrypt the encrypted mfa_token from get_access_token()
+ context = server_client.mfa.decrypt_mfa_token(error.mfa_token)
+ mfa_token = context.mfa_token
+
+ try:
+ # Get authenticators
+ authenticators = await server_client.mfa.list_authenticators(
+ {"mfa_token": mfa_token},
+ store_options=store_options
+ )
+ except ApiError as list_error:
+ print(f"Failed to retrieve authenticators: {list_error}")
+ raise
+
+ try:
+ # Initiate challenge
+ challenge = await server_client.mfa.challenge_authenticator(
+ {
+ "mfa_token": mfa_token,
+ "factor_type": "sms",
+ "authenticator_id": authenticators[0].id
+ },
+ store_options=store_options
+ )
+ except ApiError as challenge_error:
+ print(f"Challenge failed: {challenge_error}")
+ raise
+
+ try:
+ # Verify challenge
+ verify_response = await server_client.mfa.verify(
+ {
+ "mfa_token": mfa_token,
+ "oob_code": challenge.oob_code,
+ "binding_code": "123456"
+ },
+ store_options=store_options
+ )
+ except ApiError as verify_error:
+ if verify_error.status_code == 403:
+ print("Invalid code or challenge expired")
+ else:
+ print(f"Verification error: {verify_error}")
+ raise
+
+ except Exception as error:
+ print(f"Unexpected error: {error}")
+ raise
+```
+
+### Common Error Scenarios
+
+- **Invalid OTP Code**: HTTP 403 with error details
+- **Expired Challenge**: HTTP 403 with expired_token error
+- **MFA Token Expired**: HTTP 400 with context_not_found error
+- **Network Issues**: Connection errors with descriptive messages
+
+## Additional Resources
+
+- [Auth0 MFA Documentation](https://auth0.com/docs/secure/multi-factor-authentication)
diff --git a/docs/dist/examples/MultipleCustomDomains.mdx b/docs/dist/examples/MultipleCustomDomains.mdx
new file mode 100644
index 00000000..a339d8d9
--- /dev/null
+++ b/docs/dist/examples/MultipleCustomDomains.mdx
@@ -0,0 +1,423 @@
+---
+title: "Multiple Custom Domains (MCD)"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+MCD lets you resolve the Auth0 domain per request while keeping a single `ServerClient` instance. This is useful when your application uses multiple custom domains configured on the same Auth0 tenant.
+
+> **Important:** MCD supports multiple custom domains on a **single Auth0 tenant**. It does not support connecting to multiple Auth0 tenants from a single application. Each custom domain must belong to the same Auth0 tenant. Using domains from different Auth0 tenants is not supported and will result in authentication failures.
+
+**Example:**
+- `https://brand-1.yourapp.com` → Custom domain: `login.brand-1.com`
+- `https://brand-2.yourapp.com` → Custom domain: `login.brand-2.com`
+
+MCD is enabled by providing a **domain resolver function** instead of a static domain string.
+
+See [Security Best Practices](#security-best-practices) for important guidance on configuring your resolver safely.
+
+## Configuration Methods
+
+### Method 1: Static Domain (Single Domain)
+
+For applications with a single Auth0 domain:
+
+```python
+from auth0_server_python.auth_server import ServerClient
+
+client = ServerClient(
+ domain="login.yourapp.com", # Static string
+ client_id="your_client_id",
+ client_secret="your_client_secret",
+ secret="your_encryption_secret"
+)
+```
+
+`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+### Method 2: Dynamic Domain Resolver (MCD)
+
+For MCD support, provide a domain resolver function that receives a `DomainResolverContext`:
+
+```python
+from auth0_server_python.auth_server import ServerClient
+from auth0_server_python.auth_types import DomainResolverContext
+
+# Map your app hostnames to Auth0 custom domains
+DOMAIN_MAP = {
+ "brand-1.yourapp.com": "login.brand-1.com",
+ "brand-2.yourapp.com": "login.brand-2.com",
+}
+DEFAULT_DOMAIN = "login.yourapp.com"
+
+async def domain_resolver(context: DomainResolverContext) -> str:
+ """
+ Resolve Auth0 domain based on request hostname.
+
+ Args:
+ context: Contains request_url and request_headers
+
+ Returns:
+ Auth0 domain string (e.g., "login.brand-1.com")
+ """
+ # Extract hostname from request headers
+ if not context.request_headers:
+ return DEFAULT_DOMAIN
+
+ host = context.request_headers.get('host', DEFAULT_DOMAIN)
+ host_without_port = host.split(':')[0]
+
+ # Look up Auth0 domain
+ return DOMAIN_MAP.get(host_without_port, DEFAULT_DOMAIN)
+
+client = ServerClient(
+ domain=domain_resolver, # Callable function
+ client_id="your_client_id",
+ client_secret="your_client_secret",
+ secret="your_encryption_secret"
+)
+```
+
+## DomainResolverContext
+
+The `DomainResolverContext` object provides request information to your resolver:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `request_url` | `Optional[str]` | Full request URL (e.g., "https://brand-1.yourapp.com/auth/login") |
+| `request_headers` | `Optional[dict[str, str]]` | Request headers dictionary |
+
+**Common headers:**
+- `host`: Request hostname (e.g., "brand-1.yourapp.com")
+- `x-forwarded-host`: Original host when behind proxy/load balancer
+
+**Example usage:**
+
+```python
+async def domain_resolver(context: DomainResolverContext) -> str:
+ # Check if we have request headers
+ if not context.request_headers:
+ return DEFAULT_DOMAIN
+
+ # Use x-forwarded-host if behind proxy, otherwise use host
+ host = (context.request_headers.get('x-forwarded-host') or
+ context.request_headers.get('host', ''))
+
+ # Remove port number if present
+ hostname = host.split(':')[0].lower()
+
+ # Look up in mapping
+ return DOMAIN_MAP.get(hostname, DEFAULT_DOMAIN)
+```
+
+## Passing store_options
+
+In resolver mode, pass `store_options` to each SDK call so the resolver can inspect the
+current request and select the correct domain. If `store_options` are omitted, the resolver
+receives empty context (`request_url=None`, `request_headers=None`).
+
+All public SDK methods that interact with sessions or Auth0 endpoints accept `store_options`.
+Here is an example using `get_user()`:
+
+```python
+# In your route handler, pass the framework request via store_options
+store_options = {"request": request, "response": response}
+
+# The SDK calls your domain_resolver with a DomainResolverContext
+# built from the request in store_options
+user = await client.get_user(store_options=store_options)
+```
+
+The same pattern applies to `get_session()`, `get_access_token()`, `start_interactive_login()`,
+`logout()`, and all other session-aware methods.
+
+## Redirect URI Requirements
+
+In resolver mode, the SDK does not infer `redirect_uri` from the request. You must provide it
+explicitly:
+
+- Set a default `redirect_uri` when constructing `ServerClient`, or
+- Pass `redirect_uri` in `authorization_params` for each login call.
+
+Framework wrappers like `auth0-fastapi` handle this automatically by constructing the
+`redirect_uri` from the incoming request's host and scheme.
+
+> **Note:** In resolver mode, MCD needs an ID token in the callback so the SDK can validate
+> the `iss` claim. The `openid` scope is required to receive an ID token. Ensure `openid` is
+> included in your `authorization_params.scope`.
+
+## Resolver Patterns
+
+### Database Lookup (SQLAlchemy)
+
+Resolve domains from a database using async SQLAlchemy:
+
+```python
+from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
+from sqlalchemy import text
+
+engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
+
+async def domain_resolver(context: DomainResolverContext) -> str:
+ host = context.request_headers.get("host", "").split(":")[0]
+ subdomain = host.split(".")[0]
+
+ async with AsyncSession(engine) as session:
+ result = await session.execute(
+ text("SELECT auth0_domain FROM tenants WHERE slug = :slug"),
+ {"slug": subdomain}
+ )
+ row = result.fetchone()
+ if row:
+ return row[0]
+
+ return DEFAULT_DOMAIN
+```
+
+### Database Lookup with In-Memory Cache
+
+Avoid hitting the database on every request by caching the domain map:
+
+```python
+import time
+
+_domain_cache = {}
+_cache_ttl = 300 # 5 minutes
+
+async def domain_resolver(context: DomainResolverContext) -> str:
+ host = context.request_headers.get("host", "").split(":")[0]
+ subdomain = host.split(".")[0]
+
+ now = time.time()
+ cached = _domain_cache.get(subdomain)
+ if cached and cached["expires_at"] > now:
+ return cached["domain"]
+
+ # Cache miss - fetch from database
+ async with AsyncSession(engine) as session:
+ result = await session.execute(
+ text("SELECT auth0_domain FROM tenants WHERE slug = :slug"),
+ {"slug": subdomain}
+ )
+ row = result.fetchone()
+ domain = row[0] if row else DEFAULT_DOMAIN
+
+ _domain_cache[subdomain] = {"domain": domain, "expires_at": now + _cache_ttl}
+ return domain
+```
+
+### Redis Lookup
+
+Use Redis for shared domain configuration across multiple app instances:
+
+```python
+import redis.asyncio as redis
+
+redis_client = redis.Redis(host="localhost", port=6379, decode_responses=True)
+
+async def domain_resolver(context: DomainResolverContext) -> str:
+ host = context.request_headers.get("host", "").split(":")[0]
+ subdomain = host.split(".")[0]
+
+ # Key format: "tenant:brand-1" -> "login.brand-1.com"
+ domain = await redis_client.get(f"tenant:{subdomain}")
+ if domain:
+ return domain
+
+ return DEFAULT_DOMAIN
+```
+
+### Redis with Hash Map
+
+Store all domain mappings in a single Redis hash:
+
+```python
+async def domain_resolver(context: DomainResolverContext) -> str:
+ host = context.request_headers.get("host", "").split(":")[0]
+ subdomain = host.split(".")[0]
+
+ # All domains in one hash: HGET tenant_domains brand-1 -> "login.brand-1.com"
+ domain = await redis_client.hget("tenant_domains", subdomain)
+ if domain:
+ return domain
+
+ return DEFAULT_DOMAIN
+```
+
+### Path-Based Resolution
+
+Resolve from URL path instead of hostname:
+
+```python
+from urllib.parse import urlparse
+
+async def domain_resolver(context: DomainResolverContext) -> str:
+ if context.request_url:
+ path = urlparse(context.request_url).path
+ # URL pattern: /brand/brand-1/auth/login
+ parts = path.strip("/").split("/")
+ if len(parts) >= 2 and parts[0] == "brand":
+ name = parts[1]
+ return DOMAIN_MAP.get(name, DEFAULT_DOMAIN)
+
+ return DEFAULT_DOMAIN
+```
+
+### Custom Header Resolution
+
+Use a custom header set by your API gateway or load balancer:
+
+```python
+async def domain_resolver(context: DomainResolverContext) -> str:
+ headers = context.request_headers or {}
+
+ # API gateway sets X-Brand-Id header
+ brand_id = headers.get("x-brand-id")
+ if brand_id:
+ return DOMAIN_MAP.get(brand_id, DEFAULT_DOMAIN)
+
+ # Fallback to host header
+ host = headers.get("host", "").split(":")[0]
+ return DOMAIN_MAP.get(host, DEFAULT_DOMAIN)
+```
+
+## Error Handling
+
+### DomainResolverError
+
+The domain resolver should return a valid Auth0 domain string. Invalid returns will raise `DomainResolverError`:
+
+```python
+from auth0_server_python.error import DomainResolverError
+
+async def domain_resolver(context: DomainResolverContext) -> str:
+ try:
+ domain = lookup_domain_from_db(context)
+
+ if not domain:
+ # Return default instead of None
+ return DEFAULT_DOMAIN
+
+ return domain # Must be a non-empty string
+
+ except Exception as e:
+ # Log error and return default
+ logger.error(f"Domain resolution failed: {e}")
+ return DEFAULT_DOMAIN
+```
+
+**Invalid return values that raise `DomainResolverError`:**
+- `None`
+- Empty string `""`
+- Non-string types (int, list, dict, etc.)
+
+**Exceptions raised by your resolver:**
+- Automatically wrapped in `DomainResolverError`
+- Original exception accessible via `.original_error`
+
+## Session Behavior in Resolver Mode
+
+In resolver mode, sessions are bound to the domain that created them. On each request, the SDK compares the session's stored domain against the current resolved domain. If the domains do not match:
+
+- `get_user()` and `get_session()` return `None`.
+- `get_access_token()` raises `AccessTokenError` (code `MISSING_SESSION_DOMAIN` or `DOMAIN_MISMATCH`).
+- `get_access_token_for_connection()` raises `AccessTokenForConnectionError` (same codes as above).
+- `start_link_user()` and `start_unlink_user()` raise `StartLinkUserError`.
+- Token refresh uses the session's stored domain, not the current request domain.
+
+All domain mismatch errors use the message: **"Session domain does not match the current domain."**
+
+> **Note:** If a login was started before the switch to resolver mode and completes after, the SDK falls back to the current resolved domain for token exchange. The resulting session will store the resolved domain and work normally going forward.
+
+## Legacy Sessions and Migration
+
+When moving from a static domain setup to resolver mode, existing sessions can continue
+to work if the resolver returns the same Auth0 domain that was used for those legacy sessions.
+
+The SDK uses a three-tier fallback to determine the session's domain:
+
+1. **`session.domain`** — new sessions created after MCD was enabled store this field.
+2. **Static domain** — if a static `domain` string was configured, it is used as a fallback.
+3. **User's issuer claim** — the hostname is extracted from the `iss` claim in the user's
+ ID token (e.g., `https://login.brand-1.com/` yields `login.brand-1.com`).
+
+This means legacy sessions created before MCD support will still work as long as the
+resolver returns a domain that matches one of the fallback values. In most cases, the
+issuer claim already matches the Auth0 domain, so no re-authentication is needed.
+
+If the resolver returns a different domain that does not match any tier, the SDK treats
+the session as belonging to another domain and the user will need to sign in again. This
+is intentional to keep sessions isolated per domain.
+
+## Discovery Cache
+
+The SDK caches OIDC metadata and JWKS per domain in memory (LRU eviction, 600-second TTL, up to 100 domains). This avoids repeated network calls when serving multiple domains. The cache is shared across all requests to the same `ServerClient` instance.
+
+Most applications can keep the defaults, but you may want to adjust in these cases:
+- Increase `max_entries` if one process handles more than 100 distinct Auth0 domains during the TTL window. This is most common in MCD deployments that work with many custom domains.
+- Decrease `max_entries` if memory usage matters more than avoiding repeated discovery.
+- Increase TTL if the same domains are reused frequently and you want to reduce repeated discovery and JWKS fetches after cache entries expire.
+- Decrease TTL if you want the SDK to pick up Auth0 metadata or signing key changes sooner.
+
+Rule of thumb: set `max_entries` to cover the number of distinct Auth0 domains a single process is expected to use during the TTL window, with some headroom.
+
+## Security Best Practices
+
+> **The domain resolver is a security-critical component.** A misconfigured resolver can lead to authentication bypass on the relying party (RP) or expose the application to Server-Side Request Forgery (SSRF). The SDK trusts the resolved domain to fetch OIDC metadata and verification keys. It is the customer's responsibility to ensure the resolver cannot be influenced by untrusted input.
+
+**Single Tenant Limitation:**
+The domain resolver is intended solely for multiple custom domains belonging to the same Auth0 tenant. It is not a supported mechanism for connecting multiple Auth0 tenants to a single application.
+
+### Use an Allowlist in Your Resolver
+
+The SDK passes request headers to your domain resolver via `DomainResolverContext`. These headers come directly from the HTTP request and can be spoofed by an attacker (e.g., `Host: evil.com` or `X-Forwarded-Host: evil.com`).
+
+The SDK uses the resolved domain to fetch OIDC metadata and JWKS. If an attacker can influence the resolved domain, they could point the SDK at an OIDC provider they control.
+
+**Always use a mapping or allowlist — never construct domains from raw header values:**
+
+```python
+# Safe: allowlist lookup — unknown hosts fall back to default
+DOMAIN_MAP = {
+ "brand-1.yourapp.com": "login.brand-1.com",
+ "brand-2.yourapp.com": "login.brand-2.com",
+}
+
+async def domain_resolver(context: DomainResolverContext) -> str:
+ host = context.request_headers.get("host", "").split(":")[0]
+ return DOMAIN_MAP.get(host, DEFAULT_DOMAIN)
+```
+
+```python
+# Risky: constructs domain from raw input — attacker can influence resolved domain
+async def domain_resolver(context: DomainResolverContext) -> str:
+ host = context.request_headers.get("host", "").split(":")[0]
+ subdomain = host.split(".")[0]
+ return f"login.{subdomain}.com" # attacker sends Host: evil.yourapp.com → login.evil.com
+```
+
+### Secure Proxy Requirement
+
+When using Multiple Custom Domains (MCD), your application must be deployed behind a secure reverse proxy (e.g., Cloudflare, Nginx, or AWS ALB). The proxy must be configured to sanitize and overwrite `Host` and `X-Forwarded-Host` headers before they reach your application.
+
+Without a trusted proxy layer to validate these headers, an attacker can manipulate the domain resolution process. This can result in authentication bypass or Server-Side Request Forgery (SSRF).
+
+### Trust Forwarded Headers Only Behind a Proxy
+
+If your application is directly exposed to the internet (not behind a reverse proxy), do not trust `x-forwarded-host` or `x-forwarded-proto` — any client can set these headers.
+
+Only use forwarded headers when your application runs behind a trusted reverse proxy (nginx, AWS ALB, Cloudflare, etc.) that sets these headers and strips any client-provided values.
+
+```python
+# Only trust x-forwarded-host if behind a trusted proxy
+async def domain_resolver(context: DomainResolverContext) -> str:
+ headers = context.request_headers or {}
+
+ if BEHIND_TRUSTED_PROXY:
+ host = headers.get("x-forwarded-host") or headers.get("host", "")
+ else:
+ host = headers.get("host", "")
+
+ host = host.split(":")[0]
+ return DOMAIN_MAP.get(host, DEFAULT_DOMAIN)
+```
diff --git a/docs/dist/examples/MutualTLS.mdx b/docs/dist/examples/MutualTLS.mdx
new file mode 100644
index 00000000..80a9b98b
--- /dev/null
+++ b/docs/dist/examples/MutualTLS.mdx
@@ -0,0 +1,92 @@
+---
+title: "Mutual TLS (mTLS) Client Authentication"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Authenticate to Auth0 with a TLS client certificate instead of a client secret (RFC 8705). The certificate is presented during the TLS handshake. No credential travels in the request body.
+
+## Prerequisites
+
+- Auth0 **Enterprise** tenant with the **Highly Regulated Identity** add-on
+- A `self_managed_certs` **custom domain** configured on the tenant
+- **Allow mTLS Endpoint Aliases** enabled on the tenant (Dashboard → Settings → Advanced)
+- Client application's authentication method set to **mTLS** in Dashboard → Applications → Settings → Credentials
+
+## Generating a client certificate (development)
+
+```bash
+# Self-signed CA + client cert (development only - use your PKI in production)
+openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.crt -days 365 -nodes \
+ -subj "/CN=dev-ca"
+openssl req -newkey rsa:2048 -keyout client.key -out client.csr -nodes \
+ -subj "/CN=my-app-client"
+openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
+ -out client.crt -days 365
+```
+
+## Wiring into `ServerClient`
+
+```python
+import ssl
+from auth0_server_python.auth_server.server_client import ServerClient
+
+ssl_context = ssl.create_default_context() # trusts system/public CAs for the server side
+ssl_context.load_cert_chain("client.crt", "client.key") # attaches the client identity
+
+auth0 = ServerClient(
+ domain="login.example.com", # self_managed_certs custom domain
+ client_id="",
+ use_mtls=True,
+ ssl_context=ssl_context,
+ secret="",
+ authorization_params={
+ "audience": "",
+ "scope": "openid profile email offline_access",
+ },
+)
+```
+
+`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+The SDK passes `ssl_context` as `verify=ssl_context` to every `httpx.AsyncClient` it constructs, including the authlib client used for the authorization-code exchange. You never call `load_cert_chain` inside the SDK - the caller owns the TLS material.
+
+## Mutual exclusion
+
+`use_mtls=True` cannot be combined with:
+
+| Parameter | Reason |
+|-----------|--------|
+| `client_secret` | One client-auth method only - Auth0 rejects requests carrying both. |
+| `client_assertion_signing_key` | Same - one method only. |
+| `dpop_key` (per-call on `signin_with_passkey` / `mfa.verify`) | DPoP binds to its own key (`cnf.jkt`) and suppresses `cnf.x5t#S256`. Combining them silently defeats mTLS token binding. |
+
+All three raise `ConfigurationError` immediately (constructor for the first two, at the call site for DPoP).
+
+## Token sender-constraining
+
+When the target API has **Token Sender-Constraining (mTLS)** enabled, issued access tokens carry a `cnf.x5t#S256` claim binding the token to the certificate thumbprint.
+
+The SDK logs a warning at the `auth0_server_python.auth_server.server_client` logger whenever an access token returned by the authorization-code or refresh-token flow does not contain `cnf.x5t#S256`. If you see that warning, enable **Token Sender-Constraining (mTLS)** on the API resource server in the Auth0 Dashboard.
+
+## MFA under mTLS
+
+The client certificate is presented on all MFA API calls. The token-endpoint call inside `mfa.verify` is routed through the mTLS alias automatically. Challenge and enrollment calls (`/mfa/challenge`, `/mfa/associate`) go to the standard host. The certificate is still included in the TLS handshake, but whether it reaches the Auth0 backend depends on the proxy configuration.
+
+```python
+await auth0.mfa.verify(
+ {"mfa_token": encrypted_token, "otp": "123456"},
+)
+```
+
+## Passkeys under mTLS
+
+The client certificate is presented on all passkey calls. The token-exchange step (`signin_with_passkey`) calls the token endpoint, which is served on the mTLS alias and routed correctly.
+
+Challenge and enrollment calls (`/passkey/challenge`, `/passkey/register`) go to the standard host - they are not listed in `mtls_endpoint_aliases`. The certificate is still included in the TLS handshake, but whether it reaches the Auth0 backend depends on the proxy configuration - the same behaviour as `/mfa/challenge`.
+
+## Passwordless under mTLS
+
+By default, Auth0 does not require client authentication on `/passwordless/start`. If the `enforce_client_authentication_on_passwordless_start` tenant flag is enabled on your tenant, the call will fail because `/passwordless/start` does not support certificate-based client authentication.
+
+The verify step (`passwordless_client.verify`) calls the token endpoint, which is served on the mTLS alias and routed correctly.
diff --git a/docs/dist/examples/MyAccountAuthenticationMethods.mdx b/docs/dist/examples/MyAccountAuthenticationMethods.mdx
new file mode 100644
index 00000000..fbcd4a00
--- /dev/null
+++ b/docs/dist/examples/MyAccountAuthenticationMethods.mdx
@@ -0,0 +1,269 @@
+---
+title: "My Account API — Authentication Methods & Factors"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+The [My Account API](https://auth0.com/docs/manage-users/my-account-api) lets a **logged-in user manage their own account**. This guide covers the **authentication-methods** and **factors** surface: enrolling a new passkey (or other factor), and listing, reading, renaming, and deleting a user's enrolled methods.
+
+
+
+This is a different My Account resource from [Connected Accounts](/docs/sdk/python/server/examples/ConnectedAccounts) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConnectedAccounts.md)) (Token Vault). Connected-accounts management is exposed as convenience methods on `ServerClient`; **authentication-method management is on `MyAccountClient` directly**, because each call takes a user access token you obtain yourself. The two share the same My Account setup (activation, MRRT, scopes, `MyAccountApiError`) — see [ConnectedAccounts.md → Pre-requisites](/docs/sdk/python/server/examples/ConnectedAccounts#pre-requisites) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConnectedAccounts.md#pre-requisites)) for that common setup.
+
+
+
+
+
+To **sign in** with a passkey (rather than manage one), see [examples/Passkeys.md](/docs/sdk/python/server/examples/Passkeys) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/Passkeys.md)). To **bind these calls to a held key**, pass an optional `dpop_key` — see [DPoP](#dpop) below.
+
+
+
+## Table of Contents
+
+- [Prerequisites](#prerequisites)
+- [Obtaining a scoped token](#obtaining-a-scoped-token)
+- [1. List factors available for enrollment](#1-list-factors-available-for-enrollment)
+- [2. Enroll an authentication method (passkey)](#2-enroll-an-authentication-method-passkey)
+- [3. List authentication methods](#3-list-authentication-methods)
+- [4. Get a single authentication method](#4-get-a-single-authentication-method)
+- [5. Update (rename) an authentication method](#5-update-rename-an-authentication-method)
+- [6. Delete an authentication method](#6-delete-an-authentication-method)
+- [DPoP](#dpop)
+- [Error Handling](#error-handling)
+
+## Prerequisites
+
+1. [Activate the My Account API](https://auth0.com/docs/manage-users/my-account-api#activate-the-my-account-api) on your tenant and enable access for your application.
+2. [Configure MRRT](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token) so your refresh-token policy can mint tokens for the My Account audience (`https://{yourDomain}/me/`) with the authentication-methods scopes.
+3. Passkey enrollment additionally requires a [Custom Domain](https://auth0.com/docs/customize/custom-domains) and the native passkey feature on your tenant.
+
+See [Auth0 Docs → Scope](https://auth0.com/docs/manage-users/my-account-api#scope) for the full scope reference. The scopes for this surface (note the **hyphens**):
+
+| Operation | Scope |
+|-----------|-------|
+| List factors | `read:me:factors` |
+| List / get methods | `read:me:authentication-methods` |
+| Enroll / verify | `create:me:authentication-methods` |
+| Update | `update:me:authentication-methods` |
+| Delete | `delete:me:authentication-methods` |
+
+
+
+As with Connected Accounts, set the default `scope` for the My Account audience when constructing `ServerClient` to avoid a fresh token request per scope. See [ConnectedAccounts.md → A note about scopes](/docs/sdk/python/server/examples/ConnectedAccounts#a-note-about-scopes) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConnectedAccounts.md#a-note-about-scopes)).
+
+
+
+## Obtaining a scoped token
+
+`MyAccountClient` is **stateless** — it takes a correctly-scoped user access token on every call. Obtain that token from your `ServerClient` session via MRRT, then construct the client. See [Auth0 Docs → Get an access token](https://auth0.com/docs/manage-users/my-account-api#get-an-access-token) for the underlying token requirements.
+
+```python
+from auth0_server_python.auth_server.my_account_client import MyAccountClient
+
+# Fresh My Account-scoped token for the current session (MRRT exchange)
+access_token = await server_client.get_access_token(
+ store_options={"request": request, "response": response},
+ audience=f"https://{YOUR_CUSTOM_DOMAIN}/me/",
+ scope="create:me:authentication-methods read:me:authentication-methods read:me:factors",
+)
+
+my_account = MyAccountClient(domain=YOUR_CUSTOM_DOMAIN)
+```
+
+## 1. List factors available for enrollment
+
+```python
+factors = await my_account.get_factors(access_token=access_token)
+for factor in factors.factors:
+ print(factor.type, factor.usage)
+```
+
+## 2. Enroll an authentication method (passkey)
+
+Enrollment is a **two-step** ceremony, mirroring sign-in: request a challenge, sign it in the browser, then verify. See [Auth0 Docs → Enrollment flow](https://auth0.com/docs/manage-users/my-account-api#enrollment-flow) (and, for passkeys specifically, [Embedded login with native passkeys](https://auth0.com/docs/manage-users/my-account-api#embedded-login-with-native-passkeys)) for the platform-level description of this ceremony.
+
+### Step 1 — Start enrollment
+
+```python
+from auth0_server_python.auth_types import EnrollAuthenticationMethodRequest
+
+challenge = await my_account.enroll_authentication_method(
+ access_token=access_token,
+ request=EnrollAuthenticationMethodRequest(type="passkey"),
+)
+
+# challenge.authentication_method_id -> id of the new (unverified) method
+# challenge.auth_session -> Tier 1 session credential (do not log)
+# challenge.authn_params_public_key -> pass to navigator.credentials.create()
+```
+
+`EnrollAuthenticationMethodRequest.type` is a closed set: `passkey`, `email`, `phone`, `totp`, `push-notification`, `recovery-code`, `password`. For non-passkey types, supply the relevant fields (`email`, `phone_number`, `preferred_authentication_method`). An invalid type fails at construction with a clear `ValidationError`.
+
+### Step 2 — Create the credential in the browser
+
+Pass `challenge.authn_params_public_key` to `navigator.credentials.create()` and collect the resulting credential.
+
+### Step 3 — Verify enrollment
+
+```python
+from auth0_server_python.auth_types import (
+ VerifyAuthenticationMethodRequest,
+ PasskeyAuthResponse,
+)
+
+method = await my_account.verify_authentication_method(
+ access_token=access_token,
+ authentication_method_id=challenge.authentication_method_id,
+ request=VerifyAuthenticationMethodRequest(
+ auth_session=challenge.auth_session,
+ authn_response=PasskeyAuthResponse(
+ id=credential["id"],
+ raw_id=credential["rawId"],
+ type="public-key",
+ response={
+ "clientDataJSON": credential["response"]["clientDataJSON"],
+ "attestationObject": credential["response"]["attestationObject"],
+ },
+ ),
+ ),
+)
+print(f"Enrolled: {method.id} ({method.type})")
+```
+
+
+
+For non-passkey types, set the matching field on `VerifyAuthenticationMethodRequest` instead of `authn_response`: `otp_code` (email/phone/totp), `recovery_code`, or `password`. A push enrollment needs only `auth_session`.
+
+
+
+## 3. List authentication methods
+
+See [Auth0 Docs → List authentication methods](https://auth0.com/docs/manage-users/my-account-api#list-authentication-methods).
+
+```python
+all_methods = await my_account.list_authentication_methods(access_token=access_token)
+
+# Filter by type
+passkeys = await my_account.list_authentication_methods(
+ access_token=access_token,
+ type_filter="passkey",
+)
+for m in passkeys.authentication_methods:
+ print(m.id, m.type, m.created_at)
+```
+
+
+
+`AuthenticationMethod` and `Factor` are forward-tolerant (`extra="allow"`): fields or method/factor types Auth0 adds later still deserialize. Don't switch exhaustively on `type` — handle unknown types gracefully.
+
+
+
+## 4. Get a single authentication method
+
+```python
+method = await my_account.get_authentication_method(
+ access_token=access_token,
+ authentication_method_id="passkey|abc123",
+)
+```
+
+
+
+Method IDs (e.g. `passkey|abc123`) can contain characters like `|`. The SDK URL-encodes every ID it places in a path, so pass the raw ID exactly as returned — do not pre-encode it.
+
+
+
+## 5. Update (rename) an authentication method
+
+```python
+from auth0_server_python.auth_types import UpdateAuthenticationMethodRequest
+
+method = await my_account.update_authentication_method(
+ access_token=access_token,
+ authentication_method_id="totp|123",
+ request=UpdateAuthenticationMethodRequest(name="My Authenticator App"),
+)
+```
+
+## 6. Delete an authentication method
+
+See [Auth0 Docs → Delete an authentication method](https://auth0.com/docs/manage-users/my-account-api#delete-an-authentication-method).
+
+```python
+await my_account.delete_authentication_method(
+ access_token=access_token,
+ authentication_method_id="passkey|abc123",
+)
+# Returns None on success (HTTP 204).
+```
+
+## DPoP
+
+Every method above accepts an optional `dpop_key` to present a sender-constrained token (`Authorization: DPoP` + a per-request proof) instead of a Bearer token ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)). Pass the **same key** the access token was bound to at sign-in:
+
+```python
+from jwcrypto import jwk
+
+dpop_key = jwk.JWK.generate(kty="EC", crv="P-256") # the key the token was bound to
+
+methods = await my_account.list_authentication_methods(
+ access_token=access_token,
+ dpop_key=dpop_key,
+)
+```
+
+
+
+The `dpop_key` private key is a **Tier 0 secret**. Keep it in your secret store (KMS/HSM), never log it (`repr()` is redacted, but `key.export_private()` is not), use **one key per user/session** (never share across principals), and use **EC P-256 only** — any other key type fails closed with a `ValueError`.
+
+
+
+## Error Handling
+
+All errors inherit from `Auth0Error`. My Account API errors are `MyAccountApiError` (RFC 7807 problem-details, carrying `status`, `detail`, and optional `validation_errors`); missing arguments raise `MissingRequiredArgumentError`; transport or non-JSON responses surface as `ApiError`.
+
+### Basic handling (recommended)
+
+```python
+from auth0_server_python.error import Auth0Error
+
+try:
+ methods = await my_account.list_authentication_methods(access_token=access_token)
+except Auth0Error as e:
+ return {"error": str(e)}
+```
+
+### Advanced handling (when actions differ by case)
+
+```python
+from auth0_server_python.error import Auth0Error, MyAccountApiError
+
+try:
+ await my_account.enroll_authentication_method(
+ access_token=access_token,
+ request=EnrollAuthenticationMethodRequest(type="passkey"),
+ )
+except MyAccountApiError as e:
+ if e.status == 401:
+ return redirect_to_login() # token expired
+ if e.status == 403:
+ return {"error": "Missing required scope"} # e.g. create:me:authentication-methods
+ if e.status == 400 and e.validation_errors:
+ return {"error": "Validation failed", "details": e.validation_errors}
+ raise
+except Auth0Error as e:
+ return {"error": str(e)}
+```
+
+
+
+Enrollment raises `MyAccountApiError`/`ApiError`, whereas passkey **sign-in** (`ServerClient`) raises `PasskeyError`. They are two distinct API surfaces — an auth grant versus a My Account resource — so write the `except` that matches the call you made.
+
+
+
+### Common error types
+
+- **`Auth0Error`** (base): catch for general handling
+- **`MyAccountApiError`**: My Account API errors with `status`, `detail`, optional `validation_errors`
+- **`MissingRequiredArgumentError`**: a required parameter (`access_token`, `authentication_method_id`, `request`) was not provided
+- **`ApiError`**: transport failure or a non-JSON error body
diff --git a/docs/dist/examples/OrganizationLogin.mdx b/docs/dist/examples/OrganizationLogin.mdx
new file mode 100644
index 00000000..a34e21be
--- /dev/null
+++ b/docs/dist/examples/OrganizationLogin.mdx
@@ -0,0 +1,231 @@
+---
+title: "Organization Login"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+[Auth0 Organizations](https://auth0.com/docs/organizations) lets you manage business customers and partner companies as distinct entities with their own membership, branding, and login policies.
+
+`auth0-server-python` supports two patterns:
+
+- **Dedicated-org** - set `organization` on `ServerClient` once; every login is scoped to that org.
+- **Multi-org** - pass `organization` per login call; one client serves many tenants.
+
+Both patterns go through the same `start_interactive_login()` / `complete_interactive_login()` pair from [InteractiveLogin.md](/docs/sdk/python/server/examples/InteractiveLogin) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/InteractiveLogin.md)). The SDK validates the `org_id` or `org_name` claim in the returned token automatically at callback.
+
+For tenant-level setup (enabling Organizations, configuring connections, and setting display name) see [Auth0 Organizations docs](https://auth0.com/docs/organizations).
+
+## Dedicated-Org Login
+
+Set `organization` on `ServerClient` to scope every login to a single org:
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+
+auth0 = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ organization="org_abc123",
+ authorization_params={"redirect_uri": "http://localhost:3000/auth/callback"},
+ # ... your configured transaction_store and state_store
+)
+
+# `organization` is applied automatically to every start_interactive_login call.
+authorization_url = await auth0.start_interactive_login(
+ store_options={"request": request, "response": response}
+)
+```
+
+`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+`organization` accepts either an org ID (with the `org_` prefix) or a human-readable org name. The SDK validates the corresponding `org_id` or `org_name` claim in the returned ID token.
+
+## Multi-Org Login
+
+Pass `organization` per login call when a single app serves multiple orgs. The value comes from your own routing logic (subdomain, path segment, user selection) - never pass it unvalidated from user-controlled input:
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+from auth0_server_python.auth_types import StartInteractiveLoginOptions
+
+auth0 = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ authorization_params={"redirect_uri": "http://localhost:3000/auth/callback"},
+ # ... your configured transaction_store and state_store
+)
+
+# Resolve the org from your own routing (subdomain, path, user selection, etc.)
+org_id = resolve_org_for_request(request) # your own logic; never trust raw user input
+
+authorization_url = await auth0.start_interactive_login(
+ StartInteractiveLoginOptions(organization=org_id),
+ store_options={"request": request, "response": response},
+)
+```
+
+
+
+Validate that the `organization` value comes from a trusted source - a lookup in your own data store, a resolved subdomain, or a server-side mapping. Never pass a raw query parameter or form value directly.
+
+
+
+## Accepting an Invitation
+
+When a user follows an org invitation link, Auth0 appends `organization` and `invitation` to the URL. Extract both and pass them as typed fields on `StartInteractiveLoginOptions`:
+
+```python
+from auth0_server_python.auth_types import StartInteractiveLoginOptions
+
+# `organization` and `invitation` arrive as query parameters on the invitation link.
+authorization_url = await auth0.start_interactive_login(
+ StartInteractiveLoginOptions(
+ organization=request.query_params.get("organization"),
+ invitation=request.query_params.get("invitation"),
+ ),
+ store_options={"request": request, "response": response},
+)
+```
+
+The `invitation` parameter is consumed by Auth0 during the login and is not present in the session after callback.
+
+
+
+The `organization` parameter on an invitation link is set by Auth0 and refers to a specific org ID - it is safe to pass it through here. However, do not allow users to supply `organization` or `invitation` values from other sources without validation.
+
+
+
+## Handling Organization Errors
+
+Org-related failures arrive at the callback URL as OAuth error responses. The SDK surfaces them as typed exceptions:
+
+```python
+from auth0_server_python.error import ApiError, MissingTransactionError, OrganizationTokenValidationError
+
+try:
+ result = await auth0.complete_interactive_login(
+ str(request.url),
+ store_options={"request": request, "response": response},
+ )
+ return_to = (result.get("app_state") or {}).get("returnTo", "/")
+except OrganizationTokenValidationError:
+ return_to = "/error?reason=org_mismatch"
+except MissingTransactionError:
+ return_to = "/error?reason=expired_or_replayed"
+except ApiError as e:
+ return_to = f"/error?reason={e.code}"
+```
+
+| Exception | When raised |
+|-----------|-------------|
+| `OrganizationTokenValidationError` | `org_id` / `org_name` in the returned token does not match what was requested |
+| `MissingTransactionError` | The login transaction has expired or was already consumed (e.g. page reload or replayed callback) |
+| `ApiError` | Auth0 rejected the authorization request; inspect `e.code` and `e.message` for the raw OAuth error |
+
+Common `ApiError.code` values:
+
+| `e.code` | Typical cause |
+|---|---|
+| `access_denied` | User is not a member, connection not enabled for org, or member quota exceeded |
+| `invalid_request` | Invalid org format, feature disabled, client not configured for orgs, or expired / invalid invitation ticket |
+
+## Reading Organization Data from the Session
+
+After a successful org login, `org_id` is present in the user object. `org_name` is also present when the org has the display-name feature enabled:
+
+```python
+user = await auth0.get_user(store_options={"request": request, "response": response})
+if user:
+ org_id = user.get("org_id") # always present; use as the stable identifier
+ org_name = user.get("org_name") # present when org display name is enabled
+```
+
+Use `org_id` (not `org_name`) as a stable identifier in your own data store - org names can be changed.
+
+## Full Example: Multi-Org App
+
+`auth0-server-python` is framework-agnostic - it reads and writes only through the `request` / `response` objects you pass via `store_options`. The handlers below are plain `async` functions. Wire them into whatever framework you use. They model a multi-org app that resolves the current org from the subdomain and gates the dashboard on membership:
+
+```python
+from typing import Optional
+
+from auth0_server_python.auth_server.server_client import ServerClient
+from auth0_server_python.auth_types import StartInteractiveLoginOptions
+from auth0_server_python.error import ApiError, MissingTransactionError, OrganizationTokenValidationError
+
+auth0 = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ authorization_params={"redirect_uri": "http://localhost:3000/auth/callback"},
+ # ... your configured transaction_store and state_store
+)
+
+# Map your subdomains (or any trusted source) to org IDs.
+SUBDOMAIN_TO_ORG: dict = {
+ "acme": "org_acme123",
+ "globex": "org_globex456",
+}
+
+
+def resolve_org(hostname: str) -> Optional[str]:
+ subdomain = hostname.split(".")[0]
+ return SUBDOMAIN_TO_ORG.get(subdomain)
+
+
+# `request` and `response` are your framework's objects (or whatever your
+# configured transaction/state store reads cookies from and writes them to).
+async def handle_login(request, response):
+ store_options = {"request": request, "response": response}
+ org_id = resolve_org(request.headers.get("host", ""))
+ if org_id is None:
+ # No org is mapped to this host. Redirect to an error page or fall back
+ # to a non-org login depending on your app's requirements.
+ raise ValueError(f"No organization mapped for host: {request.headers.get('host', '')}")
+
+ return await auth0.start_interactive_login(
+ StartInteractiveLoginOptions(
+ organization=org_id,
+ app_state={"returnTo": "/dashboard"},
+ ),
+ store_options=store_options,
+ )
+
+
+# `callback_url` is the full URL Auth0 redirected back to, including `?code=...&state=...`.
+async def handle_callback(callback_url: str, request, response):
+ store_options = {"request": request, "response": response}
+ try:
+ result = await auth0.complete_interactive_login(callback_url, store_options=store_options)
+ return (result.get("app_state") or {}).get("returnTo", "/")
+ except OrganizationTokenValidationError:
+ return "/error?reason=org_mismatch"
+ except MissingTransactionError:
+ return "/error?reason=expired_or_replayed"
+ except ApiError as e:
+ return f"/error?reason={e.code}"
+
+
+async def handle_dashboard(request, response):
+ store_options = {"request": request, "response": response}
+ user = await auth0.get_user(store_options=store_options)
+ if not user:
+ return None # redirect to login
+
+ org_id = user.get("org_id")
+ org_name = user.get("org_name")
+
+ # Verify the session belongs to the org mapped to the current host. Without
+ # this check, a wildcard cookie (e.g. Domain=.yourapp.com) lets a session
+ # from acme.yourapp.com access globex.yourapp.com data.
+ expected_org = resolve_org(request.headers.get("host", ""))
+ if org_id != expected_org:
+ return None # org mismatch - redirect to login for the correct org
+
+ return {"user": user, "org_id": org_id, "org_name": org_name}
+```
diff --git a/docs/dist/examples/Passkeys.mdx b/docs/dist/examples/Passkeys.mdx
new file mode 100644
index 00000000..e8405fe7
--- /dev/null
+++ b/docs/dist/examples/Passkeys.mdx
@@ -0,0 +1,307 @@
+---
+title: "Passkey Authentication"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Passkeys let users sign up and log in with [WebAuthn](https://www.w3.org/TR/webauthn-2/) credentials (Touch ID, Face ID, Windows Hello, or a hardware security key) instead of a password. This guide covers the **primary authentication** flow on `ServerClient` — signing a user up or in with a passkey and establishing a server-side session.
+
+
+
+Passkeys require a [Custom Domain](https://auth0.com/docs/customize/custom-domains) (WebAuthn binds the credential to the relying-party domain) and the native passkey feature enabled on your tenant. See the [Auth0 passkey documentation](https://auth0.com/docs/authenticate/database-connections/passkeys).
+
+
+
+
+
+Managing a logged-in user's enrolled passkeys (enroll a new passkey, list, rename, delete) is a **separate** surface on the My Account API. See [examples/MyAccountAuthenticationMethods.md](/docs/sdk/python/server/examples/MyAccountAuthenticationMethods) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/MyAccountAuthenticationMethods.md)).
+
+
+
+## Table of Contents
+
+- [How the flow works](#how-the-flow-works)
+- [Prerequisites](#prerequisites)
+- [1. Passkey Signup](#1-passkey-signup)
+- [2. Passkey Login](#2-passkey-login)
+- [3. DPoP-bound passkey tokens (optional)](#3-dpop-bound-passkey-tokens-optional)
+ - [Completing MFA on a passkey login (and where the session comes from)](#completing-mfa-on-a-passkey-login-and-where-the-session-comes-from)
+- [Error Handling](#error-handling)
+
+## How the flow works
+
+A passkey ceremony is always **two steps**, because the WebAuthn signature happens in the browser between them:
+
+1. **Challenge** — the SDK asks Auth0 for a challenge (`passkey_signup_challenge` / `passkey_login_challenge`). Auth0 returns an `auth_session` and the WebAuthn options (`authn_params_public_key`).
+2. **Browser** — your front end passes those options to `navigator.credentials.create()` (signup) or `navigator.credentials.get()` (login). The authenticator produces a signed credential.
+3. **Verify / sign-in** — the SDK exchanges the signed credential for tokens (`signin_with_passkey`) and **creates a server-side session**, exactly like every other login path.
+
+## Prerequisites
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+
+# Passkey sign-in persists a server-side session, and the challenge step stores
+# short-lived transaction data, so ServerClient needs a state store and a
+# transaction store (it does not create defaults). If your framework wrapper
+# (e.g. auth0-fastapi) already provides these, reuse them. Otherwise, create
+# your own implementations of the StateStore / TransactionStore ABCs.
+state_store = YourStateStore(...) # or the store your framework provides
+transaction_store = YourTransactionStore(...) # or the store your framework provides
+
+server_client = ServerClient(
+ domain="YOUR_CUSTOM_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ state_store=state_store,
+ transaction_store=transaction_store,
+)
+```
+
+For store implementations (cookie, Redis, database) and how to pass `store_options`, see [examples/ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+The **Passkey** grant (`urn:okta:params:oauth:grant-type:webauthn`) must be enabled for your application under **Applications → Your App → Grant Types**.
+
+
+
+The passkey challenge endpoints accept a client secret only, not a client assertion. A client configured with just a `client_assertion_signing_key` (Private Key JWT) cannot use the passkey flows, so configure a `client_secret` to use them.
+
+
+
+## 1. Passkey Signup
+
+### Step 1 — Request a signup challenge
+
+```python
+from auth0_server_python.auth_types import PasskeyUserProfile
+
+challenge = await server_client.passkey_signup_challenge(
+ user_profile=PasskeyUserProfile(
+ email="new.user@example.com",
+ name="Jane Doe",
+ username="jane.doe",
+ phone_number="+15551234567",
+ given_name="Jane",
+ family_name="Doe",
+ nickname="Janie",
+ picture="https://example.com/jane.png",
+ ),
+ connection="Username-Password-Authentication", # optional database connection (realm)
+ store_options={"request": request, "response": response},
+)
+
+# Hand these to the browser:
+# challenge.auth_session -> opaque session credential (Tier 1, do not log)
+# challenge.authn_params_public_key -> pass to navigator.credentials.create()
+```
+
+
+
+`PasskeyUserProfile` allows extra fields — any additional profile attribute your tenant accepts (for example `given_name`, `family_name`, `picture`) passes through without an SDK change. Pass tenant-specific custom data via the separate `user_metadata` argument.
+
+
+
+### Step 2 — Create the credential in the browser
+
+Pass `authn_params_public_key` to `navigator.credentials.create()`. The resulting credential serializes to the shape the SDK expects in step 3 (`id`, `rawId`, `type`, and a `response` object with `clientDataJSON` + `attestationObject`).
+
+### Step 3 — Verify and establish the session
+
+```python
+from auth0_server_python.auth_types import PasskeyAuthResponse
+
+result = await server_client.signin_with_passkey(
+ auth_session=challenge.auth_session,
+ authn_response=PasskeyAuthResponse(
+ id=credential["id"],
+ raw_id=credential["rawId"], # accepts rawId alias too
+ type="public-key",
+ response={
+ "clientDataJSON": credential["response"]["clientDataJSON"],
+ "attestationObject": credential["response"]["attestationObject"],
+ },
+ ),
+ store_options={"request": request, "response": response},
+)
+
+user = result.state_data["user"]
+print(f"Signed up and logged in: {user['sub']}")
+```
+
+`signin_with_passkey` returns a `PasskeyLoginResult` whose `state_data` holds the user claims and token sets — the same shape as `complete_interactive_login` and `login_with_custom_token_exchange`. The session is persisted to your configured state store.
+
+## 2. Passkey Login
+
+Identical shape, different endpoints. The browser uses `navigator.credentials.get()` and the user picks a passkey from the prompt. That credential identifies the user, so the login challenge takes no username.
+
+```python
+# Step 1 — login challenge
+challenge = await server_client.passkey_login_challenge(
+ connection="Username-Password-Authentication", # optional
+ store_options={"request": request, "response": response},
+)
+
+# Step 2 — browser: navigator.credentials.get(challenge.authn_params_public_key)
+
+# Step 3 — sign in. The login credential's response carries
+# clientDataJSON + authenticatorData + signature + userHandle.
+result = await server_client.signin_with_passkey(
+ auth_session=challenge.auth_session,
+ authn_response=PasskeyAuthResponse(
+ id=credential["id"],
+ raw_id=credential["rawId"],
+ type="public-key",
+ response={
+ "clientDataJSON": credential["response"]["clientDataJSON"],
+ "authenticatorData": credential["response"]["authenticatorData"],
+ "signature": credential["response"]["signature"],
+ "userHandle": credential["response"]["userHandle"],
+ },
+ ),
+ store_options={"request": request, "response": response},
+)
+```
+
+
+
+The SDK is transparent to the signup-vs-login difference in the credential `response` — both flow through the same `PasskeyAuthResponse.response` dict. Send exactly the keys the browser produced.
+
+
+
+## 3. DPoP-bound passkey tokens (optional)
+
+Pass an optional `dpop_key` to bind the issued tokens to a key your server holds ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)), so a stolen token alone cannot be replayed. DPoP is **opt-in**: omit `dpop_key` and sign-in returns ordinary Bearer tokens with no behaviour change.
+
+```python
+from jwcrypto import jwk
+
+dpop_key = jwk.JWK.generate(kty="EC", crv="P-256") # you generate and keep this key (Tier 0)
+
+result = await server_client.signin_with_passkey(
+ auth_session=challenge.auth_session,
+ authn_response=authn_response,
+ dpop_key=dpop_key,
+ store_options={"request": request, "response": response},
+)
+```
+
+When `dpop_key` is supplied, the SDK attaches a token-endpoint proof so Auth0 issues a DPoP-bound token, transparently handles the server-nonce challenge, and **rejects a Bearer downgrade** — if the server returns an unbound token, `signin_with_passkey` raises `PasskeyError` rather than silently accepting a token bound to a key it never used.
+
+
+
+Reuse the **same** `dpop_key` for any subsequent My Account API calls made with the resulting token — the token is bound to that one key. See [MyAccountAuthenticationMethods.md → DPoP](/docs/sdk/python/server/examples/MyAccountAuthenticationMethods#dpop) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/MyAccountAuthenticationMethods.md#dpop)).
+
+
+
+
+
+The `dpop_key` private key is a **Tier 0 secret**. Keep it in your secret store (KMS/HSM), never log it (`repr()` is redacted, but `key.export_private()` is not), use **one key per user/session** (never share across principals), and use **EC P-256 only** — any other key type fails closed with a `ValueError` before any network call.
+
+
+
+### Completing MFA on a passkey login (and where the session comes from)
+
+When a passkey login needs a second factor, `signin_with_passkey` raises `MfaRequiredError` **before** it creates a session. You finish the login by challenging and verifying through `client.mfa`, then **store the returned tokens yourself** — on this path the SDK does not persist the session for you (`persist` defaults to `False`, and there is no existing session to update yet):
+
+```python
+from auth0_server_python.error import MfaRequiredError
+
+try:
+ result = await server_client.signin_with_passkey(
+ auth_session=auth_session,
+ authn_response=authn_response,
+ dpop_key=dpop_key, # optional; omit for Bearer tokens
+ store_options={"request": request, "response": response},
+ )
+ # No MFA needed: signin_with_passkey already persisted the session for you.
+ user = result.state_data["user"]
+
+except MfaRequiredError as e:
+ # 1. Challenge the factor (e.g. an authenticator-app OTP).
+ await server_client.mfa.challenge_authenticator(
+ {"mfa_token": e.mfa_token, "factor_type": "otp"},
+ store_options={"request": request, "response": response},
+ )
+
+ # 2. Verify the user's code. Re-supply the SAME dpop_key so the issued
+ # token stays DPoP-bound. persist=False (the default) → the SDK returns
+ # the tokens instead of writing a session.
+ verify_response = await server_client.mfa.verify(
+ {"mfa_token": e.mfa_token, "otp": otp_code},
+ dpop_key=dpop_key, # same key given to signin_with_passkey
+ store_options={"request": request, "response": response},
+ )
+
+ # 3. Persist the tokens into YOUR session yourself — this is the step the
+ # SDK skips on the MFA path because no session existed at login time.
+ save_session_for_user(
+ access_token=verify_response.access_token,
+ id_token=verify_response.id_token,
+ refresh_token=verify_response.refresh_token,
+ )
+```
+
+
+
+Re-supply the **same** `dpop_key` to `verify`. Omitting it when the login was DPoP-bound would downgrade the result to a Bearer token; `verify` **rejects** that mismatch with `MfaVerifyError` rather than silently dropping the sender constraint. DPoP is preserved end to end — `persist=False` affects only *who writes the session*, never the token binding.
+
+
+
+
+
+Do **not** pass `persist=True` on this path. It updates an *existing* session, and a passkey-first login has none yet, so it raises `MfaVerifyError("No existing session found…")` — discarding the tokens `verify` just obtained. Use `persist=False` and store the returned tokens as shown above. (This is pre-existing MFA-client behavior, unrelated to passkeys.)
+
+
+
+## Error Handling
+
+The three passkey methods raise `PasskeyError` (a subclass of `Auth0Error`). Input-validation failures raise `MissingRequiredArgumentError`; a required step-up raises `MfaRequiredError`. For most code, catching `Auth0Error` is enough.
+
+### Basic handling (recommended)
+
+```python
+from auth0_server_python.error import Auth0Error
+
+try:
+ result = await server_client.signin_with_passkey(
+ auth_session=auth_session,
+ authn_response=authn_response,
+ store_options={"request": request, "response": response},
+ )
+except Auth0Error as e:
+ return {"error": str(e)}
+```
+
+### Advanced handling (when actions differ by case)
+
+```python
+from auth0_server_python.error import PasskeyError, MfaRequiredError, Auth0Error
+
+try:
+ result = await server_client.signin_with_passkey(
+ auth_session=auth_session,
+ authn_response=authn_response,
+ store_options={"request": request, "response": response},
+ )
+except MfaRequiredError as e:
+ return start_mfa(e.mfa_token) # step-up required — challenge + verify via client.mfa,
+ # then store the returned tokens (see "Completing MFA
+ # on a passkey login" above)
+except PasskeyError as e:
+ return {"error": e.code, "detail": e.message} # branch on e.code, never on message text
+except Auth0Error as e:
+ return {"error": str(e)}
+```
+
+### Common error codes (`PasskeyErrorCode`)
+
+- `passkey_challenge_error` — the signup/login challenge request failed
+- `passkey_token_error` — token exchange failed (also used for a rejected DPoP downgrade)
+- `invalid_response` — Auth0 returned a response that could not be parsed
+
+
+
+`auth_session` is a short-lived (typically ~5 min) Tier 1 credential. It is redacted in the SDK's model `repr()`, and you should never log or persist it. If the ceremony takes too long, re-request the challenge.
+
+
diff --git a/docs/dist/examples/Passwordless.mdx b/docs/dist/examples/Passwordless.mdx
new file mode 100644
index 00000000..05b20b03
--- /dev/null
+++ b/docs/dist/examples/Passwordless.mdx
@@ -0,0 +1,402 @@
+---
+title: "Passwordless Authentication"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Passwordless lets users sign in with a one-time code sent by email or SMS, or with a magic link sent by email. This guide covers the **embedded login** flow on `ServerClient.passwordless` and how each path establishes a server-side session.
+
+
+
+Passwordless API flows use Auth0 Passwordless connections (`email` and `sms`). Enable the **Passwordless OTP** grant for your application. See the [Auth0 Passwordless API documentation](https://auth0.com/docs/authenticate/passwordless/implement-login/embedded-login/relevant-api-endpoints).
+
+
+
+
+
+These flows are for confidential server-side applications. Tokens stay on the server. The browser should only receive your application's session cookie or opaque session reference.
+
+
+
+
+
+**This SDK currently does not support DPoP on passwordless.** Neither `start()` nor `verify()` accepts a `dpop_key`, and tokens issued by the OTP grant or the magic-link callback are always Bearer tokens, never sender-constrained.
+
+
+
+## Table of Contents
+
+- [How the flow works](#how-the-flow-works)
+- [Prerequisites](#prerequisites)
+- [1. Email OTP](#1-email-otp)
+- [2. SMS OTP](#2-sms-otp)
+- [3. Email magic link](#3-email-magic-link)
+- [4. Custom scopes and audiences](#4-custom-scopes-and-audiences)
+- [5. Forwarding the end-user IP](#5-forwarding-the-end-user-ip)
+- [Completing MFA during passwordless login](#completing-mfa-during-passwordless-login)
+- [Error Handling](#error-handling)
+
+## How the flow works
+
+Passwordless has two shapes:
+
+1. **OTP code** - `start()` sends a code by email or SMS. Your app collects that code, then `verify()` exchanges it at `/oauth/token` with the passwordless OTP grant and **creates a server-side session**.
+2. **Magic link** - `start(send="link")` sends a one-click email link. Auth0 redirects the user back to your callback URL, and your app completes the flow with `complete_interactive_login()`. The callback creates the server-side session.
+
+OTP start does **not** create a session. The session exists only after `verify()` succeeds. Magic-link start writes a transaction so the callback can validate the returned `state`. The session exists only after the callback completes.
+
+## Prerequisites
+
+These flows require a **Regular Web Application**. Passwordless token exchange
+needs a client secret, which a public client cannot hold safely.
+
+Two tenant-level settings are also required:
+
+1. **Authentication Profile must be "Identifier First."** The default
+ "Universal Login" profile blocks the direct `/oauth/token` call this SDK
+ uses for OTP verification. Without it, OTP `verify()` fails with
+ `unauthorized_client`. Set it under your tenant's Authentication Profile
+ settings. (Skip this if you only use passwordless via Universal Login
+ redirects rather than this SDK's embedded flow.)
+2. **Enable the Passwordless OTP grant type** on your application. Without
+ it, OTP verification also fails with `unauthorized_client`.
+3. **Magic link only.** Set the tenant flag
+ `universal_login.passwordless.allow_magiclink_verify_without_session` to `true`.
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+
+server_client = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ redirect_uri="https://app.example.com/auth/callback",
+)
+```
+
+`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+For apps using request/response-backed stores or multiple custom domains, pass `store_options={"request": request, "response": response}` to each method that reads or writes transaction/session state.
+
+## 1. Email OTP
+
+### Step 1 - Send the code
+
+```python
+from auth0_server_python.auth_types import StartPasswordlessEmailOptions
+
+start_result = await server_client.passwordless.start(
+ StartPasswordlessEmailOptions(
+ email="user@example.com",
+ send="code",
+ language="en-US", # optional x-request-language header
+ ),
+ store_options={"request": request, "response": response},
+)
+
+```
+
+### Step 2 - Verify the code and establish the session
+
+```python
+from auth0_server_python.auth_types import VerifyPasswordlessOtpOptions
+
+result = await server_client.passwordless.verify(
+ VerifyPasswordlessOtpOptions(
+ connection="email",
+ email="user@example.com",
+ verification_code=user_entered_code,
+ ),
+ store_options={"request": request, "response": response},
+)
+
+user = result["state_data"]["user"]
+print(f"Signed in: {user['sub']}")
+```
+
+The SDK verifies the returned ID token, validates the issuer and audience, persists the tokens in the configured state store, and sources the session `sid` from the verified ID token when available.
+
+## 2. SMS OTP
+
+SMS has the same two-step shape. Phone numbers must be in E.164 format.
+
+```python
+from auth0_server_python.auth_types import (
+ StartPasswordlessSmsOptions,
+ VerifyPasswordlessOtpOptions,
+)
+
+await server_client.passwordless.start(
+ StartPasswordlessSmsOptions(
+ phone_number="+14155550100",
+ ),
+ store_options={"request": request, "response": response},
+)
+
+result = await server_client.passwordless.verify(
+ VerifyPasswordlessOtpOptions(
+ connection="sms",
+ phone_number="+14155550100",
+ verification_code=user_entered_code,
+ ),
+ store_options={"request": request, "response": response},
+)
+```
+
+By default, email OTP requests `openid profile email`. SMS OTP requests `openid profile` because SMS identities do not have an email claim to satisfy.
+
+## 3. Email magic link
+
+Magic links are email-only. `start(send="link")` persists a transaction and includes SDK-owned `redirect_uri`, `response_type`, and `state` in Auth0's `authParams`.
+
+```python
+from auth0_server_python.auth_types import StartPasswordlessEmailOptions
+
+await server_client.passwordless.start(
+ StartPasswordlessEmailOptions(
+ email="user@example.com",
+ send="link",
+ auth_params={
+ "scope": "openid profile email",
+ "login_hint": "user@example.com",
+ },
+ ),
+ store_options={"request": request, "response": response},
+)
+```
+
+
+
+Magic-link start requires `store_options` whenever your transaction store needs the framework request/response to write state. Without that transaction, the callback cannot validate the returned `state`.
+
+
+
+When the user clicks the emailed link, Auth0 redirects back to your configured callback URL. Complete it with the standard interactive-login callback:
+
+```python
+callback_url = str(request.url)
+
+result = await server_client.complete_interactive_login(
+ callback_url,
+ store_options={"request": request, "response": response},
+)
+
+user = result["state_data"]["user"]
+```
+
+
+
+Do not let callers override `redirect_uri`, `state`, `response_type`, `nonce`, or PKCE fields in magic-link `auth_params`. The SDK owns these values so the emailed authorization code and state cannot be redirected to an attacker-controlled URL.
+
+This matters more than it looks: Auth0 treats magic-link `state` as a pure echo and does not validate it server-side, and the clicked link's query string can overwrite whatever the browser originally stored. The SDK's single-use, `state`-keyed transaction plus the exact-match `redirect_uri` are therefore the *entire* CSRF / authorization-code-interception defense on this flow. Auth0 will not catch a bypass for you.
+
+
+
+
+
+If the callback fails (expired link, JWKS unavailable, a rejected ID token), have the user restart the flow from `start()` rather than retrying the same link. A failed callback does not guarantee the transaction was cleaned up, so re-submitting the same callback URL can produce a confusing error instead of a clear "session expired, please try again."
+
+
+
+## 4. Custom scopes and audiences
+
+For OTP flows, pass `scope` and `audience` to `verify()`. These become the `/oauth/token` request parameters.
+
+```python
+result = await server_client.passwordless.verify(
+ VerifyPasswordlessOtpOptions(
+ connection="email",
+ email="user@example.com",
+ verification_code=user_entered_code,
+ audience="https://api.example.com",
+ scope="openid profile email offline_access read:orders",
+ ),
+ store_options={"request": request, "response": response},
+)
+```
+
+A caller-supplied OTP `scope` **replaces** the default wholesale rather than merging with it. The SDK re-injects `openid` when your scope omits it, for the same reason as magic link below: without it, Auth0 returns no ID token and `verify()` fails.
+
+For magic links, pass allowed authorization parameters through `auth_params` at `start()` time:
+
+```python
+await server_client.passwordless.start(
+ StartPasswordlessEmailOptions(
+ email="user@example.com",
+ send="link",
+ auth_params={
+ "audience": "https://api.example.com",
+ "scope": "openid profile email offline_access read:orders",
+ "login_hint": "user@example.com",
+ },
+ ),
+ store_options={"request": request, "response": response},
+)
+```
+
+A caller-supplied magic-link `scope` **replaces** the default wholesale rather than merging with it. The SDK re-injects `openid` when your scope omits it, because the magic-link callback would otherwise complete on a token response carrying no ID token, a session built from claims nothing verified. `openid` is never duplicated and your scope order is preserved otherwise.
+
+
+
+`state` is intentionally not a caller-supplied auth parameter in this SDK. If you need app-specific return data, store it server-side against your own transaction/session context instead of putting it into the Auth0 magic-link `state`.
+
+
+
+## 5. Forwarding the end-user IP
+
+Auth0 attack protection and rate limiting normally see the IP address of the server making the API call. For confidential clients, Auth0 can use the `auth0-forwarded-for` header when the **Trust Token Endpoint IP Header** setting is enabled.
+
+```python
+await server_client.passwordless.start(
+ StartPasswordlessEmailOptions(
+ email="user@example.com",
+ send="code",
+ client_ip=request.client.host,
+ ),
+ store_options={"request": request, "response": response},
+)
+
+result = await server_client.passwordless.verify(
+ VerifyPasswordlessOtpOptions(
+ connection="email",
+ email="user@example.com",
+ verification_code=user_entered_code,
+ client_ip=request.client.host,
+ ),
+ store_options={"request": request, "response": response},
+)
+```
+
+
+
+Only forward a trusted, normalized end-user IP from your edge/proxy layer. Do not blindly copy arbitrary client-supplied headers into `client_ip`.
+
+
+
+## Completing MFA during passwordless login
+
+Auth0 can require MFA during passwordless OTP verification. In that case, the SDK raises `MfaRequiredError` before it creates a session. Complete the MFA challenge with `server_client.mfa` and pass `persist=True` on verification so the SDK creates the session from the final MFA token response.
+
+```python
+from auth0_server_python.error import MfaRequiredError
+
+try:
+ result = await server_client.passwordless.verify(
+ VerifyPasswordlessOtpOptions(
+ connection="email",
+ email="user@example.com",
+ verification_code=user_entered_code,
+ ),
+ store_options={"request": request, "response": response},
+ )
+ user = result["state_data"]["user"]
+
+except MfaRequiredError as e:
+ await server_client.mfa.challenge_authenticator(
+ {"mfa_token": e.mfa_token, "factor_type": "otp"},
+ store_options={"request": request, "response": response},
+ )
+
+ await server_client.mfa.verify(
+ {"mfa_token": e.mfa_token, "otp": mfa_code, "persist": True},
+ store_options={"request": request, "response": response},
+ )
+
+ session = await server_client.get_session(
+ store_options={"request": request, "response": response},
+ )
+ user = session["user"]
+```
+
+
+
+For Passwordless OTP MFA there is no existing application session until MFA verification succeeds. `persist=True` creates the initial SDK session when the MFA response includes an ID token.
+
+
+
+## Error Handling
+
+Passwordless methods raise typed SDK errors:
+
+- `PasswordlessStartError` - `POST /passwordless/start` failed
+- `PasswordlessVerifyError` - OTP token exchange or ID-token verification failed, including an issuer or audience mismatch (`invalid_issuer` / `invalid_audience`)
+- `MfaRequiredError` - Auth0 requires MFA before completing login
+- `MissingRequiredArgumentError` - required SDK input is missing, such as magic-link `store_options`
+- `InvalidArgumentError` - caller input is rejected before a network call
+
+### Basic handling
+
+```python
+from auth0_server_python.error import Auth0Error
+
+try:
+ result = await server_client.passwordless.verify(
+ VerifyPasswordlessOtpOptions(
+ connection="email",
+ email="user@example.com",
+ verification_code=user_entered_code,
+ ),
+ store_options={"request": request, "response": response},
+ )
+except Auth0Error as e:
+ return {"error": str(e)}
+```
+
+### Advanced handling
+
+```python
+from auth0_server_python.error import (
+ Auth0Error,
+ MfaRequiredError,
+ PasswordlessStartError,
+ PasswordlessVerifyError,
+)
+
+try:
+ result = await server_client.passwordless.verify(
+ VerifyPasswordlessOtpOptions(
+ connection="email",
+ email="user@example.com",
+ verification_code=user_entered_code,
+ ),
+ store_options={"request": request, "response": response},
+ )
+except MfaRequiredError as e:
+ return start_mfa(e.mfa_token)
+except PasswordlessVerifyError as e:
+ return {"error": e.code, "detail": e.message, "retry_after": e.retry_after}
+except PasswordlessStartError as e:
+ return {"error": e.code, "detail": e.message, "retry_after": e.retry_after}
+except Auth0Error as e:
+ return {"error": str(e)}
+```
+
+`PasswordlessStartError` and `PasswordlessVerifyError` both carry:
+
+- `code` / `message` - the Auth0 `error` and `error_description`, or an SDK-side code when the response had neither
+- `error` / `error_description` - the raw values from a JSON error body
+- `retry_after` - seconds from the `Retry-After` response header, typically on a 429. `None` when the header is absent or in HTTP-date form (which the SDK does not interpret)
+- `cause` - the parsed JSON error body, or the response text truncated to 2048 characters when the body was not JSON
+
+
+
+`cause` may hold a raw upstream body (an HTML error page, WAF block page, or proxy dump). It is length-capped, but not redacted. Do not log it at a level where untrusted upstream content is unwelcome.
+
+
+
+Because a 429 that carries an explicit Auth0 `error` reports that server code, `code == "too_many_requests"` is not a reliable rate-limit predicate. Branch on `retry_after is not None`, or on the HTTP status if you need certainty.
+
+### Common error codes (`PasswordlessErrorCode`)
+
+- `bad.connection` - the passwordless connection is disabled or invalid
+- `bad.email` - the email address is invalid or rejected by Auth0
+- `sms_provider_error` - Auth0 could not send the SMS
+- `too_many_requests` - rate limiting or attack protection blocked the request (`start()` or `verify()`)
+- `invalid_grant` - the OTP is invalid, expired, or already used
+- `invalid_issuer` - returned ID token issuer does not match your configured Auth0 domain
+- `invalid_audience` - returned ID token audience does not match the SDK client
+- `token_expired` - the returned ID token's signature has already expired
+- `discovery_error` - the SDK could not load authorization server metadata
+- `passwordless_start_failed` - SDK-side start failure
+- `passwordless_verify_failed` - SDK-side verify failure
diff --git a/docs/dist/examples/RetrievingData.mdx b/docs/dist/examples/RetrievingData.mdx
new file mode 100644
index 00000000..e531c39b
--- /dev/null
+++ b/docs/dist/examples/RetrievingData.mdx
@@ -0,0 +1,281 @@
+---
+title: "Retrieving Data"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+## Retrieving the logged-in User
+
+The SDK's `get_user()` can be used to retrieve the current logged-in user:
+
+```python
+user = await server_client.get_user()
+```
+
+### Passing Store Options
+
+Just like most methods, `get_user` accept an argument that is used to pass to the configured Transaction and State Store:
+
+```python
+store_options = {
+ # e.g. "request": ,
+ # "response":
+}
+user = await server_client.get_user(store_options=store_options)
+```
+
+Read more above in [Configuring the Store](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+## Retrieving the Session Data
+
+The SDK's `get_session()` can be used to retrieve the current session data:
+
+```python
+session = await server_client.get_session()
+```
+
+### Passing Store Options
+
+Just like most methods, `get_session` accept an argument that is used to pass to the configured Transaction and State Store:
+
+```python
+store_options = {
+ # e.g. "request": ,
+ # "response":
+}
+session = await server_client.get_session(store_options=store_options)
+```
+
+Read more above in [Configuring the Store](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+## Retrieving an Access Token
+
+The SDK's `get_access_token()` can be used to retrieve an Access Token for the current logged-in user:
+
+```python
+access_token = await server_client.get_access_token()
+```
+
+The SDK will cache the token internally, and return it from the cache when not expired. When no token is found in the cache, or the token is expired, calling `get_access_token()` will call Auth0 to retrieve a new token and update the cache.
+
+In order to do this, the SDK needs access to a Refresh Token. By default, the SDK is configured to request the `offline_access` scope. If you override the scopes, ensure to always include `offline_access` if you want to be able to retrieve and refresh an access token.
+
+### Passing Store Options
+
+Just like most methods, `get_access_token` accept an argument that is used to pass to the configured Transaction and State Store:
+
+```python
+store_options = {
+ # e.g. "request": ,
+ # "response":
+}
+access_token = await server_client.get_access_token(store_options=store_options)
+```
+
+Read more above in [Configuring the Store](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+## Session Expiry from the Upstream IdP
+
+For enterprise connections, the upstream identity provider can impose a ceiling on how long the user's session may live. This ceiling is delivered to the SDK as a `session_expiry` claim (an absolute Unix timestamp, **in seconds**) on the ID token. The SDK reads this value at login, stores it with the session, and enforces it on every subsequent read.
+
+### Emitting the claim
+
+The `session_expiry` claim is set by a Post-Login Action on your tenant, and **must** be an absolute Unix timestamp in **seconds**, not milliseconds. For the canonical Action setup, see the [Auth0 documentation](https://auth0.com/docs) (will be adding the link to the session_expiry Action guide once published).
+
+
+
+`session_expiry` is interpreted as **seconds** since the Unix epoch (per RFC 7519 `NumericDate`). A common mistake is emitting milliseconds (e.g. `getTime()` without `/ 1000`). The SDK rejects implausibly large values (anything at or above `10_000_000_000`, ≈ year 2286) as malformed and treats them as **no ceiling**, so a milliseconds value will silently disable enforcement rather than expiring the session ~55,000 years from now. Always divide by 1000.
+
+Because the claim is authored by your Action (untrusted input), the SDK **fails open** on any malformed value — a non-numeric, zero, negative, boolean, or millisecond value is treated as "no ceiling" and login proceeds normally. Only a clean, future, seconds timestamp is enforced.
+
+
+
+Once the ceiling is reached, the read methods behave as follows:
+
+- `get_user()` returns `None`, as if no session exists.
+- `get_session()` returns `None`, as if no session exists.
+- `get_access_token()` raises an `AccessTokenError` with code `session_expired`.
+
+`get_access_token_for_connection()` (Token Vault) is **not** gated by the session ceiling — connection tokens follow the upstream IdP's own `expires_in`, so they remain retrievable from cache even after the session ceiling has passed.
+
+```python
+from auth0_server_python.error import AccessTokenError, AccessTokenErrorCode
+
+try:
+ access_token = await server_client.get_access_token(store_options=store_options)
+except AccessTokenError as error:
+ if error.code == AccessTokenErrorCode.SESSION_EXPIRED:
+ # The upstream session ceiling has been reached; start a new login.
+ ...
+```
+
+When the ceiling is reached, the SDK deletes the stored session before returning, so the next request starts clean.
+
+If the upstream IdP asserts a ceiling that is already in the past at login time, `complete_interactive_login()` raises a `SessionExpiredError` rather than persisting an already-expired session:
+
+```python
+from auth0_server_python.error import SessionExpiredError
+
+try:
+ await server_client.complete_interactive_login(url, store_options=store_options)
+except SessionExpiredError:
+ # The session was already past its ceiling on arrival; start a new login.
+ ...
+```
+
+
+
+**Upgrading:** with this feature enabled, `get_user()` and `get_session()` can return `None` for a user who was previously logged in, once the upstream ceiling passes. Applications that assumed these always return a value after login should add a null check and route the user back through login.
+
+
+
+The `session_expiry` value is also surfaced through the user claims, so you can read it without triggering enforcement:
+
+```python
+user = await server_client.get_user(store_options=store_options)
+session_expires_at = (user or {}).get("session_expiry")
+```
+
+
+
+Enforcement applies a small negative leeway (30 seconds) to account for clock skew, so a session is treated as expired slightly before the exact `session_expiry` timestamp. The refresh-token grant preserves the original ceiling - refreshing an access token does not extend the upstream session.
+
+
+
+## Multi-Resource Refresh Tokens (MRRT)
+
+Multi-Resource Refresh Tokens allow using a single refresh token to obtain access tokens for multiple audiences, simplifying token management in applications that interact with multiple backend services.
+
+Read more about [Multi-Resource Refresh Tokens in the Auth0 documentation](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token).
+
+
+
+
+When using Multi-Resource Refresh Token Configuration (MRRT), **Refresh Token Policies** on your Application need to be configured with the audiences you want to support. See the [Auth0 MRRT documentation](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token) for setup instructions.
+
+**Tokens requested for audiences outside your configured policies will be ignored by Auth0, which will return a token for the default audience instead!**
+
+
+
+### Configuring Scopes Per Audience
+
+When working with multiple APIs, you can define different default scopes for each audience by passing an object instead of a string. This is particularly useful when different APIs require different default scopes:
+
+```python
+server_client = ServerClient(
+ ...
+ authorization_params={
+ "audience": "https://api.example.com", # Default audience
+ "scope": {
+ "https://api.example.com": "openid profile email offline_access read:products read:orders",
+ "https://analytics.example.com": "openid profile email offline_access read:analytics write:analytics",
+ "https://admin.example.com": "openid profile email offline_access read:admin write:admin delete:admin"
+ }
+ }
+)
+```
+
+**How it works:**
+
+- Each key in the `scope` object is an `audience` identifier
+- The corresponding value is the scope string for that audience
+- When calling `get_access_token(audience=audience)`, the SDK automatically uses the configured scopes for that audience. When scopes are also passed in the method call, they are be merged with the default scopes for that audience.
+
+### Usage Example
+
+To retrieve access tokens for different audiences, use the `get_access_token()` method with an `audience` (and optionally also the `scope`) parameter.
+
+```python
+
+server_client = ServerClient(
+ ...
+ authorization_params={
+ "audience": "https://api.example.com", # Default audience
+ "scope": {
+ "https://api.example.com": "openid email profile",
+ "https://analytics.example.com": "read:analytics write:analytics"
+ }
+ }
+)
+
+# Get token for default audience
+default_token = await server_client.get_access_token()
+# returns token for https://api.example.com with openid, email, and profile scopes
+
+ # Get token for different audience
+data_token = await server_client.get_access_token(audience="https://analytics.example.com")
+# returns token for https://analytics.example.com with read:analytics and write:analytics scopes
+
+# Get token with additional scopes
+admin_token = await server_client.get_access_token(
+ audience="https://api.example.com",
+ scope="write:admin"
+)
+# returns token for https://api.example.com with openid, email, profile and write:admin scopes
+
+```
+
+### Token Management Best Practices
+
+**Configure Broad Default Scopes**: Define comprehensive scopes in your `ServerClient` constructor for common use cases. This minimizes the need to request additional scopes dynamically, reducing the amount of tokens that need to be stored.
+
+```python
+server_client = ServerClient(
+ ...
+ authorization_params={
+ "audience": "https://api.example.com", # Default audience
+ # Configure broad default scopes for most common operations
+ "scope": {
+ "https://api.example.com": "openid profile email offline_access read:products read:orders read:users"
+ }
+ }
+)
+```
+
+**Minimize Dynamic Scope Requests**: Avoid passing `scope` when calling `get_access_token()` unless absolutely necessary. Each `audience` + `scope` combination results in a token to store in the session, increasing session size.
+
+```python
+# Preferred: Use default scopes
+token = await server_client.get_access_token(audience="https://api.example.com")
+
+
+# Avoid unless necessary: Dynamic scopes increase session size
+token = await server_client.get_access_token(
+ audience="https://api.example.com",
+ scope="openid profile email read:products write:products admin:all"
+)
+```
+
+## Retrieving an Access Token for a Connections
+
+The SDK's `get_access_token_for_connection()` can be used to retrieve an Access Token for a connection (e.g. `google-oauth2`) for the current logged-in user:
+
+```python
+connection_options = {
+ "connection": "google-oauth2"
+ # optionally "login_hint": ""
+}
+access_token_for_google = await server_client.get_access_token_for_connection(connection_options)
+
+```
+
+- `connection`: The connection for which an access token should be retrieved, e.g. `google-oauth2` for Google.
+- `login_hint`: Optional login hint to inform which connection account to use, can be useful when multiple accounts for the connection exist for the same user.
+
+The SDK will cache the token internally, and return it from the cache when not expired. When no token is found in the cache, or the token is expired, calling `get_access_token_for_connection()` will call Auth0 to retrieve a new token and update the cache.
+
+In order to do this, the SDK needs access to a Refresh Token. So ensure to always include `offline_access` if you want to be able to retrieve and refresh an access token for a connection.
+
+### Passing Store Options
+
+Just like most methods, `get_access_token_for_connection()` accepts a second argument that is used to pass to the configured Transaction and State Store:
+
+```python
+store_options = {
+ # e.g. "request": ,
+ # "response":
+}
+access_token_for_google = await server_client.get_access_token_for_connection(connection_options, store_options=store_options)
+```
+
+Read more above in [Configuring the Store](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
diff --git a/docs/dist/examples/StepUpAuthentication.mdx b/docs/dist/examples/StepUpAuthentication.mdx
new file mode 100644
index 00000000..790b0eb0
--- /dev/null
+++ b/docs/dist/examples/StepUpAuthentication.mdx
@@ -0,0 +1,157 @@
+---
+title: "Step-Up Authentication (MFA)"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+Step-up authentication asks an already-logged-in user to re-authenticate with a stronger factor before a sensitive action (transferring funds, changing security settings, deleting data). The user keeps their session. You just require higher assurance for the one action.
+
+With `auth0-server-python` you request step-up through the ordinary redirect flow - the same `start_interactive_login()` / `complete_interactive_login()` pair from [InteractiveLogin.md](/docs/sdk/python/server/examples/InteractiveLogin) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/InteractiveLogin.md)) - by adding an `acr_values` authorization parameter. Auth0's Universal Login handles the MFA challenge. When the user returns, you confirm it by reading the `acr` / `amr` claims from the session.
+
+For the concepts, tenant requirements, and available `acr_values` policies, see the [Auth0 step-up authentication for web apps docs](https://auth0.com/docs/secure/multi-factor-authentication/step-up-authentication/configure-step-up-authentication-for-web-apps).
+
+
+
+This is the right approach for apps using the **hosted login (Universal Login) redirect flow**. If your app drives an **embedded** MFA challenge itself (no redirect), use the MFA API via `client.mfa` instead - see [MFA.md](/docs/sdk/python/server/examples/MFA) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/MFA.md)).
+
+
+
+## Requesting Step-Up
+
+Pass `acr_values` inside `authorization_params` when starting the login. Add `max_age: 0` to force a fresh authentication instead of silently reusing the current session, and use `app_state` to remember where to resume. This assumes a configured `server_client` (see the constructor in [InteractiveLogin.md](/docs/sdk/python/server/examples/InteractiveLogin) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/InteractiveLogin.md)), or the full example below):
+
+```python
+from auth0_server_python.auth_types import StartInteractiveLoginOptions
+
+# The OIDC "multi-factor" policy URI - asks Auth0 to enforce MFA for this login.
+MFA_ACR = "http://schemas.openid.net/pape/policies/2007/06/multi-factor"
+
+authorization_url = await server_client.start_interactive_login(
+ StartInteractiveLoginOptions(
+ authorization_params={
+ "acr_values": MFA_ACR,
+ "max_age": 0, # force a fresh authentication instead of reusing the session
+ },
+ app_state={"returnTo": "/transfer"}, # resume this action after step-up
+ ),
+ store_options={"request": request, "response": response},
+)
+# Redirect the user to authorization_url.
+```
+
+## Verifying MFA Was Performed
+
+After the user returns, complete the login as usual, then read the claims from the session. When the multi-factor policy is satisfied, Auth0 sets `acr` to the requested policy URI and includes `mfa` in the `amr` array. Treat step-up as satisfied when **either** signal is present:
+
+```python
+from typing import Optional
+
+MFA_ACR = "http://schemas.openid.net/pape/policies/2007/06/multi-factor"
+
+def mfa_completed(user: Optional[dict]) -> bool:
+ if not user:
+ return False
+ return user.get("acr") == MFA_ACR or "mfa" in (user.get("amr") or [])
+```
+
+`acr` and `amr` are standard OIDC claims the SDK surfaces on the user object alongside `sub`, `email`, and the rest. They are only present when the corresponding authentication took place, so a plain login (no step-up) will not carry them.
+
+
+
+This check is per-session, not per-action. Once a user steps up, `acr` and `amr` stay on the session, so every later sensitive action passes without a fresh prompt. If you want a fresh challenge per action, also check login recency via the `auth_time` claim and re-trigger step-up when it is older than your threshold.
+
+
+
+## Full Example: Gating a Sensitive Action
+
+`auth0-server-python` is framework-agnostic - it only reads and writes through the `request` / `response` objects you hand it via `store_options`. The handlers below are plain `async` functions. Wire them into whatever framework you use. They gate a **Transfer Funds** action: if the session has not completed MFA, they return the authorization URL for your app to redirect to, then resume the transfer when the user returns.
+
+```python
+from typing import Optional
+
+from auth0_server_python.auth_server.server_client import ServerClient
+from auth0_server_python.auth_types import StartInteractiveLoginOptions
+
+MFA_ACR = "http://schemas.openid.net/pape/policies/2007/06/multi-factor"
+
+server_client = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ authorization_params={"redirect_uri": "http://localhost:3000/auth/callback"},
+ # ... your configured transaction_store and state_store
+)
+
+
+def mfa_completed(user: Optional[dict]) -> bool:
+ if not user:
+ return False
+ return user.get("acr") == MFA_ACR or "mfa" in (user.get("amr") or [])
+
+
+# `request` and `response` are your framework's objects (or whatever your
+# configured transaction/state store reads cookies from and writes them to).
+async def handle_transfer(request, response):
+ store_options = {"request": request, "response": response}
+
+ user = await server_client.get_user(store_options=store_options)
+ if not mfa_completed(user):
+ # Not stepped up yet - return this URL and have your app redirect to it.
+ return await server_client.start_interactive_login(
+ StartInteractiveLoginOptions(
+ authorization_params={"acr_values": MFA_ACR, "max_age": 0},
+ app_state={"returnTo": "/transfer"},
+ ),
+ store_options=store_options,
+ )
+
+ # MFA satisfied for this session - safe to run the sensitive action.
+ run_transfer() # your own transfer logic
+ return {"status": "transfer complete"}
+
+
+# `callback_url` is the full URL Auth0 redirected back to, including the
+# `?code=...&state=...` query string.
+async def handle_callback(callback_url, request, response):
+ store_options = {"request": request, "response": response}
+
+ result = await server_client.complete_interactive_login(
+ callback_url, store_options=store_options
+ )
+ # Return where to send the user next. Your app performs the redirect.
+ return (result.get("app_state") or {}).get("returnTo", "/")
+```
+
+`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+
+
+The redirect itself is framework-specific - these handlers return the URL to redirect to, and your app issues the actual HTTP redirect (e.g. a `302`/`303`). `max_age: 0` matters: without it, a user who authenticated moments ago may be returned straight to your callback without a fresh MFA prompt.
+
+
+
+
+
+Your tenant must be configured to enforce MFA for the requested policy (see [Configure Step-up Authentication for Web Apps](https://auth0.com/docs/secure/multi-factor-authentication/step-up-authentication/configure-step-up-authentication-for-web-apps)). If it is not, the user returns without `acr` / `amr`, `mfa_completed` stays `False`, and re-running `handle_transfer` redirects them again, so they loop. Guard against this by not re-redirecting when the claim is still absent immediately after a return.
+
+
+
+## Wiring the Redirect (FastAPI)
+
+The handlers above return the URL to redirect to. Your framework issues the redirect. In FastAPI:
+
+```python
+from fastapi.responses import RedirectResponse
+
+@app.post("/transfer")
+async def transfer(request: Request, response: Response):
+ result = await handle_transfer(request, response)
+ # A string is the step-up URL. A dict is the completed action.
+ return RedirectResponse(result, 303) if isinstance(result, str) else result
+
+@app.get("/auth/callback")
+async def callback(request: Request, response: Response):
+ return_to = await handle_callback(str(request.url), request, response)
+ return RedirectResponse(return_to, 303)
+```
diff --git a/docs/dist/examples/UserLinking.mdx b/docs/dist/examples/UserLinking.mdx
new file mode 100644
index 00000000..b499269c
--- /dev/null
+++ b/docs/dist/examples/UserLinking.mdx
@@ -0,0 +1,250 @@
+---
+title: "User Linking"
+---
+
+
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+## Start Linking The User
+
+User‑linking begins with configuring a `redirect_uri`—the URL Auth0 will use to redirect the user after authentication—and then calling `start_link_user()` to obtain an authorization URL.
+
+```python
+# Instantiate the core server client with global authorization parameters.
+server_client = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ authorization_params={
+ "audience": "urn:custom:api",
+ "redirect_uri"="http://localhost:3000/auth/callback",
+ "scope": "your-scopes"
+ }
+)
+
+# Start the link user flow by providing options programmatically.
+options = {
+ "connection": "connection-name",
+ "connection_scope": "connection-scope",
+ "authorization_params": {"redirect_uri": "http://localhost:3000/auth/callback"},
+ "app_state": {"returnTo": "http://localhost:3000"}
+}
+
+# Assume store_options contains Request/Response objects required by the state store.
+store_options = {"request": DummyRequest(), "response": DummyResponse()}
+
+link_user_url = await server_client.start_link_user(options, store_options=store_options)
+
+# Redirect the user to link_user_url
+# (In a FastAPI route, you would return a RedirectResponse with link_user_url)
+
+```
+
+Once the link user flow is completed, the user will be redirected back to the `redirect_uri` specified in the `authorization_params`. At that point, it's required to call `complete_link_user()` to finalize the user-linking process. Read more below in [Complete Linking The User](#complete-linking-the-user).
+
+### Passing `authorization_params`
+
+You can customize the parameters passed to the /authorize endpoint in two ways:
+
+1. **Globally:**
+ Configure them when instantiating the ServerClient:
+ ```python
+ server_client = ServerClient(
+ domain="YOUR_AUTH0_DOMAIN",
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ secret="YOUR_SECRET",
+ authorization_params={
+ "redirect_uri": "http://localhost:3000/auth/callback"
+ "audience": "urn:custom:api",
+ "scope": "openid profile email offline_access"
+ }
+ )
+ ```
+
+2. **Per-call Override:**
+ Supply them when calling `start_link_user()`.
+
+
+ ```python
+ options = {
+ "authorization_params": {
+ "audience": "urn:another:api",
+ "foo": "bar"
+ }
+ }
+ link_user_url = await server_client.start_link_user(options, store_options=store_options)
+ });
+ ```
+
+
+Keep in mind that, any `authorization_params` property specified when calling `start_link_user`, will override the same, statically configured, `authorization_params` property on `ServerClient`.
+
+
+
+
+### Passing App State to track state during login
+
+The `app_state` parameter allows you to pass custom data (for example, a returnTo URL) that will be returned when the linking process is complete.
+
+```python
+options = {
+ "app_state": {"return_to": "http://localhost:3000/dashboard"}
+}
+link_user_url = await server_client.start_link_user(options, store_options=store_options)
+# Later, when completing linking:
+result = await server_client.complete_link_user(callback_url, store_options=store_options)
+print(result.get("app_state").get("return_to")) # Should output "http://localhost:3000/dashboard"
+
+```
+
+
+
+Using `app_state` can be useful for a variaty of reasons, but is mostly supported to enable using a `return_to` parameter in framework-specific SDKs that use this SDK.
+
+
+
+### Passing Store Options
+Every method that interacts with the state or transaction store accepts a second parameter, `store_options`. This parameter should include the HTTP request and response objects (or equivalents) needed to manage cookies or sessions.
+
+```python
+store_options = {"request": request, "response": response}
+link_user_url = await server_client.start_link_user(options, store_options=store_options)
+
+```
+
+Read more above in [Configuring the Transaction and State Store](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+## Complete Linking The User
+
+After the user has been redirected back to your application (at the `redirect_uri`), you need to complete the linking process. This is done by calling `complete_link_user()`, which extracts the necessary parameters from the callback URL and returns the `app_state`.
+
+```python
+# Complete the linking process:
+result = await server_client.complete_link_user(callback_url, store_options=store_options)
+# Retrieve the appState:
+print(result.get("appState").get("return_to"))
+```
+
+
+
+TThe URL passed to `complete_link_user()` should be the full callback URL from Auth0, including the `state` and `code` parameters.
+
+
+
+
+### Passing Store Options
+Just like most methods, `complete_link_user()` accept a second argument that is used to pass to the configured Transaction and State Store:
+
+```python
+store_options = {"request": request, "response": response}
+link_user_url = await server_client.start_link_user(options, store_options=store_options)
+```
+
+Read more above in [Configuring the Transaction and State Store](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+## Start Unlinking The User
+
+User unlinking allows you to remove a previously linked identity from a user account. The process is similar to linking and begins by calling `start_unlink_user()` to obtain an authorization URL.
+
+```python
+# Start the unlink user flow by providing the connection to unlink.
+options = {
+ "connection": "google-oauth2", # The connection to unlink
+ "authorization_params": {"redirect_uri": "http://localhost:3000/auth/callback"},
+ "app_state": {"returnTo": "http://localhost:3000/profile"}
+}
+
+# Assume store_options contains Request/Response objects required by the state store.
+store_options = {"request": request, "response": response}
+
+unlink_user_url = await server_client.start_unlink_user(options, store_options=store_options)
+
+# Redirect the user to unlink_user_url
+# (In a FastAPI route, you would return a RedirectResponse with unlink_user_url)
+```
+
+Once the unlink user flow is completed, the user will be redirected back to the `redirect_uri` specified in the `authorization_params`. At that point, it's required to call `complete_unlink_user()` to finalize the user-unlinking process. Read more below in [Complete Unlinking The User](#complete-unlinking-the-user).
+
+### Passing `authorization_params`
+
+Just like `start_link_user()`, you can customize the parameters passed to the `/authorize` endpoint:
+
+1. **Globally:**
+ Configure them when instantiating the `ServerClient`.
+
+2. **Per-call Override:**
+ Supply them when calling `start_unlink_user()`.
+
+```python
+options = {
+ "connection": "google-oauth2",
+ "authorization_params": {
+ "redirect_uri": "http://localhost:3000/auth/callback",
+ "audience": "urn:custom:api"
+ }
+}
+unlink_user_url = await server_client.start_unlink_user(options, store_options=store_options)
+```
+
+
+
+Any `authorization_params` property specified when calling `start_unlink_user()` will override the same, statically configured, `authorization_params` property on `ServerClient`.
+
+
+
+### Passing App State
+
+The `app_state` parameter allows you to pass custom data (for example, a return URL) that will be returned when the unlinking process is complete.
+
+```python
+options = {
+ "connection": "google-oauth2",
+ "app_state": {"return_to": "http://localhost:3000/profile"}
+}
+unlink_user_url = await server_client.start_unlink_user(options, store_options=store_options)
+
+# Later, when completing unlinking:
+result = await server_client.complete_unlink_user(callback_url, store_options=store_options)
+print(result.get("app_state").get("return_to")) # Should output "http://localhost:3000/profile"
+```
+
+### Passing Store Options
+
+Every method that interacts with the state or transaction store accepts a second parameter, `store_options`. This parameter should include the HTTP request and response objects (or equivalents) needed to manage cookies or sessions.
+
+```python
+store_options = {"request": request, "response": response}
+unlink_user_url = await server_client.start_unlink_user(options, store_options=store_options)
+```
+
+Read more above in [Configuring the Transaction and State Store](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
+
+## Complete Unlinking The User
+
+After the user has been redirected back to your application (at the `redirect_uri`), you need to complete the unlinking process. This is done by calling `complete_unlink_user()`, which extracts the necessary parameters from the callback URL and returns the `app_state`.
+
+```python
+# Complete the unlinking process:
+result = await server_client.complete_unlink_user(callback_url, store_options=store_options)
+
+# Retrieve the app_state:
+print(result.get("app_state").get("return_to"))
+```
+
+
+
+The URL passed to `complete_unlink_user()` should be the full callback URL from Auth0, including the `state` and `code` parameters.
+
+
+
+### Passing Store Options
+
+Just like most methods, `complete_unlink_user()` accepts a second argument that is used to pass to the configured Transaction and State Store:
+
+```python
+store_options = {"request": request, "response": response}
+result = await server_client.complete_unlink_user(callback_url, store_options=store_options)
+```
+
+Read more above in [Configuring the Transaction and State Store](/docs/sdk/python/server/examples/ConfigureStore) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/ConfigureStore.md)).
diff --git a/docs/dist/index.fjson b/docs/dist/index.fjson
new file mode 100644
index 00000000..f18133f9
--- /dev/null
+++ b/docs/dist/index.fjson
@@ -0,0 +1 @@
+{"parents": [], "prev": null, "next": {"link": "reference/auth_server/server_client/", "title": "Server Client"}, "title": "auth0-server-python", "meta": null, "body": "\n
\n:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}\n.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}\nli:has(a[href^=\"/docs/sdk/python/server/\"])>button svg[style*=\"/folder.svg\"]{background-color:var(--sig-default)}\na[href^=\"/docs/sdk/python/server/\"] svg[style*=\"/file-code.svg\"]{background-color:var(--sig-name)}\n\n"}} />
+
+The Auth0 Server Python SDK is a library for implementing user authentication in Python applications.
+
+
+
+[](https://opensource.org/licenses/MIT)
+[](https://deepwiki.com/auth0/auth0-server-python)
+
+📚 [Documentation](#documentation) - 🚀 [Getting Started](#getting-started) - 💬 [Feedback](#feedback)
+
+## Documentation
+
+- [Examples](https://github.com/auth0/auth0-server-python/blob/main/examples) - examples for your different use cases.
+- [Docs Site](https://auth0.com/docs) - explore our docs site and learn more about Auth0.
+
+## Getting Started
+
+### 1. Install the SDK
+
+```shell
+pip install auth0-server-python
+```
+
+If you’re using Poetry:
+
+```shell
+poetry add auth0-server-python
+```
+
+### 2. Create the Auth0 SDK client
+
+Create an instance of the Auth0 client. This instance will be imported and used in anywhere we need access to the authentication methods.
+
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+
+auth0 = ServerClient(
+ domain='',
+ client_id='',
+ client_secret='',
+ secret='',
+ authorization_params= {
+ 'redirect_uri': '',
+ }
+)
+```
+
+The `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET` can be obtained from the [Auth0 Dashboard](https://manage.auth0.com) once you've created an application. **This application must be a `Regular Web Application`**.
+
+The `AUTH0_REDIRECT_URI` tells Auth0 what URL to use while redirecting the user back after successful authentication, e.g. `http://localhost:3000/auth/callback`. Note: your application needs to handle this endpoint and call the SDK's `complete_interactive_login(url: string)` to finish the authentication process. See below for more information.
+
+The `AUTH0_SECRET` is the key used to encrypt the session and transaction cookies. You can generate a secret using `openssl`:
+
+```shell
+openssl rand -hex 64
+```
+
+#### Authenticating with Private Key JWT
+
+The SDK authenticates to Auth0 with either a client secret or a Private Key JWT (`private_key_jwt`). To use Private Key JWT, pass your private signing key as `client_assertion_signing_key` instead of `client_secret`:
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+
+with open('private_key.pem') as f:
+ private_key = f.read()
+
+auth0 = ServerClient(
+ domain='',
+ client_id='',
+ client_assertion_signing_key=private_key,
+ secret='',
+ authorization_params={
+ 'redirect_uri': '',
+ }
+)
+```
+
+The key must be a PKCS8 PEM private key. Register its public key on your Auth0 application under **Settings → Credentials**, and set the application's authentication method to Private Key JWT. The signing algorithm defaults to `RS256` and can be overridden with `client_assertion_signing_alg`. Auth0 accepts `RS256`, `RS384`, and `PS256`, all of which use an RSA key. The algorithm must match the key type and the algorithm chosen when the public key credential was created.
+
+
+
+The passkey challenge endpoints (`register` and `challenge`) accept only a client secret, so a client configured with just a signing key cannot use them.
+
+
+
+
+
+Private keys must not be committed to source control. Load them from a secure secret store or an environment-provided file.
+
+
+
+#### Authenticating with Mutual TLS (mTLS)
+
+The SDK supports mTLS client authentication (RFC 8705): the client presents a TLS certificate during the handshake instead of a client secret. Pass `use_mtls=True` and a caller-built `ssl.SSLContext` that already has the certificate loaded:
+
+```python
+import ssl
+
+ssl_context = ssl.create_default_context()
+ssl_context.load_cert_chain("client.crt", "client.key")
+
+auth0 = ServerClient(
+ domain="login.example.com", # self_managed_certs custom domain
+ client_id="",
+ use_mtls=True,
+ ssl_context=ssl_context,
+ secret="",
+)
+```
+
+`use_mtls=True` requires an Enterprise tenant with the Highly Regulated Identity add-on, a `self_managed_certs` custom domain, and mTLS endpoint aliases enabled. It cannot be combined with `client_secret`, `client_assertion_signing_key`, or a per-call `dpop_key`. Each raises `ConfigurationError`. See the [Auth0 mTLS configuration docs](https://auth0.com/docs/get-started/applications/configure-mtls) for tenant-side setup steps.
+
+See [examples/MutualTLS.md](/docs/sdk/python/server/examples/MutualTLS) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/MutualTLS.md)) for the full setup guide, certificate generation, and token sender-constraining details.
+
+### 3. Add login to your Application (interactive)
+
+Before using redirect-based login, ensure the `redirect_uri` is configured when initializing the SDK:
+
+```python
+auth0 = ServerClient(
+ # ...
+ redirect_uri='',
+ # ...
+)
+```
+
+
+
+You will need to register the `AUTH0_REDIRECT_URI` in your Auth0 Application as an **Allowed Callback URLs** via the [Auth0 Dashboard](https://manage.auth0.com).
+
+
+
+In order to add login to any application, call `start_interactive_login()`, and redirect the user to the returned URL.
+
+The implementation will vary based on the framework being used, but here is an example of what this would look like in FastAPI:
+
+```python
+from fastapi import FastAPI, Request, Response
+from starlette.responses import RedirectResponse
+
+app = FastAPI()
+
+
+@app.get("/auth/login")
+async def login(request: Request):
+ authorization_url = await auth0.start_interactive_login()
+ return RedirectResponse(url=authorization_url)
+```
+
+Once the user has successfully authenticated, Auth0 will redirect the user back to the provided `redirect_uri` which needs to be handled in the application.
+
+This implementation will also vary based on the framework used, but what needs to happen is:
+
+- register an endpoint that will handle the configured `redirect_uri`.
+- call the SDK's `complete_interactive_login(url)`, passing it the full URL, including query parameters.
+
+Here is an example of what this would look like in FastAPI, with `redirect_uri` configured as `http://localhost:3000/auth/callback`:
+
+```python
+@app.get("/auth/callback")
+async def callback(request: Request):
+ result = await auth0.complete_interactive_login(str(request.url))
+ # Store session or set cookies as needed
+ return RedirectResponse(url="/")
+```
+
+#### Organizations
+
+The SDK supports [Auth0 Organizations](https://auth0.com/docs/organizations) with first-class `organization` and `invitation` parameters on `ServerClient` and `StartInteractiveLoginOptions`. Token claim validation is enforced automatically at callback. For dedicated-org and multi-org patterns, invitation flows, error handling, and reading org data from the session, see [examples/OrganizationLogin.md](/docs/sdk/python/server/examples/OrganizationLogin) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/OrganizationLogin.md)).
+
+#### Experiment Center Overrides
+
+[Auth0 Experiment Center](https://auth0.com/docs/customize/experiment-center/overview) runs A/B tests on your login flows and assigns each user to a variation automatically. To force a specific variation for a single login, pass `experiment_id`, `variation_id`, and optionally `segment_id` as authorization params on the `start_interactive_login()` call. Experiment Center is an Enterprise feature. For per-call usage and the segment-targeting variant, see [examples/InteractiveLogin.md](/docs/sdk/python/server/examples/InteractiveLogin#experiment-center-overrides) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/InteractiveLogin.md#experiment-center-overrides)).
+
+### 4. Login with Custom Token Exchange
+
+If you're migrating from a legacy authentication system or integrating with a custom identity provider, you can exchange external tokens for Auth0 tokens using the OAuth 2.0 Token Exchange specification (RFC 8693):
+
+```python
+from auth0_server_python.auth_types import LoginWithCustomTokenExchangeOptions
+
+# Exchange a custom token and establish a session
+result = await auth0.login_with_custom_token_exchange(
+ LoginWithCustomTokenExchangeOptions(
+ subject_token="your-custom-token",
+ subject_token_type="urn:acme:mcp-token",
+ audience="https://api.example.com"
+ ),
+ store_options={"request": request, "response": response}
+)
+
+# Access the user session
+user = result.state_data["user"]
+```
+
+For advanced token exchange scenarios (without creating a session), use `custom_token_exchange()` directly:
+
+```python
+from auth0_server_python.auth_types import CustomTokenExchangeOptions
+
+# Exchange a custom token for Auth0 tokens
+response = await auth0.custom_token_exchange(
+ CustomTokenExchangeOptions(
+ subject_token="your-custom-token",
+ subject_token_type="urn:acme:mcp-token",
+ audience="https://api.example.com",
+ scope="read:data write:data"
+ )
+)
+
+print(response.access_token)
+```
+
+Building on token exchange, the SDK also supports:
+
+- **[Delegation and Impersonation](/docs/sdk/python/server/examples/CustomTokenExchange#3-actor-tokens-delegation) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/CustomTokenExchange.md#3-actor-tokens-delegation))** - exchange with an `actor_token` so the issued tokens record who is acting on whose behalf (the `act` claim).
+- **[Impersonation via Session Transfer (STT)](/docs/sdk/python/server/examples/CustomTokenExchange#8-impersonation-via-session-transfer-stt) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/CustomTokenExchange.md#8-impersonation-via-session-transfer-stt))** - mint a Session Transfer Token to log an agent into a target app as a customer, via `request_session_transfer_token()` and `build_session_transfer_redirect()`.
+
+For more details and examples, see [examples/CustomTokenExchange.md](/docs/sdk/python/server/examples/CustomTokenExchange) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/CustomTokenExchange.md)).
+
+### 5. Multiple Custom Domains (MCD)
+
+For applications that use multiple custom domains on the same Auth0 tenant, pass a domain resolver function instead of a static domain string:
+
+```python
+from auth0_server_python.auth_server.server_client import ServerClient
+from auth0_server_python.auth_types import DomainResolverContext
+
+async def domain_resolver(context: DomainResolverContext) -> str:
+ host = context.request_headers.get('host', '').split(':')[0]
+ domain_map = {
+ "acme.yourapp.com": "acme.auth0.com",
+ "globex.yourapp.com": "globex.auth0.com",
+ }
+ return domain_map.get(host, "default.auth0.com")
+
+auth0 = ServerClient(
+ domain=domain_resolver, # Callable enables MCD mode
+ client_id='',
+ client_secret='',
+ secret='',
+)
+```
+
+The SDK handles per-domain OIDC discovery, JWKS fetching, issuer validation, and session isolation automatically. Static string domains continue to work unchanged.
+
+For more details and examples, see [examples/MultipleCustomDomains.md](/docs/sdk/python/server/examples/MultipleCustomDomains) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/MultipleCustomDomains.md)).
+
+### 6. Session Expiry from the Upstream IdP
+
+For enterprise connections, the upstream identity provider can cap how long a user's session lives. When the connection is configured to honor it, Auth0 includes a `session_expiry` claim in the ID token, and the SDK enforces this ceiling on every session read. Once it is reached, `get_user()` and `get_session()` return `None`, and `get_access_token()` raises an `AccessTokenError` with code `session_expired`. If the asserted ceiling is already in the past at login, `complete_interactive_login()` raises a `SessionExpiredError` instead of persisting an already-expired session.
+
+For more details and examples, see [examples/RetrievingData.md](/docs/sdk/python/server/examples/RetrievingData#session-expiry-from-the-upstream-idp) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/RetrievingData.md#session-expiry-from-the-upstream-idp)).
+
+### 7. Passkey Authentication
+
+Sign users up or in with [WebAuthn](https://www.w3.org/TR/webauthn-2/) passkeys (Touch ID, Face ID, Windows Hello, or a security key) instead of a password, via [Auth0 passkeys](https://auth0.com/docs/authenticate/database-connections/passkeys). The ceremony is two steps — request a challenge, sign it in the browser, then complete sign-in — and establishes a server-side session like every other login path. For the signup and login flows, organizations, step-up MFA, and error handling, see [examples/Passkeys.md](/docs/sdk/python/server/examples/Passkeys) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/Passkeys.md)).
+
+### 8. My Account API — Authentication Methods
+
+Let a logged-in user manage their own enrolled authentication methods — enroll a new passkey (or other factor), list, rename, and delete — via the [My Account API](https://auth0.com/docs/manage-users/my-account-api). For obtaining a scoped token, the enroll/verify ceremony, listing, updating, deleting, and error handling, see [examples/MyAccountAuthenticationMethods.md](/docs/sdk/python/server/examples/MyAccountAuthenticationMethods) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/MyAccountAuthenticationMethods.md)).
+
+### 9. DPoP — Sender-Constrained Tokens (Passkeys & MyAccount)
+
+Bind tokens to a key your server holds ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)) so a stolen token alone cannot be replayed. DPoP is supported for Passkey sign-in (`signin_with_passkey`) and the authentication-methods/factors methods on `MyAccountClient`. For key generation and usage, see [examples/Passkeys.md](/docs/sdk/python/server/examples/Passkeys#3-dpop-bound-passkey-tokens-optional) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/Passkeys.md#3-dpop-bound-passkey-tokens-optional)) and [examples/MyAccountAuthenticationMethods.md](/docs/sdk/python/server/examples/MyAccountAuthenticationMethods#dpop) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/MyAccountAuthenticationMethods.md#dpop)).
+
+### 10. Passwordless Authentication
+
+Sign users in with a one-time code sent by email or SMS, or with a magic link sent by email, via [Auth0 embedded passwordless login](https://auth0.com/docs/authenticate/passwordless/implement-login/embedded-login/relevant-api-endpoints). OTP verification and the magic-link callback each establish a server-side session like every other login path. For prerequisites, both flows, custom scopes/audiences, step-up MFA, and error handling, see [examples/Passwordless.md](/docs/sdk/python/server/examples/Passwordless) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/Passwordless.md)).
+
+### 11. Anonymous Sessions
+
+Give a visitor an Auth0 `anon@` identity before they log in, so cart/preference metadata attached pre-login is available to Post-Login Actions once they do. Requires a separate `anonymous_store` instance, never the same instance as `state_store`, and a tenant-level paid add-on flag. For setup, the token renewal ladder, login injection, and the store-isolation requirement, see [examples/AnonymousSessions.md](/docs/sdk/python/server/examples/AnonymousSessions) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/AnonymousSessions.md)).
+
+### 12. Enterprise Connect (Embedded Login)
+
+Sign users in through their company's identity provider while **your application owns the session**. Opt in with `enterprise_connect=True`; Auth0 acts as a pure SSO relay and issues no refresh token. `start_enterprise_login()` discovers whether an email domain is managed and returns an authorization URL or `None`, and `complete_interactive_login()` returns the verified claims and access token for your app to build its own session from. Early Access. For discovery, the callback contract, multi-tenant `org_id` checks, and federated logout, see [examples/EnterpriseConnect.md](/docs/sdk/python/server/examples/EnterpriseConnect) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/examples/EnterpriseConnect.md)).
+
+## Feedback
+
+### Contributing
+
+We appreciate feedback and contribution to this repo! Before you get started, please read the following:
+
+- [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
+- [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
+- [This repo's contribution guide](/docs/sdk/python/server/CONTRIBUTING) ([GitHub](https://github.com/auth0/auth0-server-python/blob/main/CONTRIBUTING.md))
+
+### Raise an issue
+
+To provide feedback or report a bug, please [raise an issue on our issue tracker](https://github.com/auth0/auth0-server-python/issues).
+
+## Vulnerability Reporting
+
+Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues.
+
+## What is Auth0?
+
+
+ Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout Why Auth0?
+
+
+ This project is licensed under the MIT license. See the LICENSE file for more info.
+
To implement a custom authentication scheme, subclass Auth and override\nthe .auth_flow() method.
\n
If the authentication scheme does I/O such as disk access or network calls, or uses\nsynchronization primitives such as locks, you should override .sync_auth_flow()\nand/or .async_auth_flow() instead of .auth_flow() to provide specialized\nimplementations that will be used by Client and AsyncClient respectively.
To implement a custom authentication scheme, subclass Auth and override\nthe .auth_flow() method.
\n
If the authentication scheme does I/O such as disk access or network calls, or uses\nsynchronization primitives such as locks, you should override .sync_auth_flow()\nand/or .async_auth_flow() instead of .auth_flow() to provide specialized\nimplementations that will be used by Client and AsyncClient respectively.
Build a DPoP proof JWT for use at the token endpoint (RFC 9449 \u00a74.2).\nUnlike resource-server proofs, token-endpoint proofs do NOT include ath\nbecause no access token exists yet at issuance time.
options \u2013 Optional bundle of audience/scope/metadata, accepted as a\nCreateAnonymousSessionOptions or a plain dict. Explicit keyword\narguments below always win over the same field on options.
\n
audience \u2013 Audience for the session. Falls back to options.audience,\nthen to the client\u2019s configured default, when omitted.
\n
scope \u2013 Scope for the session. Falls back to options.scope, then to\nthe client\u2019s configured default, when omitted.
\n
metadata \u2013 Metadata to attach at creation, up to 1KB. Cannot be\nchanged after creation. Falls back to options.metadata.
\n
store_options \u2013 Options passed to the anonymous store.
Return stored anonymous session identity without calling Auth0.
\n
\n
Parameters:
\n
store_options \u2013 Options passed to the anonymous store.
\n
\n
Returns:
\n
The stored AnonymousSessionData, or None when there is no session,\nthe session is corrupt, or (in resolver mode) the stored domain\ndoes not match the current tenant.
Provides methods for listing authenticators, enrolling new authenticators,\ndeleting authenticators, challenging authenticators, and verifying MFA codes.
\n
All public API methods accept an encrypted mfa_token (as issued by\nMfaRequiredError) and decrypt it internally. Callers never handle the\nraw Auth0 mfa_token directly.
Verifies an MFA code and completes authentication.
\n
Supports OTP, OOB (with binding code), and recovery code verification.
\n
If Auth0 returns \u2018mfa_required\u2019 again (chained MFA), raises MfaRequiredError\nwith a raw mfa_token. The framework SDK (e.g. auth0-fastapi) is responsible\nfor encrypting the token before returning it to the client.
\n
\n
Parameters:
\n
\n
options \u2013 Dict containing \u2018mfa_token\u2019 and one of:\n- \u2018otp\u2019: OTP code\n- \u2018oob_code\u2019 + \u2018binding_code\u2019: OOB verification\n- \u2018recovery_code\u2019: Recovery code\n- \u2018persist\u2019: bool (optional, default=False) - Persist tokens to state store\n- \u2018audience\u2019: str (optional, required if persist=True) - Audience for token_set\n- \u2018scope\u2019: str (optional) - Scope for token_set
\n
store_options \u2013 Optional options passed to the State Store (e.g. request/response).
\n
dpop_key \u2013 Optional EC P-256 JWK to DPoP-bind the token. Pass the same\nkey used at login (e.g. given to signin_with_passkey) to preserve\nthe sender constraint through step-up. Never stored by the SDK.
\n
\n
\n
Returns:
\n
MfaVerifyResponse with access_token, token_type, etc.
\n
\n
Raises:
\n
\n
MfaVerifyError \u2013 When verification fails, or when dpop_key was supplied\nbut the server returned an unbound (Bearer) token.
Step 1 of 2: Start enrollment (POST /me/v1/authentication-methods).
\n
For passkey enrollment, pass the returned authn_params_public_key to\nnavigator.credentials.create(), then call verify_authentication_method()\nwith the auth_session and credential result.
Implements embedded passwordless login for a client:
\n
\n
Email OTP / SMS OTP - start() sends a code, verify() exchanges it for\ntokens via the passwordless-OTP grant and establishes a server-side session.
\n
Magic link - start(send="link") emails a one-click link. Completion is\nhandled by the standard callback (ServerClient.complete_interactive_login),\nnot by verify().
\n
\n
Tokens never leave the server. The browser holds only the opaque session.
store_options \u2013 Options passed to the transaction store (e.g.\nrequest/response). Required for the magic-link flow so the\ntransaction cookie can be written.
\n
\n
\n
Returns:
\n
PasswordlessStartResult with Auth0\u2019s start response payload.
InvalidArgumentError \u2013 When options is not a recognized type, or\ncaller auth_params contains an SDK-owned or unrecognized key.
\n
MissingRequiredArgumentError \u2013 When a magic link is requested but no\nredirect_uri is configured on the client, or store_options\nis not provided.
This method starts a Client-Initiated Backchannel Authentication (CIBA) flow,\nwhich allows an application to request authentication from a user via a separate\ndevice or channel.
\n
Then polls the token endpoint until the user has authenticated or the request times out.
\n
\n
Parameters:
\n
options (dict) \u2013
Configuration options for backchannel authentication\n- login_hint (dict): Must contain a \u2018sub\u2019 field (e.g., {\u2018sub\u2019: \u2018user_id\u2019}).\n- binding_message (str, optional): Message to display to the user.\n- authorization_params (dict, optional): Additional authorization parameters.
\n
\n
\n
requested_expiry (int, optional): Requested expiry time in seconds, default is 30 secs.
\n
authorization_details (str, optional): JSON string for RAR.
\n
\n
\n\n
\n
Returns:
\n
Token response data from the backchannel authentication
\n
\n
Raises:
\n
\n
ApiError \u2013 If the backchannel authentication fails
Builds the redirect URL that hands the STT to the target app\u2019s login URL.
\n
target_login_url must be a trusted, app-controlled absolute https URL (http is allowed\nonly for localhost/loopback) - the STT is a single-use credential and must not leak to an\nuntrusted host.
\n
\n
Parameters:
\n
\n
target_login_url \u2013 The target app\u2019s login URL (absolute, https)
\n
result \u2013 The SessionTransferTokenResult from request_session_transfer_token
\n
organization \u2013 Organization identifier to forward (optional)
\n
\n
\n
Returns:
\n
A URL string with session_transfer_token (and organization) as query parameters
Handles the redirect callback to complete the connect account flow for linking a third-party\naccount to the user\u2019s profile.
\n
This works similar to the redirect from the login flow except it verifies the connect_code\nwith the My Account API rather than the code with the Authorization Server.
\n
\n
Parameters:
\n
\n
url \u2013 The full callback URL including query parameters
\n
store_options \u2013 Optional options used to pass to the Transaction and State Store.
Retrieves the access token from the store, or calls Auth0 when the access token\nis expired and a refresh token is available in the store.\nAlso updates the store when a new token was retrieved from Auth0.
\n
\n
Parameters:
\n
store_options \u2013 Optional options used to pass to the Transaction and State Store.
\n
\n
Returns:
\n
The access token, retrieved from the store or Auth0.
\n
\n
Raises:
\n
\n
AccessTokenError \u2013 If the token is expired and no refresh token is available.
This method attempts to obtain an access token for a specified connection.\nIt first checks if a refresh token exists in the store.\nIf no refresh token is found, it throws an AccessTokenForConnectionError indicating\nthat the refresh token was not found.
\n
\n
Parameters:
\n
\n
options \u2013 Options for retrieving an access token for a connection.
\n
store_options \u2013 Optional options used to pass to the Transaction and State Store.
\n
\n
\n
Returns:
\n
The access token for the connection
\n
\n
Raises:
\n
\n
AccessTokenForConnectionError \u2013 If the access token was not found or\nthere was an issue requesting the access token.
options \u2013 Options for retrieving an access token for a connection.\nMust include \u2018connection\u2019 and \u2018refresh_token\u2019 keys.\nMay optionally include \u2018login_hint\u2019.
This method starts a Client-Initiated Backchannel Authentication (CIBA) flow,\nwhich allows an application to request authentication from a user via a separate\ndevice or channel.
\n
\n
Parameters:
\n
options (dict) \u2013
Configuration options for backchannel authentication\n- login_hint (dict): Must contain a \u2018sub\u2019 field (e.g., {\u2018sub\u2019: \u2018user_id\u2019}).\n- binding_message (str, optional): Message to display to the user.\n- authorization_params (dict, optional): Additional authorization parameters.
\n
\n
\n
requested_expiry (int, optional): Requested expiry time in seconds, default is 30 secs.
\n
authorization_details (str, optional): JSON string for RAR.
\n
\n
\n\n
\n
Returns:
\n
\n
Response data from the bc-authorize backchannel authentication
\n
auth_req_id (str): The authentication request ID.
\n
expires_in (int): Time in seconds until the request expires.
\n
interval (int, optional): Polling interval in seconds.
\n
\n
\n
\n\n
\n
Return type:
\n
dict
\n
\n
Raises:
\n
ApiError \u2013 If the backchannel authentication fails
Retrieves a list of available connections that can be used connected accounts for the authenticated user.
\n
\n
Parameters:
\n
\n
from_param (Optional[str],optional) \u2013 A pagination token indicating where to start retrieving results, obtained from a prior request. Defaults to None.
\n
take (Optional[int],optional) \u2013 The maximum number of connections to retrieve. Defaults to None.
\n
store_options \u2013 Optional options used to pass to the Transaction and State Store.
\n
\n
\n
Returns:
\n
The response object containing the list of connected account connections.
Retrieves a list of connected accounts for the authenticated user.
\n
\n
Parameters:
\n
\n
connection (Optional[str],optional) \u2013 Filter results to a specific connection. Defaults to None.
\n
from_param (Optional[str],optional) \u2013 A pagination token indicating where to start retrieving results, obtained from a prior request. Defaults to None.
\n
take (Optional[int],optional) \u2013 The maximum number of connections to retrieve. Defaults to None.
\n
store_options \u2013 Optional options used to pass to the Transaction and State Store.
\n
\n
\n
Returns:
\n
The response object containing the list of connected accounts.
Step 1 of 2: Initiate a passkey login challenge (POST /passkey/challenge).
\n
Pass the returned authn_params_public_key to navigator.credentials.get(),\nthen call signin_with_passkey() with the auth_session and credential result.
\n
\n
Parameters:
\n
\n
username \u2013 Deprecated and ignored. Auth0 rejects username on the\npasskey login challenge (the user is identified by the selected\ncredential, not by a supplied username), so it is never forwarded.\nRetained for backward compatibility and will be removed in a future\nmajor release. Passing it emits a DeprecationWarning.
\n
connection \u2013 Auth0 database connection name (realm).
\n
organization \u2013 Auth0 organization ID or name.
\n
store_options \u2013 Optional options for domain resolution.
\n
\n
\n
Returns:
\n
PasskeyLoginChallengeResponse with auth_session and authn_params_public_key.
\n
\n
Raises:
\n
\n
PasskeyError \u2013 If the challenge request fails.
Step 1 of 2: Initiate a passkey signup challenge (POST /passkey/register).
\n
Pass the returned authn_params_public_key to navigator.credentials.create(),\nthen call signin_with_passkey() with the auth_session and credential result.
\n
\n
Parameters:
\n
\n
user_profile \u2013 Optional user profile data (email, name, username, etc.).\nUse PasskeyUserProfile \u2014 supports extra fields for forward compatibility.
\n
connection \u2013 Auth0 database connection name (realm).
\n
organization \u2013 Auth0 organization ID or name.
\n
user_metadata \u2013 Optional custom metadata added at the root of the request body,\nnot nested inside user_profile (per Auth0 API spec).
\n
store_options \u2013 Optional options for domain resolution.
\n
\n
\n
Returns:
\n
PasskeySignupChallengeResponse with auth_session and authn_params_public_key.
\n
\n
Raises:
\n
\n
PasskeyError \u2013 If the challenge request fails.
Requests a Session Transfer Token (STT) for impersonation via session transfer.
\n
Performs a custom token exchange against the session_transfer audience. The returned\nSTT is opaque and single-use; hand it to build_session_transfer_redirect and do not\ndecode or store it. The act claim is not on this result.
\n
\n
Parameters:
\n
\n
subject_token \u2013 Your proof of which customer to impersonate (validated by your Action)
\n
subject_token_type \u2013 The subject token type URI routing to your CTE Profile
\n
actor_token \u2013 The acting party\u2019s token; optional. Defaults to the agent session\u2019s ID token
\n
actor_token_type \u2013 Type URI of the actor token; defaults to the ID token URN
\n
scope \u2013 Space-delimited list of scopes (optional)
Completes passkey authentication by exchanging the WebAuthn assertion\nfor tokens and establishing a server-side session.
\n
This is step 2 of 2: call passkey_signup_challenge or passkey_login_challenge\nfirst to obtain auth_session and the WebAuthn challenge options.
\n
\n
Parameters:
\n
\n
auth_session \u2013 Session credential from passkey_signup_challenge or passkey_login_challenge.
\n
authn_response \u2013 Serialized WebAuthn credential from navigator.credentials.create/get.
\n
store_options \u2013 Options passed to the state store (e.g., request/response for cookies).\nPassed through to the store on every call.
\n
connection \u2013 Auth0 database connection name (realm).
\n
organization \u2013 Auth0 organization ID or name.
\n
scope \u2013 OAuth2 scope string.
\n
audience \u2013 Target API audience.
\n
dpop_key \u2013 Optional EC P-256 JWK for DPoP-bound token exchange. When provided,\nattaches a DPoP proof header so Auth0 issues a DPoP-bound token\n(token_type: DPoP). Required when the tenant mandates DPoP binding.
\n
\n
\n
Returns:
\n
PasskeyLoginResult containing state_data with user claims and token sets,\nconsistent with complete_interactive_login and login_with_custom_token_exchange.
ConfigurationError \u2013 If dpop_key is combined with use_mtls. DPoP and mTLS\nuse incompatible token-binding mechanisms and cannot be used together.
\n
PasskeyError \u2013 If token exchange or session creation fails.
\n
OrganizationTokenValidationError \u2013 If an organization was requested but the\ntoken response included no ID token, or the ID token\u2019s org claim does\nnot match.
Initiates the connect account flow for linking a third-party account to the user\u2019s profile.
\n
This method generates PKCE parameters, creates a transaction and calls the My Account API\nto create a connect account request, returning /connect url containing a ticket.
\n
\n
Parameters:
\n
\n
options \u2013 Options for retrieving an access token for a connection.
\n
store_options \u2013 Optional options used to pass to the Transaction and State Store.
\n
\n
\n
Returns:
\n
The a connect URL containing a ticket to redirect the user to.
Begin an Enterprise Connect login from an email address.
\n
Runs WebFinger discovery on the email domain. When the domain is managed,\nbuilds an authorization URL with the email as login_hint so Auth0 can\nresolve the connection and organization. When it is not managed, returns\nNone so the caller can fall back to its own login. A static client-level\norganization is never forwarded - Auth0 resolves it from the email.
\n
\n
Parameters:
\n
\n
options \u2013 Enterprise login options carrying the user\u2019s email.
\n
store_options \u2013 Per-request store options threaded through the flow.
\n
\n
\n
Returns:
\n
The authorization URL to redirect to, or None when the domain is not\nmanaged by Auth0.
Check whether an email domain is Auth0-managed for enterprise SSO via WebFinger.
\n
A stateless routing hint, not an authorization decision. Fails closed to False\non any error or non-200/non-404 response. Prefer ServerClient in normal\nuse, which caches results and resolves the domain per request. This standalone\nform is for callers that need a one-off check without a client instance.
\n
\n
Parameters:
\n
\n
domain \u2013 The Auth0 domain to query.
\n
email_domain \u2013 The email domain to check (case-insensitive).
\n
timeout \u2013 Per-request timeout in seconds.
\n
\n
\n
Returns:
\n
True only when Auth0 reports the domain as a managed OIDC issuer.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
\n
\n\n
\n
\naaguid:str|None=None
\n
\n\n
\n
\nconfirmed:bool|None=None
\n
\n\n
\n
\ncreated_at:str
\n
\n\n
\n
\ncredential_backed_up:bool|None=None
\n
\n\n
\n
\ncredential_device_type:str|None=None
\n
\n\n
\n
\nemail:str|None=None
\n
\n\n
\n
\nid:str
\n
\n\n
\n
\nidentity_user_id:str|None=None
\n
\n\n
\n
\nkey_id:str|None=None
\n
\n\n
\n
\nlast_password_reset:str|None=None
\n
\n\n
\n
\nmodel_config
\n
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
\n
\n\n
\n
\nmodel_config
\n
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
\n
\n\n
\n
\nmodel_config
\n
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
Contains the session data established after the webauthn token exchange.\nMirrors LoginWithCustomTokenExchangeResult \u2014 passkey sign-in is a complete\nlogin ceremony and creates a server-side session like every other login path.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
\n
\n\n
\n
\naccess_token:str
\n
\n\n
\n
\nexpires_at:int
\n
\n\n
\n
\nexpires_in:int
\n
\n\n
\n
\nid_token:str|None=None
\n
\n\n
\n
\nmodel_config
\n
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
\n
\n\n
\n
\ndisplay_name:str|None=None
\n
\n\n
\n
\nid:str
\n
\n\n
\n
\nmodel_config
\n
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
\n
\n\n
\n
\nemail:str|None=None
\n
\n\n
\n
\nfamily_name:str|None=None
\n
\n\n
\n
\ngiven_name:str|None=None
\n
\n\n
\n
\nmodel_config
\n
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
The names of the class variables defined on the model.
\n
\n\n
\n
\n__private_attributes__
\n
Metadata about the private attributes of the model.
\n
\n\n
\n
\n__signature__
\n
The synthesized __init__ [Signature][inspect.Signature] of the model.
\n
\n\n
\n
\n__pydantic_complete__
\n
Whether model building is completed, or if there are still undefined fields.
\n
\n\n
\n
\n__pydantic_core_schema__
\n
The core schema of the model.
\n
\n\n
\n
\n__pydantic_custom_init__
\n
Whether the model has a custom __init__ function.
\n
\n\n
\n
\n__pydantic_decorators__
\n
Metadata containing the decorators defined on the model.\nThis replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
\n
\n\n
\n
\n__pydantic_generic_metadata__
\n
A dictionary containing metadata about generic Pydantic models.\nThe origin and args items map to the [__origin__][genericalias.__origin__]\nand [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],\nand the parameter item maps to the __parameter__ attribute of generic classes.
\n
\n\n
\n
\n__pydantic_parent_namespace__
\n
Parent namespace of the model, used for automatic rebuilding of models.
\n
\n\n
\n
\n__pydantic_post_init__
\n
The name of the post-init method for the model, if defined.
\n
\n\n
\n
\n__pydantic_root_model__
\n
Whether the model is a [RootModel][pydantic.root_model.RootModel].
\n
\n\n
\n
\n__pydantic_serializer__
\n
The pydantic-core SchemaSerializer used to dump instances of the model.
\n
\n\n
\n
\n__pydantic_validator__
\n
The pydantic-core SchemaValidator used to validate instances of the model.
\n
\n\n
\n
\n__pydantic_fields__
\n
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
\n
\n\n
\n
\n__pydantic_computed_fields__
\n
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
\n
\n\n
\n
\n__pydantic_extra__
\n
A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra]\nis set to \u2018allow\u2019.
\n
\n\n
\n
\n__pydantic_fields_set__
\n
The names of fields explicitly set during instantiation.
\n
\n\n
\n
\n__pydantic_private__
\n
Values of private attributes set on the model instance.
Error raised when token issuer validation fails.\nThis can happen when the issuer claim in a token does not match\nthe expected issuer for the configured domain.
Error thrown when MFA step-up is required during token refresh.
\n
Contains an encrypted mfa_token that can be passed to MfaClient methods.\nThis error is raised in get_access_token() when the token endpoint returns\n\u2018mfa_required\u2019.
Error raised when a required transaction is missing.\nThis typically happens during the callback phase when the transaction\nfrom the initial authorization request cannot be found.
Error raised when user linking process fails to start.\nThis typically happens when trying to link accounts without\nhaving an authenticated user first.
Per the OIDC Back-Channel Logout spec, either sid or sub\n(or both) will be present. Implementations should match on\nwhichever claim is present (OR logic), not require both (AND).
\n
When iss is present in claims (always included by the SDK),\nimplementations should validate that the token\u2019s issuer matches\nthe session\u2019s stored domain before deleting. This prevents\ncross-domain session deletion in MCD deployments.
\n
\n
Parameters:
\n
\n
claims \u2013
Claims from the logout token. Keys:\n- sub: Subject identifier of the user\n- sid: Session identifier\n- iss: Token issuer (always included; compare against
True when the session ceiling is already in the past at login.
\n
Compares the ceiling against the ID token iat, or wall-clock now when\niat is absent, using the same leeway as is_session_ceiling_reached. A\nNone ceiling means none was asserted and is never treated as expired.
True when the session ceiling has been reached (applying negative\nleeway for clock skew). None means no ceiling was asserted, so the\nsession is never expired on this basis.
\n\n\n";
+
+
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 00000000..7d21decf
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,24 @@
+auth0-server-python
+===================
+
+Auth0 server-side Python SDK: authentication flows, token management, and session handling
+for web applications.
+
+.. toctree::
+ :maxdepth: 1
+ :caption: API Reference
+
+ reference/auth_server/server_client/index
+ reference/auth_server/anonymous/client/index
+ reference/auth_server/mfa_client/index
+ reference/auth_server/my_account_client/index
+ reference/auth_server/passwordless_client/index
+ reference/auth_schemes/bearer_auth/index
+ reference/auth_schemes/dpop_auth/index
+ reference/auth_schemes/client_assertion/index
+ reference/auth_types/index
+ reference/store/abstract/index
+ reference/encryption/encrypt/index
+ reference/error/index
+ reference/utils/helpers/index
+ reference/telemetry/index
diff --git a/docs/mintlify_export/README.md b/docs/mintlify_export/README.md
new file mode 100644
index 00000000..87444873
--- /dev/null
+++ b/docs/mintlify_export/README.md
@@ -0,0 +1,43 @@
+# mintlify_export
+
+Sphinx extension that post-processes the `json` builder output into docs-site
+MDX pages and a `navigation.json`.
+
+It uses only the standard library and imports nothing from the SDK or from
+`conf.py`. Every project-specific value is a `mintlify_*` config value, so the
+folder can be copied to another repo unchanged.
+
+## What it does on `build-finished` (json builder only)
+
+1. Converts Markdown guides (README, CONTRIBUTING, examples) to MDX. GitHub
+ alerts become callouts. A relative link to another guide opens that guide's page and
+ is followed by a GitHub link. Any other relative link points at `mintlify_github_blob_url`.
+2. Removes unwanted reference pages.
+3. Rewrites reference cross-links to absolute page URLs under `mintlify_site_dir`.
+4. Writes each reference page as MDX and removes its `.fjson`. Pages are titled by
+ module path, with a module icon. Code blocks get a copy button.
+5. Writes `navigation.json` from `mintlify_navigation`, filling the `{reference}` placeholder
+ with a tree that mirrors the package hierarchy: a package or subpackage becomes a group
+ named after it, a module becomes a page. Groups sort before pages, each by name.
+
+## Adding docs
+
+Nothing to edit. New modules, packages and classes are picked up from the
+generated pages, and new guides from the Markdown files in the guides folder.
+Titles, sidebar labels, icons, colors and navigation groups are all derived.
+
+## Config values
+
+| Name | Meaning |
+|---|---|
+| `mintlify_site_dir` | Page path prefix in the docs site |
+| `mintlify_navigation` | Navigation tree written to `navigation.json`, with a `{reference}` placeholder |
+| `mintlify_guides` | Output page to `(markdown path, fixed title or "")` |
+| `mintlify_guides_root` | Absolute folder the guide paths are relative to |
+| `mintlify_github_blob_url` | Base URL for relative Markdown links |
+| `mintlify_remove_pages` | Reference page paths to drop |
+
+## Moving it out of this repo
+
+Copy this folder next to the consuming `conf.py`, add its parent to `sys.path`,
+list `"mintlify_export"` in `extensions`, and set the config values above.
diff --git a/docs/mintlify_export/__init__.py b/docs/mintlify_export/__init__.py
new file mode 100644
index 00000000..cef32a90
--- /dev/null
+++ b/docs/mintlify_export/__init__.py
@@ -0,0 +1,57 @@
+"""Sphinx extension that turns the ``json`` build into docs-site MDX pages.
+
+Self-contained: standard library only, no imports from the SDK or from conf.py.
+All project-specific values come from the ``mintlify_*`` config values below,
+so the folder can be moved to another repo as is.
+"""
+
+import json
+import os
+
+from . import guides, navigation, reference
+
+__all__ = ["setup"]
+
+
+def _on_build_finished(app, exception):
+ if exception or app.builder.name != "json":
+ return
+
+ cfg = app.config
+ out = app.outdir
+
+ guides.write_pages(
+ out,
+ cfg.mintlify_guides_root,
+ cfg.mintlify_guides,
+ cfg.mintlify_github_blob_url,
+ cfg.mintlify_site_dir,
+ )
+ reference.remove_pages(out, cfg.mintlify_remove_pages)
+ reference.absolutize_links(out, cfg.mintlify_site_dir)
+ pages = reference.write_mdx(out, cfg.mintlify_site_dir)
+ tree = navigation.reference_tree(cfg.mintlify_site_dir, pages)
+
+ nav_path = os.path.join(out, "navigation.json")
+ with open(nav_path, "w") as f:
+ json.dump({"pages": navigation.fill(cfg.mintlify_navigation, tree)}, f, indent=2)
+
+ print(f"[mintlify_export] Wrote {nav_path}")
+
+
+def setup(app):
+ # Directory prefix of every page in the docs site, e.g. "docs/sdk/python/server".
+ app.add_config_value("mintlify_site_dir", "", "")
+ # Navigation tree written to navigation.json. The string "{reference}" is replaced by
+ # the API reference groups, which are built from the generated pages.
+ app.add_config_value("mintlify_navigation", [], "")
+ # Output page path -> (Markdown path relative to mintlify_guides_root, fixed title or "").
+ app.add_config_value("mintlify_guides", {}, "")
+ # Absolute path of the folder holding the guide Markdown files.
+ app.add_config_value("mintlify_guides_root", "", "")
+ # Base URL that relative Markdown links are rewritten to.
+ app.add_config_value("mintlify_github_blob_url", "", "")
+ # Reference page paths (no extension) to drop.
+ app.add_config_value("mintlify_remove_pages", [], "")
+ app.connect("build-finished", _on_build_finished)
+ return {"parallel_read_safe": True, "parallel_write_safe": True}
diff --git a/docs/mintlify_export/guides.py b/docs/mintlify_export/guides.py
new file mode 100644
index 00000000..ff165f1e
--- /dev/null
+++ b/docs/mintlify_export/guides.py
@@ -0,0 +1,114 @@
+"""Convert repository Markdown (README, CONTRIBUTING, guides) into MDX pages."""
+
+import os
+import posixpath
+import re
+
+from .style import as_mdx_element, guide_css
+
+# GitHub alert type to callout component.
+ALERT_COMPONENTS = {
+ "NOTE": "Note",
+ "TIP": "Tip",
+ "IMPORTANT": "Warning",
+ "WARNING": "Warning",
+ "CAUTION": "Warning",
+}
+
+
+def _toggles_fence(line):
+ return re.match(r"[>\s]*(```|~~~)", line) is not None
+
+
+def _convert_alerts(lines):
+ out, in_fence, i = [], False, 0
+ while i < len(lines):
+ line = lines[i]
+ if _toggles_fence(line):
+ in_fence = not in_fence
+ match = None if in_fence else re.match(r">\s*\[!(\w+)\]\s*(.*)$", line)
+ if not match or match.group(1) not in ALERT_COMPONENTS:
+ out.append(line)
+ i += 1
+ continue
+ tag = ALERT_COMPONENTS[match.group(1)]
+ body = [match.group(2).strip()] if match.group(2).strip() else []
+ i += 1
+ while i < len(lines) and lines[i].startswith(">"):
+ body.append(re.sub(r"^>\s?", "", lines[i]).rstrip())
+ i += 1
+ out += [f"<{tag}>", "", *body, "", f"{tag}>"]
+ return out
+
+
+def _rewrite_links(lines, source_dir, blob_url, guide_urls):
+ def repl(match):
+ href = match.group(1)
+ if re.match(r"([a-z][a-z0-9+.-]*:|#|/)", href, re.IGNORECASE):
+ return match.group(0)
+ path, sep, anchor = href.partition("#")
+ target = posixpath.normpath(posixpath.join(source_dir, path))
+ github = f"{blob_url}/{target}{sep}{anchor}"
+ site = guide_urls.get(target)
+ if site:
+ return f"]({site}{sep}{anchor}) ([GitHub]({github}))"
+ return f"]({github})"
+
+ out, in_fence = [], False
+ for line in lines:
+ if _toggles_fence(line):
+ in_fence = not in_fence
+ out.append(line)
+ else:
+ out.append(line if in_fence else re.sub(r"\]\(([^)\s]+)\)", repl, line))
+ return out
+
+
+def markdown_to_mdx(md, title, source_dir, blob_url, guide_urls, css):
+ """Return an MDX page for ``md``.
+
+ ``source_dir`` is the Markdown file's directory relative to the repo root.
+ A relative link to another guide points at the guide's page, followed by a
+ GitHub link. Any other relative link points at ``blob_url``.
+ """
+ # The logo block uses /, which MDX cannot parse.
+ md = re.sub(
+ r'
\s*.*?\s*
\s*', "", md, flags=re.DOTALL
+ )
+ lines = md.split("\n")
+
+ first = next((i for i, line in enumerate(lines) if line.strip()), None)
+ if first is not None and re.match(r"# \S", lines[first]):
+ title = title or lines[first][2:].strip()
+ del lines[first]
+
+ lines = _rewrite_links(_convert_alerts(lines), source_dir, blob_url, guide_urls)
+ safe_title = title.replace('"', '\\"')
+ body = "\n".join(lines).strip()
+ return f'---\ntitle: "{safe_title}"\n---\n\n{as_mdx_element(css)}\n\n{body}\n'
+
+
+def write_pages(out, root, pages, blob_url, site_dir):
+ """Write one MDX page per entry of ``pages`` under ``out``.
+
+ ``pages`` maps an output page path to ``(markdown path relative to root,
+ fixed title or "")``. ``site_dir`` is the page path prefix in the docs site.
+ """
+ guide_urls = {
+ source: f"/{site_dir}/{page_path}" for page_path, (source, _) in pages.items()
+ }
+ css = guide_css(site_dir)
+ for page_path, (source, title) in pages.items():
+ with open(os.path.join(root, source)) as f:
+ md = f.read()
+ if not title and not re.match(r"# \S", md.lstrip()):
+ stem = posixpath.splitext(posixpath.basename(source))[0]
+ title = re.sub(r"(?<=[a-z])(?=[A-Z])", " ", stem)
+ mdx_path = os.path.join(out, page_path + ".mdx")
+ os.makedirs(os.path.dirname(mdx_path), exist_ok=True)
+ with open(mdx_path, "w") as f:
+ f.write(
+ markdown_to_mdx(
+ md, title, posixpath.dirname(source), blob_url, guide_urls, css
+ )
+ )
diff --git a/docs/mintlify_export/navigation.py b/docs/mintlify_export/navigation.py
new file mode 100644
index 00000000..f517090e
--- /dev/null
+++ b/docs/mintlify_export/navigation.py
@@ -0,0 +1,60 @@
+"""Build the API reference part of the navigation from the generated pages."""
+
+from .style import GROUP_ICON
+
+PLACEHOLDER = "{reference}"
+
+
+def page_parts(page):
+ """Module path segments of a reference page, e.g. ``["auth_server", "mfa_client"]``."""
+ parts = page.split("/")[1:]
+ return parts[:-1] if parts and parts[-1] == "index" else parts
+
+
+def _items(site_dir, children):
+ groups, leaves = [], []
+ for name in sorted(children):
+ entry = children[name]
+ if not entry["children"]:
+ leaves.append(f"{site_dir}/{entry['page']}")
+ continue
+ group = {
+ "group": name,
+ "icon": GROUP_ICON,
+ "expanded": True,
+ "pages": _items(site_dir, entry["children"]),
+ }
+ if entry["page"]:
+ group["root"] = f"{site_dir}/{entry['page']}"
+ groups.append(group)
+ return groups + leaves
+
+
+def reference_tree(site_dir, pages):
+ """Mirror the package hierarchy of the reference pages.
+
+ A package or subpackage with pages below it becomes a group named after it,
+ and a module becomes a page. Groups sort before pages, each by name, as in
+ a file browser.
+ """
+ root = {}
+ for page in pages:
+ children = root
+ for name in page_parts(page)[:-1]:
+ children = children.setdefault(name, {"page": None, "children": {}})["children"]
+ leaf = children.setdefault(page_parts(page)[-1], {"page": None, "children": {}})
+ leaf["page"] = page
+ return _items(site_dir, root)
+
+
+def fill(nodes, items):
+ """Replace ``PLACEHOLDER`` in a navigation tree with ``items``."""
+ out = []
+ for node in nodes:
+ if node == PLACEHOLDER:
+ out.extend(items)
+ elif isinstance(node, dict):
+ out.append({**node, "pages": fill(node["pages"], items)})
+ else:
+ out.append(node)
+ return out
diff --git a/docs/mintlify_export/reference.py b/docs/mintlify_export/reference.py
new file mode 100644
index 00000000..ae0523b5
--- /dev/null
+++ b/docs/mintlify_export/reference.py
@@ -0,0 +1,130 @@
+"""Post-process Sphinx JSON reference pages into MDX pages."""
+
+import json
+import os
+import posixpath
+import re
+
+from .navigation import page_parts
+from .style import MODULE_ICON, as_html_tag, reference_css
+
+
+def _load(path):
+ with open(path) as f:
+ return json.load(f)
+
+
+def _dump(path, data):
+ with open(path, "w") as f:
+ json.dump(data, f, indent=2)
+
+
+def _page_files(out):
+ for root, _, files in os.walk(os.path.join(out, "reference")):
+ for name in files:
+ if name.endswith(".fjson"):
+ yield os.path.join(root, name)
+
+
+def remove_pages(out, pages):
+ for page_path in pages:
+ fpath = os.path.join(out, page_path + ".fjson")
+ if os.path.exists(fpath):
+ os.remove(fpath)
+
+
+def _page_slug(out, resolved):
+ for slug in (resolved, resolved + "/index"):
+ if os.path.exists(os.path.join(out, slug + ".fjson")):
+ return slug
+ return None
+
+
+def absolutize_links(out, site_dir):
+ """Rewrite Sphinx's directory-relative page links to absolute page URLs."""
+
+ def rewrite(body, base):
+ def repl(match):
+ href = match.group(1)
+ if re.match(r"([a-z][a-z0-9+.-]*:|#|/)", href, re.IGNORECASE):
+ return match.group(0)
+ path, sep, anchor = href.partition("#")
+ resolved = posixpath.normpath(posixpath.join(base, path))
+ slug = _page_slug(out, resolved)
+ if slug is None:
+ return match.group(0)
+ return f'href="/{site_dir}/{slug}{sep}{anchor}"'
+
+ return re.sub(r'href="([^"]*)"', repl, body)
+
+ for fpath in _page_files(out):
+ page = os.path.relpath(fpath, out)[: -len(".fjson")].replace(os.sep, "/")
+ base = page[: -len("index")] if page.endswith("index") else page + "/"
+ data = _load(fpath)
+ data["body"] = rewrite(data["body"], base)
+ _dump(fpath, data)
+
+
+# Inline handler, because scripts inside injected HTML do not run.
+_COPY_BUTTON = (
+ '"
+)
+
+
+def _add_copy_buttons(html):
+ """Add a copy button to every highlighted code block."""
+ return re.sub(
+ r'(
)',
+ lambda match: match.group(1) + _COPY_BUTTON,
+ html,
+ )
+
+
+def _html_to_mdx(frontmatter, html, css):
+ html = re.sub(r']*>.*?', "", html)
+ # Drop the first
, the docs site renders the title itself.
+ html = re.sub(r"
]*>.*?
", "", html, count=1, flags=re.DOTALL)
+ header = "".join(
+ f'{key}: "{value.replace(chr(34), chr(92) + chr(34))}"\n'
+ for key, value in frontmatter.items()
+ )
+ body = as_html_tag(css) + _add_copy_buttons(html.strip())
+ return (
+ f"---\n{header}---\n\n"
+ f"export const rawHtml = {json.dumps(body)};\n\n"
+ "\n"
+ )
+
+
+def _frontmatter(page):
+ parts = page_parts(page)
+ return {"title": ".".join(parts), "sidebarTitle": parts[-1], "icon": MODULE_ICON}
+
+
+def write_mdx(out, site_dir):
+ """Write an MDX page beside each reference page, then remove the JSON.
+
+ Pages are titled by module path, with a module icon and the last path
+ segment as the sidebar label. Returns the written page paths, without
+ extension.
+ """
+ css = reference_css(site_dir)
+ pages = []
+ for fpath in list(_page_files(out)):
+ data = _load(fpath)
+ if not data.get("body"):
+ continue
+ page = os.path.relpath(fpath, out)[: -len(".fjson")].replace(os.sep, "/")
+ with open(fpath[: -len(".fjson")] + ".mdx", "w") as f:
+ f.write(_html_to_mdx(_frontmatter(page), data["body"], css))
+ os.remove(fpath)
+ pages.append(page)
+ return sorted(pages)
diff --git a/docs/mintlify_export/style.py b/docs/mintlify_export/style.py
new file mode 100644
index 00000000..5b90a7ad
--- /dev/null
+++ b/docs/mintlify_export/style.py
@@ -0,0 +1,82 @@
+"""Colors for signatures, code blocks and sidebar icons.
+
+The docs site does not load Sphinx's theme CSS, so everything is styled here
+and embedded in each generated page. One palette drives all of it: CSS
+variables with a light default and a ``.dark`` override, since the docs site
+sets ``dark`` on the root element.
+"""
+
+import json
+
+# One icon per kind of node. The page front matter, the navigation and the
+# sidebar colors below all read these, so changing one keeps the rest in step.
+GROUP_ICON = "folder"
+MODULE_ICON = "file-code"
+
+PALETTE = """:root{--sig-keyword:#7e22ce;--sig-name:#1d4ed8;--sig-type:#0f766e;--sig-default:#b45309;--sig-muted:#64748b}
+.dark{--sig-keyword:#c084fc;--sig-name:#60a5fa;--sig-type:#5eead4;--sig-default:#fbbf24;--sig-muted:#94a3b8}
+"""
+
+SIGNATURES = """dt.sig{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.875rem;line-height:1.7;
+padding:.5rem .75rem;border-radius:.5rem;background:rgba(127,127,127,.08);overflow-x:auto}
+dt.sig em{font-style:normal}
+dt.sig .pre{font-weight:400}
+dt.sig .property,dt.sig .property .pre{color:var(--sig-keyword)}
+dt.sig .sig-prename,dt.sig .sig-prename .pre{color:var(--sig-muted)}
+dt.sig .sig-name,dt.sig .sig-name .pre{color:var(--sig-name);font-weight:600}
+dt.sig .sig-param>.n:not(:first-child),dt.sig .sig-return-typehint{color:var(--sig-type)}
+dt.sig .default_value{color:var(--sig-default)}
+dt.sig .p,dt.sig .o,dt.sig .sig-paren,dt.sig .sig-return-icon{color:var(--sig-muted)}
+dt.sig{position:relative;padding-right:4.5rem}
+dt.sig a:has(.viewcode-link){position:absolute;top:.5rem;right:.75rem;font-size:.75rem;color:var(--sig-muted);text-decoration:none}
+dt.sig .viewcode-link{font-weight:400}
+div[class^=highlight-]{position:relative;margin:.5rem 0}
+div.highlight{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:rgba(127,127,127,.08);overflow-x:auto}
+div.highlight pre{display:block;margin:0;padding:0;border:0;background:transparent;color:inherit;
+font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.8125rem;line-height:1.7;
+white-space:pre-wrap;overflow-wrap:anywhere;padding-right:2.25rem}
+p.rubric{margin:1rem 0 .25rem;font-size:.75rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--sig-muted)}
+.copy-btn{position:absolute;top:.3125rem;right:.375rem;z-index:1;display:flex;align-items:center;justify-content:center;
+width:1.75rem;height:1.75rem;border:0;border-radius:.375rem;background:transparent;color:var(--sig-muted);
+cursor:pointer;opacity:.6}
+.copy-btn:hover{opacity:1;background:rgba(127,127,127,.18)}
+.copy-btn .i-check{display:none;color:var(--sig-type)}
+.copy-btn[data-copied] .i-copy{display:none}
+.copy-btn[data-copied] .i-check{display:block}
+.highlight .k,.highlight .kn,.highlight .kd,.highlight .kc,.highlight .kp,.highlight .kr,.highlight .ow,.highlight .nd{color:var(--sig-keyword)}
+.highlight .nn,.highlight .o,.highlight .p,.highlight .c,.highlight .c1,.highlight .cm,.highlight .cs{color:var(--sig-muted)}
+.highlight .nf,.highlight .nc,.highlight .fm{color:var(--sig-name);font-weight:600}
+.highlight .nb,.highlight .bp,.highlight .kt,.highlight .m,.highlight .mi,.highlight .mf,.highlight .mh{color:var(--sig-type)}
+.highlight .s,.highlight .s1,.highlight .s2,.highlight .sa,.highlight .sb,.highlight .sc,.highlight .sd,.highlight .se,.highlight .si,.highlight .sx,.highlight .sr,.highlight .ss{color:var(--sig-default)}
+.highlight .c,.highlight .c1,.highlight .cm,.highlight .cs{font-style:italic}
+"""
+
+
+def _sidebar_icons(site_dir):
+ """Color sidebar icons by kind: package (amber), module (blue).
+
+ The docs site draws each icon as an element masked by its icon file, so the
+ kind is matched on the icon name and scoped to this SDK's pages.
+ """
+ prefix = f"/{site_dir}/"
+ return (
+ f'li:has(a[href^="{prefix}"])>button svg[style*="/{GROUP_ICON}.svg"]{{background-color:var(--sig-default)}}\n'
+ f'a[href^="{prefix}"] svg[style*="/{MODULE_ICON}.svg"]{{background-color:var(--sig-name)}}\n'
+ )
+
+
+def guide_css(site_dir):
+ return PALETTE + _sidebar_icons(site_dir)
+
+
+def reference_css(site_dir):
+ return PALETTE + _sidebar_icons(site_dir) + SIGNATURES
+
+
+def as_html_tag(css):
+ return f"\n"
+
+
+def as_mdx_element(css):
+ # A