Python (Flask) quickstart
🔑 Auth:
client_id+client_secret(server-side) · 👤 This IS the login — standard OpenID Connect · 🐍 Raw API recipe · ⏱ ~10 minutes
What you’ll build: a two-route Flask app where “Connect with Valyd” returns the user’s
pseudonymous valyd_id to your backend — no SDK, just requests.
Your real values are pre-filled on your app’s Quick setup tab in the Developer Portal .
1. Create the app
In the Developer Portal create an application, enable the profile
scope, and register this exact redirect URI:
http://localhost:5000/callbackCopy the client_id and the one-time client_secret.
2. Install
mkdir valyd-login && cd valyd-login
pip install flask requestsExport your credentials (replace with the real values from the portal):
export VALYD_CLIENT_ID=YOUR_CLIENT_ID
export VALYD_CLIENT_SECRET=YOUR_CLIENT_SECRET
export VALYD_REDIRECT_URI=http://localhost:5000/callback
export SESSION_SECRET=replace_with_at_least_32_random_characters3. Write the app
app.py
import os
import secrets
from urllib.parse import urlencode
import requests
from flask import Flask, abort, redirect, request, session
VALYD = "https://idp.valyd.work"
app = Flask(__name__)
app.secret_key = os.environ["SESSION_SECRET"]
@app.route("/")
def home():
return '<a href="/login">Connect with Valyd</a>'
@app.route("/login")
def login():
state = secrets.token_hex(16) # CSRF: Valyd echoes it back on the callback
nonce = secrets.token_hex(16) # bound into the id_token (replay protection)
session["valyd_state"] = state
session["valyd_nonce"] = nonce
return redirect(f"{VALYD}/api/auth/oidc/authorize?" + urlencode({
"client_id": os.environ["VALYD_CLIENT_ID"],
"redirect_uri": os.environ["VALYD_REDIRECT_URI"], # must exactly match an app redirect URI
"response_type": "code",
"scope": "openid profile", # openid is REQUIRED
"state": state,
"nonce": nonce,
}))
@app.route("/callback")
def callback():
if "error" in request.args or "code" not in request.args:
abort(400)
if not secrets.compare_digest(
session.pop("valyd_state", ""), request.args.get("state", "")
):
abort(400) # CSRF check — never skip it
token = requests.post(f"{VALYD}/api/auth/oidc/token", data={ # form-encoded
"grant_type": "authorization_code",
"code": request.args["code"],
"client_id": os.environ["VALYD_CLIENT_ID"],
"client_secret": os.environ["VALYD_CLIENT_SECRET"],
"redirect_uri": os.environ["VALYD_REDIRECT_URI"], # same URI used at authorize
})
token.raise_for_status()
tokens = token.json() # TOP-LEVEL: access_token, refresh_token, id_token, expires_in (~900)
user = requests.get(
f"{VALYD}/api/auth/oidc/userinfo",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
).json()
# user["valyd_id"] is the stable pseudonymous ID —
# find-or-create your local user against it, then start your own session.
return {"valyd_id": user["valyd_id"], "id_verified": user["id_verified"]}
if __name__ == "__main__":
app.run(port=5000)The id_token is an RS256-signed JWT. This quickstart trusts it because it arrived over TLS
directly from the token endpoint; for production, verify its signature against the JWKS at
https://idp.valyd.work/api/auth/oidc/jwks.json and check that its nonce claim equals
session["valyd_nonce"] (PyJWT does both), or use an OIDC library — see
Use any OIDC library.
4. Run it
python app.pyOpen http://localhost:5000 and click Connect with Valyd.
Checkpoint: /login redirects to https://idp.valyd.work/api/auth/oidc/authorize?... with
state and nonce in the URL; after consent, /callback returns your valyd_id and
id_verified; and a forged callback (wrong or missing state) is rejected with HTTP 400.
Troubleshooting
redirect_urimismatch — the value sent at/authorizeand/tokenmust both exactly match a registered redirect URI. See Errors & troubleshooting.- State check fails on legitimate logins — the Flask session cookie was lost (different
host/port between login and callback, or cookies blocked); start both routes on
localhost:5000. See Errors & troubleshooting. invalid_scope/ missingopenid—scopemust includeopenid, and every requested scope must be enabled on the app in the portal. See Errors & troubleshooting.
Next steps
- Scopes — request
verifications,email, or license scopes. - Account API — read userinfo, licenses (
/api/auth/oidc/licenses), and verification proofs (/api/auth/oidc/verifications) with the access token. - Attach a verification — run a new KYC or license check with the user’s token so the proof saves to their Valyd account.
- Moving to Node later? The official SDK (
npm install @valyd/sdk) does this whole flow, plus PKCE and id_token verification, in two calls — see the Node.js quickstart.