Skip to main content
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 '{
    "signal_hook_url": "https://api.example.com/hooks/stepup",
    "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": ["transfer:write"]
  }'
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

FieldTypeDescription
substringThe Prelude user ID. Must match the challenge token’s sub.
expintegerExpiration time (Unix timestamp).
nbfintegerNot-before time (Unix timestamp).
iatintegerIssued-at time (Unix timestamp).
jtistringA unique JWT ID. Must be unique across all tokens. Prelude rejects reused JTIs.
challenge_idstringThe challenge ID from the challenge token. Must match exactly.
keystringThe step key being completed. Must match the current step in the challenge.
statusstringMust be "completed".

Token requirements

RuleDetail
AlgorithmRS256 only
JWT header kidRequired — must match a key ID in your JWKS endpoint
JTI uniquenessEach jti can only be used once. Replayed tokens are rejected with token_reused.
Field matchingsub 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:
  1. Fetching your public keys from your JWKS endpoint
  2. Verifying the RS256 signature and expiration
  3. Checking that sub, challenge_id, and key match the challenge token
  4. 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

ErrorStatusDescription
invalid_verification_token400The verification token is malformed, expired, or the signature is invalid.
token_mismatch400sub, challenge_id, or key in the verification token does not match the challenge.
step_not_completed400The step’s status is not "completed".
step_bypassed400A previous step in the sequence has not been completed.
step_not_found404The step key from the verification token does not exist in the challenge.
token_reused409The verification token’s jti has already been used.