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.
When a user requests a scope grant via POST /v1/session/stepup/request, Prelude calls your step-up hook — the delegation_hook you registered for that scope in the step-up configuration when its mode is delegated. Your hook decides whether to grant the scope immediately, require a multi-step challenge, or block the request.
Hook request
Prelude sends a signed POST request to your hook URL with the following JSON body:
{
"scope_requested": "transfer:write",
"user_id": "usr_39CfbdV8AsXQwdphtbJe4yH07aF",
"identifiers": [
{
"type": "email_address",
"value": "user@example.com"
},
{
"type": "phone_number",
"value": "+33612345678"
}
],
"signals": {
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
"platform": "WEB",
"ip": "203.0.113.42"
},
"metadata": {
"amount": "500",
"currency": "USD"
}
}
Request fields
| Field | Type | Description |
|---|
scope_requested | string | The scope the user is requesting. |
user_id | string | The Prelude user ID. |
identifiers | array | The user’s identifiers (email addresses, phone numbers). |
identifiers[].type | string | "email_address" or "phone_number". |
identifiers[].value | string | The identifier value. |
signals | object | Contextual signals from the user’s session. |
signals.user_agent | string | The user’s browser or app user agent string. |
signals.platform | string | The platform (e.g. "WEB", "ANDROID", "IOS"). |
signals.ip | string | The user’s IP address. |
metadata | object | Optional metadata passed by the frontend when requesting the scope. Max 5 fields, keys max 12 characters, values max 32 characters. |
| Header | Description |
|---|
Content-Type | application/json |
User-Agent | Prelude-SessionStepUpHook/1.0 |
X-Webhook-Signature | Base64 URL-encoded RSASSA-PSS SHA-256 signature of the request body. |
X-Webhook-Signature-Key-Id | The ID of the signing key used to produce the signature. |
Request signature
The hook request is signed using the same mechanism as webhooks. Verify the X-Webhook-Signature header using the public key matching the X-Webhook-Signature-Key-Id from your application’s JWKS endpoint.
Hook response
Your endpoint must return a JSON response with a verdict.
Return status: "continue" to grant the scope without any challenge:
{
"status": "continue",
"granted_for": 3600,
"grant_mode": "session-bound"
}
Require a challenge
Return status: "review" with one or more steps the user must complete:
{
"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
}
]
}
Block the request
Return status: "block" to deny the scope entirely:
Response fields
| Field | Type | Required | Description |
|---|
status | string | Yes | "continue", "review", or "block". |
granted_for | integer | Yes (when continue or review) | Duration in seconds for which the scope is granted. Must be between 0 and 86400 (24 hours). |
grant_mode | string | Yes (when continue or review) | "single-use" or "session-bound". See below. |
steps | array | Yes (when review) | The steps the user must complete, in order. Must not be empty when status is review. Must not be present when status is continue or block. |
steps[].order | integer | Yes | The position of this step in the sequence (starting from 1). |
steps[].key | string | Yes | The step identifier. Use "verify_sms" or "verify_email" for Prelude-owned steps, or a custom key you registered in the step-up configuration. |
steps[].expiration_duration | integer | Yes | Time in seconds the user has to complete this step. Must be between 0 and 86400 (24 hours). |
Grant modes
| Mode | Behavior |
|---|
single-use | The scope is attached only to the next access token. It is not persisted on the session. granted_for must be at least 1 second. |
session-bound | The scope is stored on the session and included in every subsequent refresh for granted_for seconds. If granted_for is less than 1, it defaults to 600 seconds (10 minutes). |
Response constraints
| Rule | Limit |
|---|
| Maximum response body size | 64 KB |
granted_for range | 0 to 86400 seconds (24 hours). Cannot be negative. |
steps with review | Must contain at least one step. |
steps with continue or block | Must not be present. |
Step key format | Only a-z, A-Z, 0-9, and .-_: characters. |
Step expiration_duration range | 0 to 86400 seconds (24 hours). Cannot be negative. |
Response HTTP status
Your hook must return HTTP 200 with the JSON body. Any non-200 response or timeout (5 seconds) will cause the step-up request to fail.
Example implementation
Here is a minimal Node.js example of a step-up hook:
app.post("/hooks/stepup", (req, res) => {
const { scope_requested, user_id, signals, metadata } = req.body;
// Block requests from unknown IPs
if (!isKnownIP(signals.ip)) {
return res.json({ status: "block" });
}
// High-value transfers require SMS verification + KYC
if (scope_requested === "transfer:write" && parseInt(metadata?.amount) > 1000) {
return res.json({
status: "review",
granted_for: 120,
grant_mode: "single-use",
steps: [
{ order: 1, key: "verify_sms", expiration_duration: 600 },
{ order: 2, key: "kyc_review", expiration_duration: 300 }
]
});
}
// Low-risk operations: grant directly
return res.json({
status: "continue",
granted_for: 3600,
grant_mode: "session-bound"
});
});
Custom steps and verification tokens
If your hook returns custom step keys (anything other than verify_sms or verify_email), your backend must issue verification tokens to advance the challenge. See Custom Steps for the full verification token format, requirements, and code examples.