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

::alert{type="info"}
Account sharing prevention is a special case and doesn't follow this flow. See [Access protection](/docs/v3/fundamentals/access-protection) instead!.
::

## 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,
)
```

#react-native

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

::

You will then hand the evaluation ID to your server. The server will get the evaluation details (including the verdict and redirect URL) and hand them back to the client if needed.

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

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

When a client in your success page hands you an evaluation ID, you should consume it. This gives you the evaluation state and marks it as consumed. This is done to prevent someone from taking a successful challenge and using it again to honor other actions. Check that the challenge completed and the evaluation's action and identifiers match what you expected, and only then honor the action.

::CodeTabs

#Node SDK

```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.metadata !== YOUR_EXPECTED_METADATA // Metadata mismatch
) {
  // Integrity mismatch. Block the action.
  return reject("Integrity mismatch");
}

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

#NET SDK

```csharp
using Rupt.Api;

var rupt = new RuptApi("API_SECRET");

Evaluation 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.ConsumeEvaluationAsync(evaluationId);
}
catch (RuptApiException ex) when (ex.StatusCode == 409)
{
    // Already used. Reject and have the user start the action again.
    return Reject("This challenge was already used. Start the action again.");
}
catch (RuptException)
{
    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
)
{
    // Integrity mismatch. Block the action.
    return Reject("Integrity mismatch");
}

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

#cURL

```sh
# Consume reads and claims the evaluation in one shot. A replay of the
# success URL gets 409 because the evaluation was already used.
curl -X POST "https://api.rupt.dev/v3/evaluations/$EVALUATION_ID/consume" \
  -H "Authorization: Bearer $API_SECRET"
```

::

If you consume via the raw API, check `challenge.status`, `action`, and the user identifiers in the response before you honor the action, exactly as the SDK examples do. Treat any challenge `status` other than `completed` as a block.

Check the `verdict` too. A route that finishes a challenge should only ever see `verdict: "challenge"`, so anything else reaching it means the client took a path it shouldn't have. Reject it.

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