> ## 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.

# Step-Up Hook Reference

> API reference for the step-up hook endpoint your backend must implement.

When a user requests a scope grant via [`POST /v1/session/stepup/request`](/session/api-reference/frontend/stepup-request), Prelude calls your **step-up hook** — the `delegation_hook` you registered for that scope in the [step-up configuration](/session/api-reference/management/config/stepup/create-stepup-config) 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:

```json theme={null}
{
  "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. |

### Request headers

| 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](/session/documentation/webhooks/introduction#webhook-signature). Verify the `X-Webhook-Signature` header using the public key matching the `X-Webhook-Signature-Key-Id` from your application's [JWKS endpoint](/session/documentation/jwks).

## Hook response

Your endpoint must return a JSON response with a verdict.

### Grant immediately

Return `status: "continue"` to grant the scope without any challenge:

```json theme={null}
{
  "status": "continue",
  "granted_for": 3600,
  "grant_mode": "session-bound"
}
```

### Require a challenge

Return `status: "review"` with one or more steps the user must complete:

```json theme={null}
{
  "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:

```json theme={null}
{
  "status": "block"
}
```

### 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:

```javascript theme={null}
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](/session/documentation/step-up-custom-steps) for the full verification token format, requirements, and code examples.
