Documentation Index
Fetch the complete documentation index at: https://docs.prelude.so/llms.txt
Use this file to discover all available pages before exploring further.
Beyond Prelude’s managed steps (verify_sms, verify_email), you can define custom steps that your own backend handles — KYC review, biometric verification, document upload, or any other process. When a custom step is reached, your backend verifies the user and issues a signed token that Prelude validates to advance the challenge.
Make sure you are familiar with the Step-Up Authentication flow before reading this page.
Prerequisites
- A working step-up configuration
- An RSA key pair for signing verification tokens
- A public JWKS endpoint exposing your public keys
Setup
1. Register your custom step keys
Add your custom step keys and JWKS URL to the step-up configuration:
curl -X POST https://api.prelude.dev/v2/session/apps/${APP_ID}/config/stepup \
-H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"jwks_url": "https://api.example.com/.well-known/jwks.json",
"step_keys": [
{
"key": "kyc_review",
"description": "Identity verification via KYC provider"
},
{
"key": "biometric_check",
"description": "Face recognition verification"
}
],
"allowed_scopes": [
{
"scope": "transfer:write",
"mode": "delegated",
"delegated": {
"delegation_hook": "https://api.example.com/hooks/stepup"
}
}
]
}'
Step keys must only contain: a-z, A-Z, 0-9, and .-_:.
2. Expose a JWKS endpoint
Your jwks_url must serve a standard RFC 7517 JSON Web Key Set containing the RSA public keys used to verify your verification tokens. Each key must include a kid (key ID).
{
"keys": [
{
"kty": "RSA",
"kid": "my-key-1",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGc...",
"e": "AQAB"
}
]
}
Prelude caches your JWKS for 10 minutes and automatically re-fetches on key-not-found to handle key rotation.
3. Return custom steps from your hook
In your hook response, include your custom step keys alongside any managed steps:
{
"status": "review",
"granted_for": 180,
"grant_mode": "single-use",
"steps": [
{
"order": 1,
"key": "verify_sms",
"expiration_duration": 600
},
{
"order": 2,
"key": "kyc_review",
"expiration_duration": 300
}
]
}
Steps are completed in order. In this example, the user first completes SMS verification (handled by Prelude), then your KYC review.
Completing a custom step
When the challenge reaches a custom step, the user completes it on your side (your UI, your backend logic). Once verified, your backend issues a verification token and the user sends it to Prelude to advance the challenge.
1. Issue a verification token
Your backend creates an RS256 JWT signed with your private key:
{
"sub": "usr_01kg1y07cze24ty0yw32jrwwf7",
"exp": 1770885590,
"nbf": 1770884990,
"iat": 1770884990,
"jti": "07b3bfc2-425d-4c78-af63-fd024918f0cf",
"challenge_id": "cha_01kh8fh1hzeqvvfsmz7r1rn331",
"key": "kyc_review",
"status": "completed"
}
Token fields
| Field | Type | Description |
|---|
sub | string | The Prelude user ID. Must match the challenge token’s sub. |
exp | integer | Expiration time (Unix timestamp). |
nbf | integer | Not-before time (Unix timestamp). |
iat | integer | Issued-at time (Unix timestamp). |
jti | string | A unique JWT ID. Must be unique across all tokens. Prelude rejects reused JTIs. |
challenge_id | string | The challenge ID from the challenge token. Must match exactly. |
key | string | The step key being completed. Must match the current step in the challenge. |
status | string | Must be "completed". |
Token requirements
| Rule | Detail |
|---|
| Algorithm | RS256 only |
JWT header kid | Required — must match a key ID in your JWKS endpoint |
| JTI uniqueness | Each jti can only be used once. Replayed tokens are rejected with token_reused. |
| Field matching | sub and challenge_id must match the challenge token. The key must correspond to the current step. |
Example (Node.js)
const jwt = require("jsonwebtoken");
const { v4: uuidv4 } = require("uuid");
function issueVerificationToken(userId, challengeId, stepKey, privateKey, keyId) {
return jwt.sign(
{
sub: userId,
jti: uuidv4(),
challenge_id: challengeId,
key: stepKey,
status: "completed"
},
privateKey,
{
algorithm: "RS256",
expiresIn: 300,
notBefore: 0,
keyid: keyId
}
);
}
2. Advance the challenge
Once your backend issues the verification token, the frontend SDK advances the challenge:
await client.continueStepUp(verificationToken, (info) => {
console.log(info.currentStep); // next step key, or "completed"
});
See the Web SDK Step-Up guide for full details.
Prelude validates the token by:
- Fetching your public keys from your JWKS endpoint
- Verifying the RS256 signature and expiration
- Checking that
sub, challenge_id, and key match the challenge token
- Checking the
jti has not been used before
If all steps are now done, the SDK automatically refreshes the session with the granted scope.
Validation errors
| Error | Status | Description |
|---|
invalid_verification_token | 400 | The verification token is malformed, expired, or the signature is invalid. |
token_mismatch | 400 | sub, challenge_id, or key in the verification token does not match the challenge. |
step_not_completed | 400 | The step’s status is not "completed". |
step_bypassed | 400 | A previous step in the sequence has not been completed. |
step_not_found | 404 | The step key from the verification token does not exist in the challenge. |
token_reused | 409 | The verification token’s jti has already been used. |