Flow API
The Flow API provides a set of endpoints to implement custom authentication flows. Each endpoint represents a step in the authentication process and follows consistent patterns for state management, error handling, and flow progression.
Core Concepts
CORS
The Flow API enforces a strict CORS policy. Cross-origin requests are only allowed from origins (scheme://host:port) that match a URI registered in a configured flow. Any other origin receives no CORS headers and the browser blocks the request.
This means that if you serve your custom flow from a different server than SympAuthy, you must declare its URL in the flows.<id> configuration. OPTIONS preflight requests from an allowed origin are handled automatically before authentication, so no additional setup is needed on your side.
See the Security page for a full description of this policy.
State Management
All authenticated endpoints require a state token that identifies the ongoing authorization attempt and session (all endpoints except the initial configuration call require it).
How the state is transmitted depends on the HTTP method:
| Request type | State location |
|---|---|
GET /flow/** (page navigation) | ?state= query parameter |
GET /api/v1/flow/** (AJAX) | ?state= query parameter |
POST /api/v1/flow/** (AJAX) | Authorization: State <jwt> header |
For GET requests, the state is passed as a URL query parameter so that browsers can follow server-side redirects and single-page applications can read it on page load.
For POST requests, the state must be sent in the Authorization header using the custom State scheme:
Authorization: State <jwt>Sending the state in a custom Authorization header rather than the URL provides CSRF protection: this header cannot be included in a cross-origin request without triggering a CORS preflight. Combined with the strict CORS policy that allows only registered flow origins, a forged cross-origin POST is rejected before it can execute.
See the Security page for a full description of this mechanism.
Redirect Pattern
All endpoints follow a consistent redirect pattern:
Operation Complete → Determine Next Step → Return redirect_urlThe redirect_url property will be present in responses when:
- The user has completed the current step and should proceed to the next one
- An unrecoverable error occurred (e.g., the session expired)
- The flow is complete and the user should be redirected back to the client application
All server-side redirects use HTTP 303 (See Other), never 307 (Temporary Redirect). OAuth 2.1 (section 7.5.3) prohibits 307 because it preserves the original HTTP method and request body — a POST carrying user credentials would be forwarded as-is to the redirect target, risking credential leakage. HTTP 303 forces the browser to issue a GET, which strips the request body and prevents this class of vulnerability.
Response Patterns
Endpoints return one of two response patterns:
- Simple Response - Contains only
redirect_url(used when operation always redirects to next step) - Complex Response - Contains data AND optionally
redirect_url:- When
redirect_urlis present: User must be redirected (step can be skipped) - When
redirect_urlis absent: User must complete an action on the current step
- When
Flow Endpoints
1. Configuration Endpoint
Path: /api/v1/flow/configuration
Authentication: None required (anonymous access)
Purpose: Provides initial configuration for the authentication flow. This should be the first call made by a custom flow.
GET Request
Returns the flow configuration including enabled features, collectable claims, and available authentication providers.
Response Format:
{
"claims": [
{
"id": "email",
"required": true,
"name": "Email Address",
"type": "string"
}
],
"features": {
"password_sign_in": true,
"sign_up": true
},
"password": {
"identifier_claims": [
"email"
]
},
"providers": [
{
"id": "google",
"name": "Google",
"authorize_url": "/api/v1/flow/providers/google/authorize"
}
]
}Properties:
claims: Array of claims that can be collected from usersid: Unique claim identifierrequired: Whether the claim must be providedname: Localized display namegroup: Optional grouping identifiertype: Data type (string,number, ordate)
features: Enabled authentication featurespassword_sign_in: Whether password-based sign-in is availablesign_up: Whether user registration is available
password: Password authentication configurationidentifier_claims: Claims used as login identifiers and required during registration
providers: Available OAuth 2 identity providersid: Provider identifiername: Display nameauthorize_url: URL to initiate OAuth flow (state parameter must be added)
Important Notes:
- This configuration is cacheable across users (client-specific, not user-specific)
- URLs in the configuration require the
stateparameter to be manually added before use - The configuration determines which other endpoints are available
2. Sign-Up Endpoint
Path: /api/v1/flow/sign-up
Authentication: Requires valid state in Authorization: State <jwt> header
Purpose: Handles new user registration with password-based authentication.
POST Request
Creates a new user account with the provided password and claims.
Request Format:
{
"password": "securePassword123",
"email": "user@example.com",
"username": "johndoe"
}The request accepts:
password: User's chosen password (required)- Additional claims as configured in
identifier_claims(dynamic properties)
Response Format:
{
"redirect_url": "/api/v1/flow/claims?state=..."
}Workflow:
- Validates the password and required claims
- Creates the user account
- Returns redirect to the next step (typically claims collection or validation)
3. Sign-In Endpoint
Path: /api/v1/flow/sign-in
Authentication: Requires valid state in Authorization: State <jwt> header
Purpose: Authenticates existing users with login and password credentials.
POST Request
Validates user credentials and establishes an authenticated session.
Request Format:
{
"login": "user@example.com",
"password": "securePassword123"
}Properties:
login: User identifier (matched against claims configured inidentifier_claims)password: User's password
Response Format:
{
"redirect_url": "/api/v1/flow/claims?state=..."
}Workflow:
- Validates login/password combination
- Identifies user by matching login against configured
identifier_claims - Returns redirect to next step (typically claims collection or flow completion)
4. Providers Endpoints
Base Path: /api/v1/flow/providers/{providerId}
Purpose: Handles OAuth 2 authorization with third-party identity providers.
Authorize Endpoint
Path: /api/v1/flow/providers/{providerId}/authorize
Method: GET
Authentication: Requires valid state parameter
Response: HTTP 303 redirect to the provider's authorization page
Parameters:
providerId: Identifier of the OAuth provider (from configuration)
Callback Endpoint
Path: /api/v1/flow/providers/{providerId}/callback
Method: GET
Authentication: None required (anonymous access)
Query Parameters:
code: OAuth authorization code (provided by the provider)state: Flow state parameter
Response: HTTP 303 redirect to the next flow step
Workflow:
- Authorize: User clicks provider button → redirected to provider's authorization page
- Provider Authentication: User authenticates with the third-party provider
- Callback: Provider redirects back with authorization code
- Token Exchange: Server exchanges code for user information
- Redirect: User redirected to next step in the flow
5. Claims Endpoint
Path: /api/v1/flow/claims
Authentication: GET requires ?state= query parameter; POST requires Authorization: State <jwt> header
Purpose: Handles collection of additional user information (claims) during the authentication flow.
GET Request
Returns all collectable claims with their metadata, any already-collected values, and suggested values from external providers. Only claims within the user's consented scopes are returned; identifier claims (used for sign-in/sign-up) are excluded.
This single endpoint provides everything needed to build the claims collection form — there is no need to cross-reference the configuration endpoint for claim metadata.
Response Format:
When claims need to be collected:
{
"claims": [
{
"id": "phone",
"required": true,
"name": "Phone Number",
"type": "phone_number",
"group": "identity",
"collected": false,
"value": null,
"suggested_value": "+1234567890"
},
{
"id": "birthdate",
"required": false,
"name": "Date of Birth",
"type": "date",
"group": null,
"collected": true,
"value": "1990-01-15",
"suggested_value": null
}
]
}When no claims need collection (auto-skip):
{
"redirect_url": "/client/callback?code=..."
}Claim Properties:
id: Claim identifierrequired: Whether this claim must be providedname: Localized display name (depends onAccept-Languageheader)type: Data type (e.g.string,date,phone_number,timezone)group: Group this claim belongs to (e.g.identity,address), ornullif ungroupedcollected: Whether the user has already been presented with this claim during a previous flow stepvalue: Current value provided by the user (nullif not yet collected or user declined)suggested_value: Value from a third-party provider, suggested as a default
Behavior:
- If
redirect_urlis present: No collectable claims, proceed to next step automatically - If
claimsarray is present: User must provide or confirm the listed claims
POST Request
Saves claims collected from the user.
Request Format:
{
"phone": "+1234567890",
"birthdate": "1990-01-15",
"address": null
}The request accepts dynamic claim properties. Set a claim to null or omit it to indicate the user chose not to provide it.
Response Format:
{
"redirect_url": "/api/v1/flow/claims/validation/EMAIL?state=..."
}Workflow:
- GET: Check if claims need collection
- If no collectable claims: Returns
redirect_urlto skip this step - If claims needed: Returns all collectable claims with metadata, collected values, and suggested values
- If no collectable claims: Returns
- POST: Save claim values
- The server filters updates to only collectable claims (user-inputted, non-identifier, within consented scopes)
- Null/empty values indicate claim not provided
- Returns redirect to next step
Notes:
- Identifier claims are excluded (already collected during sign-in/sign-up)
- Only claims within the user's consented scopes are returned
- Pre-filled values from providers can be edited by the user
6. Claims Validation Endpoints
Base Path: /api/v1/flow/claims/validation
Authentication: GET requires ?state= query parameter; POST requires Authorization: State <jwt> header
Purpose: Handles validation of user claims (e.g., email verification) via codes sent through various media channels.
Get Validation Code
Path: /api/v1/flow/claims/validation/{media}
Method: GET
Parameters:
media: Media type for code delivery (e.g.,EMAIL,SMS)
Response Format:
When validation is needed:
{
"media": "EMAIL",
"code": {
"id": "abc123",
"media": "EMAIL",
"reasons": [
"EMAIL_CLAIM"
],
"resendDate": "2026-02-14T10:43:30Z"
}
}When no validation needed (auto-skip):
{
"redirect_url": "/client/callback?code=..."
}Code Properties:
id: Unique identifier for this validation codemedia: Media through which code was sentreasons: Why validation is required (e.g.,EMAIL_CLAIM,PHONE_CLAIM)resendDate: ISO 8601 timestamp (UTC) when code can be resent
Behavior:
- First call: Sends validation code to user
- Subsequent calls: Returns existing code info without resending (anti-spam)
- If
redirect_urlis present: No validation needed, skip this step
Validate Code
Path: /api/v1/flow/claims/validation
Method: POST
Request Format:
{
"media": "EMAIL",
"code": "123456"
}Properties:
media: Media through which code was receivedcode: Code entered by the user
Response Format:
{
"redirect_url": "/client/callback?code=..."
}Workflow:
- Validates the provided code matches what was sent
- Marks the associated claim(s) as validated
- Returns redirect to next step
Resend Validation Code
Path: /api/v1/flow/claims/validation/resend
Method: POST
Request Format:
{
"media": "EMAIL"
}Response Format:
When code was resent:
{
"media": "EMAIL",
"resent": true,
"code": {
"id": "def456",
"media": "EMAIL",
"reasons": [
"EMAIL_CLAIM"
],
"resendDate": "2026-02-14T10:48:30Z"
}
}When resend was blocked (anti-spam):
{
"media": "EMAIL",
"resent": false
}Properties:
media: Media type for the resent coderesent: Whether a new code was actually sentcode: New code information (only present ifresentistrue)
Workflow:
- Check if enough time has passed since last send (based on
resendDate) - If allowed: Send new code and return new code information
- If blocked: Return
resent: falseto prevent spam
Supported Media Types: EMAIL, SMS
7. MFA Endpoints
Base Path: /api/v1/flow/mfa
Purpose: Handles multi-factor authentication during the interactive flow. These endpoints are only active when at least one MFA method is enabled in the configuration.
MFA Router
Path: /api/v1/flow/mfa
Method: GET
Authentication: Requires ?state= query parameter
Purpose: Determines the next MFA step based on the server configuration and the user's enrollment state. The UI should call this endpoint and follow the returned redirect.
Routing Logic:
mfa.required | Methods enrolled | Behavior |
|---|---|---|
false | None | Method selection with enrollment offers + skip option |
true | None | Auto-redirect to TOTP enrollment |
| any | Exactly one | Auto-redirect to challenge for that method |
| any | Multiple (future) | Method selection without skip |
Response Format:
When the step can be auto-skipped or auto-redirected:
{
"redirect_url": "/api/v1/flow/mfa/totp/enroll?state=..."
}When method selection is needed:
{
"methods": [
"totp"
],
"skip_redirect_url": "/api/v1/flow/mfa/skip?state=..."
}Properties:
redirect_url: URL to redirect to (enrollment, challenge, or next step)methods: Array of available MFA method identifiers (only when user must choose or may enrol)skip_redirect_url: URL to skip MFA (only present whenmfa.requiredisfalseand the user has not enrolled in any MFA method)
TOTP Enrollment
Path: /api/v1/flow/mfa/totp/enroll
Authentication: GET requires ?state= query parameter; POST requires Authorization: State <jwt> header
Purpose: Handles first-time TOTP setup. The user scans a QR code or enters the secret manually into their authenticator app, then confirms by entering the first valid code.
GET Request
Returns the enrollment data needed to register the TOTP secret with an authenticator app.
Response Format:
{
"otpauth_uri": "otpauth://totp/SympAuthy:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=SympAuthy",
"secret": "JBSWY3DPEHPK3PXP"
}Properties:
otpauth_uri: A URI following theotpauth://scheme that can be rendered as a QR code. Scanning this QR code with an authenticator app registers the secret automatically.secret: The base32-encoded TOTP secret, displayed for users who prefer to enter it manually.
POST Request
Confirms the TOTP enrollment by validating the first code entered by the user. A successful confirmation also marks MFA as passed for the current session.
Request Format:
{
"code": "123456"
}Response Format:
{
"redirect_url": "/api/v1/flow/claims?state=..."
}Workflow:
- User scans QR code or enters secret into their authenticator app
- User enters the 6-digit code shown by the app
- Server validates the code against the pending enrollment
- On success: enrollment is confirmed, MFA is marked as passed, redirect to next step
- On failure: recoverable error, user can retry
TOTP Challenge
Path: /api/v1/flow/mfa/totp
Authentication: Requires Authorization: State <jwt> header
Purpose: Validates a TOTP code for users who have already enrolled. This is the screen returning users see on subsequent sign-ins.
POST Request
Request Format:
{
"code": "123456"
}Response Format:
{
"redirect_url": "/api/v1/flow/claims?state=..."
}Workflow:
- User enters the 6-digit code from their authenticator app
- Server validates the code against the user's enrolled TOTP secret
- On success: MFA is marked as passed, redirect to next step
- On failure: recoverable error, user can retry
MFA Skip
Path: /api/v1/flow/mfa/skip
Method: GET
Authentication: Requires ?state= query parameter
Purpose: Marks MFA as passed without completing a challenge. This endpoint is only available when mfa.required is false and the user has not enrolled in any MFA method. Calling it when mfa.required is true, or when the user has already enrolled in at least one MFA method, returns an error.
Response Format:
{
"redirect_url": "/api/v1/flow/claims?state=..."
}Implementing a Custom Flow
Recommended Implementation Steps
Initialize Session
GET /api/v1/flow/configuration- No state parameter required
- Cache configuration for the session
- Determine available authentication methods
- Build UI based on enabled features
Authenticate User (choose one path)
Option A - Password Sign-In:
httpPOST /api/v1/flow/sign-in Authorization: State {state}Option B - Password Sign-Up:
httpPOST /api/v1/flow/sign-up Authorization: State {state}Option C - Provider Authentication:
Redirect to provider's authorize_url from configuration (add state parameter to the URL)Multi-Factor Authentication (if MFA is enabled)
httpGET /api/v1/flow/mfa?state={state}- Follow the returned
redirect_url— it points to enrollment, challenge, or the next step - If redirected to enrollment: display QR code and secret from
GET /api/v1/flow/mfa/totp/enroll, then POST the confirmation code - If redirected to challenge: display code input, POST to
/api/v1/flow/mfa/totp - If method selection is returned: show available methods and optional skip button
- Follow the returned
Collect Additional Claims
httpGET /api/v1/flow/claims?state={state} POST /api/v1/flow/claims Authorization: State {state}- GET returns all collectable claims with full metadata (
required,name,type,group), collected values, and suggested values — build the entire form from this single response - May auto-redirect if no claims need collection
- Pre-fill with
valueorsuggested_valuefrom GET response - POST collected values
- GET returns all collectable claims with full metadata (
Validate Claims (for each required media)
httpGET /api/v1/flow/claims/validation/{media}?state={state} POST /api/v1/flow/claims/validation Authorization: State {state} POST /api/v1/flow/claims/validation/resend (if needed) Authorization: State {state}- GET to trigger code sending
- Display code input form with resend option
- POST code for validation
- Use resend endpoint if user didn't receive code
Follow Redirects
After each step, check the
redirect_urlproperty:- If points to another flow endpoint: Continue to that step
- If points to client application: Flow complete, handle success
- If points to error endpoint: Handle error appropriately
Example Flow Sequence
1. GET /api/v1/flow/configuration
↓
2. POST /api/v1/flow/sign-in [Authorization: State abc123]
→ Returns: {"redirect_url": "/api/v1/flow/mfa?state=abc123"}
↓
3. GET /api/v1/flow/mfa?state=abc123
→ Returns: {"redirect_url": "/api/v1/flow/mfa/totp?state=abc123"}
↓
4. POST /api/v1/flow/mfa/totp [Authorization: State abc123] {"code": "123456"}
→ Returns: {"redirect_url": "/api/v1/flow/claims?state=abc123"}
↓
5. GET /api/v1/flow/claims?state=abc123
→ Returns: {"claims": [...]}
↓
6. POST /api/v1/flow/claims [Authorization: State abc123]
→ Returns: {"redirect_url": "/api/v1/flow/claims/validation/EMAIL?state=abc123"}
↓
7. GET /api/v1/flow/claims/validation/EMAIL?state=abc123
→ Returns: {"media": "EMAIL", "code": {...}}
↓
8. POST /api/v1/flow/claims/validation [Authorization: State abc123]
→ Returns: {"redirect_url": "https://client.app/callback?code=xyz789"}
↓
9. Redirect to client application (flow complete)Error Handling
The Flow API implements two types of error handling:
Recoverable Errors (HTTP 4xx):
- User can modify their request and retry
- Example: Invalid password, validation code incorrect
- Display error message to user and allow retry
Unrecoverable Errors (HTTP 303 redirect):
- Session expired, configuration error, etc.
- User automatically redirected to error page
- Flow must be restarted from the beginning
Best Practices
- Always follow redirects: The server controls flow progression through
redirect_url - Check for auto-skip: Some GET endpoints may return only
redirect_urlif the step can be skipped - Preserve state: Include the state token in all authenticated requests — as
?state=query parameter for GET requests, and asAuthorization: State <jwt>header for POST requests - Handle dynamic claims: Claims are configuration-driven; don't hardcode which claims to collect
- Respect resend limits: Honor the
resendDateto prevent spam and improve deliverability - Pre-fill values: Use
valueandsuggested_valuefrom responses to improve user experience - Localization: Send appropriate
Accept-Languageheader for localized claim names and messages