---
title: Challenge flow
---

# Challenge flow

This picks up where the [quick start](/docs/v3/quick-start) leaves off. You've wired `evaluate` on the client and the server confirmation. Now a [policy](/docs/v3/concepts/policies) returns a `challenge` [verdict](/docs/v3/concepts/verdicts) and Rupt runs an interactive [challenge](/docs/v3/concepts/challenges): it sends the user to a hosted verification page, and when they pass, sends them back to you. This guide is the generic wiring for that round trip, and every other guide that challenges a user reuses it.

The shape never changes: evaluate on the client, let Rupt run the challenge, and confirm the outcome on your server before you honor the action. The final server step consumes the evaluation, so a passed challenge is single-use and can't be replayed. Nothing trusts the client.

## The flow

<!-- prettier-ignore-start -->
::MermaidDiagram
---
code: |
  sequenceDiagram
    actor U as User
    participant C as Client app
    participant S as Your server
    participant R as Rupt
    participant X as Challenge UI

    U->>C: Takes the action
    C->>R: evaluate(action, identifiers)
    R-->>C: { evaluation_id, redirect? }
    alt verdict needs a challenge
      C->>X: Redirect to the challenge UI
      U->>X: Completes verification
      X->>C: Return to success_url?evaluation=…
    end
    C->>S: Send evaluation_id with the action request
    S->>R: Consume the evaluation (single-use)
    R-->>S: { challenge.status, … } or 409 if already used
    Note over S: Confirm status + integrity, then honor once
    S-->>U: Honor or block the action
---
::
<!-- prettier-ignore-end -->

## Before you start: the policy and its challenge config

A challenge only happens because a [policy](/docs/v3/concepts/policies) told it to. In the dashboard, create a policy whose **action is `challenge`** for the [action](/docs/v3/concepts/actions) you're protecting (`login`, `signup`, or `access`) and the [checks](/docs/v3/concepts/checks) you want to gate on.

Every challenge policy points at a **challenge config**, which holds the verification channels and the URLs Rupt uses for the round trip:

- **Success URL**: where Rupt sends the user after they pass. This is the page on your site that finishes the action.
- **Primary, secondary, and logout URLs**: the links shown inside the challenge UI (for example, "back to app" or "log out").

You set these once on the challenge config, and they apply to every challenge that policy issues.

## Step 1: Evaluate on the client

Call `evaluate` at the moment the user takes the action. Pass whatever identifiers you have: a `user` id, `email`, `phone`. The response carries an `evaluation_id` and, when a challenge is required, a `redirect`.

::ClientPlatform

#web

```js
import Rupt from "@ruptjs/client";

const rupt = new Rupt({ clientId: "your_client_id" });

const evaluation = await rupt.evaluate.login({
  user: "USER_ID",
  email: form.email,
});
```

#ios

```swift
let response = try await rupt.evaluate(
  action: "login",
  user: "USER_ID",
  email: form.email
)
```

#android

```kotlin
val response = rupt.evaluate(
  action = "login",
  user = "USER_ID",
  email = form.email,
)
```

::

For `access`, the SDK navigates to the challenge UI automatically when one is required. Every other action (`login`, `signup`, and custom events) doesn't auto-navigate by default, so the form your user is filling isn't abandoned mid-submit. The redirect URL comes back in the response and your server decides what to do with it. See [Signup protection](/docs/v3/fundamentals/signup-protection). You can force either behavior per call with `auto_challenge: true | false`.

## Step 2: Set the success URL

In the dashboard, on the challenge config your policy uses, set **Success URL** to the page that completes the action, for example `https://yourapp.com/verified`.

When the user passes, Rupt redirects there with the evaluation ID appended:

```
https://yourapp.com/verified?evaluation=68f…
```

## Step 3: Hand the evaluation ID to your server

::alert{type="info"}
This step is optional for account-sharing prevention.
::

Your success page reads the evaluation ID off the URL and posts it to your backend. The client never decides the outcome. It only carries the evaluation ID across.

```js
const params = new URLSearchParams(window.location.search);

await fetch("/verify-challenge", {
  method: "POST",
  body: JSON.stringify({
    evaluation_id: params.get("evaluation"),
  }),
});
```

## Step 4: Consume the evaluation on your server

Make the final step a consumption. Consuming reads the evaluation and claims it in one atomic, single-use step, so a passed challenge can't be replayed into a second honored action. Confirm the consume succeeded, check that the challenge completed and the evaluation's action and identifiers match what you expected, and only then honor the action. (Flows with no server step, like self-managed account sharing, skip this.)

```js
import { RuptAPI } from "@ruptjs/api";

const rupt = new RuptAPI("API_SECRET");

let evaluation;
try {
  // Consume reads and claims the evaluation in one shot. A replay of the
  // success URL gets 409 because the evaluation was already used.
  evaluation = await rupt.consumeEvaluation(evaluation_id);
} catch (err) {
  if (err.status === 409) {
    // Already used. Reject and have the user start the action again.
    return reject("This challenge was already used. Start the action again.");
  }
  return reject("Could not verify the challenge");
}

if (evaluation.challenge?.status !== "completed") {
  // Challenge not completed. Send the user back to the challenge UI.
  return redirect(evaluation.redirect);
}
if (
  evaluation.user?.email !== expectedEmail || // Identity mismatch
  evaluation.action !== "YOUR_EXPECTED_ACTION" || // Action mismatch
  evaluation.user?.metadata !== YOUR_EXPECTED_METADATA // Metadata mismatch
) {
  // Integrity mismatch. Block the action.
  return reject("Integrity mismatch");
}

// Honor the action
honorTheAction();
```

Treat any challenge `status` other than `completed` as a block.

## Where to go next

- **[Signup protection](/docs/v3/fundamentals/signup-protection)**: the signup variant. The user is new with no ID yet, so you store a little state to bind the pending signup to its challenge.
- **[Login protection](/docs/v3/fundamentals/login-protection)**: the login variant. Nothing to store, just hold off issuing the session until the challenge completes.
- **[Account sharing prevention](/docs/v3/guides/account-sharing-prevention)**: a self-managed variant on `access`, with no server step.
