> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tracenow.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-side integration

> Add the in-app layer to send the real user ID, confirmed status, and act on the verdict.

This is the **in-app layer** of the Cloudflare integration. The [Cloudflare Worker](/guides/cloudflare) already calls `/trace` at the edge before every monitored request reaches you, but the edge doesn't know who the user is yet. This backend call adds the **real `subject_user_id`** and confirmed outcome status, which unlocks per-user signals the edge cannot see: `velocity.user.*`, `ip.is_new_country_for_user`, `device.is_new_for_user`, and `ip.impossible_travel`.

<Note>
  Complete the [Cloudflare integration](/guides/cloudflare) before adding this layer. The Worker
  injects the fingerprint script automatically and attaches the device token to every request as the
  `x-tn-device-token` header, so no frontend work is required on your end.
</Note>

## When to call it

Call `/trace` from your backend at the moment you authenticate the user: after you verify credentials but before you issue a session. This is when you have:

* The **real user ID** (`subject_user_id`), which the edge never has
* The **confirmed outcome** (`succeeded` or `failed`): the edge infers this from HTTP status codes, but you know it precisely
* The **device token**, already on the incoming request as `x-tn-device-token`

## Getting the device token

The Cloudflare Worker injects `trace.min.js` first-party onto your pages. That script calls `/identify` and then attaches the resulting device token to every subsequent request as the `x-tn-device-token` header. You don't add any frontend code; just read the header your backend already receives:

```typescript TypeScript theme={null}
const deviceToken = req.headers["x-tn-device-token"];
```

```python Python theme={null}
device_token = request.headers.get("x-tn-device-token")
```

## Full login example

<Steps>
  <Step title="Read the device token from the header">
    The Worker has already attached it. Extract it before you do anything else.

    <CodeGroup>
      ```typescript TypeScript (Node) theme={null}
      app.post("/login", async (req, res) => {
        const deviceToken = req.headers["x-tn-device-token"] as string | undefined;
        const { email, password } = req.body;

        // Verify credentials first
        const user = await verifyCredentials(email, password);
        const loginStatus = user ? "succeeded" : "failed";
      ```

      ```python Python theme={null}
      @app.post("/login")
      async def login(request: Request, body: LoginBody):
          device_token = request.headers.get("x-tn-device-token")
          
          # Verify credentials first
          user = await verify_credentials(body.email, body.password)
          login_status = "succeeded" if user else "failed"
      ```
    </CodeGroup>
  </Step>

  <Step title="POST to /trace with subject_user_id and confirmed status">
    Send the real user ID (even on failure, if you resolved the account) and the confirmed outcome.

    <CodeGroup>
      ```typescript TypeScript (Node) theme={null}
        const ip =
          (req.headers["x-forwarded-for"] as string)?.split(",")[0].trim() ??
          req.headers["x-real-ip"] as string;

        const trace = await fetch("https://api.tracenow.io/trace", {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.TRACENOW_SECRET_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            event: "login",
            status: loginStatus,
            ip,
            email,
            device_token: deviceToken,
            subject_user_id: user?.id ?? null,
          }),
        }).then((r) => r.json());
      ```

      ```python Python theme={null}
          # Get the real client IP from the forwarded header
          forwarded_for = request.headers.get("x-forwarded-for")
          ip = forwarded_for.split(",")[0].strip() if forwarded_for else request.headers.get("x-real-ip")

          trace = httpx.post(
              "https://api.tracenow.io/trace",
              headers={"Authorization": f"Bearer {os.environ['TRACENOW_SECRET_KEY']}"},
              json={
                  "event": "login",
                  "status": login_status,
                  "ip": ip,
                  "email": body.email,
                  "device_token": device_token,
                  "subject_user_id": str(user.id) if user else None,
              },
          ).json()
      ```
    </CodeGroup>
  </Step>

  <Step title="Switch on the verdict">
    `allow` → proceed, `challenge` → require step-up, `deny` → block.

    <CodeGroup>
      ```typescript TypeScript (Node) theme={null}
        switch (trace.verdict) {
          case "allow":
            if (!user) return res.status(401).json({ error: "Invalid credentials" });
            return res.json({ session: await issueSession(user) });

          case "challenge":
            // Surface step-up — MFA, CAPTCHA, OTP, etc.
            // You decide the mechanism; Tracenow just tells you a challenge is needed.
            return res.status(200).json({
              requires_challenge: true,
              challenge_type: "mfa",
              trace_id: trace.trace_id,
            });

          case "deny":
            return res.status(403).json({ error: "Access denied" });
        }
      });
      ```

      ```python Python theme={null}
          verdict = trace.get("verdict")

          if verdict == "allow":
              if not user:
                  return JSONResponse({"error": "Invalid credentials"}, status_code=401)
              return JSONResponse({"session": await issue_session(user)})

          elif verdict == "challenge":
              # Surface step-up to the client — MFA, CAPTCHA, OTP, etc.
              return JSONResponse({
                  "requires_challenge": True,
                  "challenge_type": "mfa",
                  "trace_id": trace.get("trace_id"),
              })

          elif verdict == "deny":
              return JSONResponse({"error": "Access denied"}, status_code=403)
      ```
    </CodeGroup>
  </Step>
</Steps>

## Getting the real client IP

Pass the real client IP, not your server's outbound address. Extract it from the forwarded headers set by your reverse proxy or load balancer.

<CodeGroup>
  ```typescript TypeScript (Node) theme={null}
  const ip =
    (req.headers["x-forwarded-for"] as string)?.split(",")[0].trim() ??
    (req.headers["x-real-ip"] as string);
  ```

  ```typescript TypeScript (Next.js) theme={null}
  import { headers } from "next/headers";

  const ip =
    headers().get("x-forwarded-for")?.split(",")[0].trim() ??
    headers().get("x-real-ip") ??
    undefined;
  ```

  ```python Python theme={null}
  forwarded_for = request.headers.get("x-forwarded-for")
  ip = forwarded_for.split(",")[0].strip() if forwarded_for else request.headers.get("x-real-ip")
  ```
</CodeGroup>

<Warning>
  Never use `req.socket.remoteAddress` (Node) or `request.remote_addr` (Python/WSGI) in a proxied
  environment, as these return your load balancer's IP, not the user's.
</Warning>

## Handling signals\_complete: false

On a first-time lookup for a new IP or email (a cache miss), some async enrichment may not have resolved before the response returns. The `signals_complete: false` flag tells you the verdict was made on partial data.

For most checkpoints this is fine: your policies still evaluated on what was available, and the result is usually correct. For high-stakes decisions (transaction approval, privileged action), retry with the same payload after \~500 ms to get the warm-cache result:

```typescript TypeScript theme={null}
let trace = await callTrace(payload);

if (!trace.signals_complete && isHighStakes) {
  await new Promise((resolve) => setTimeout(resolve, 500));
  trace = await callTrace(payload);
}
```

## What the in-app call adds

| Signal group                 | Cloudflare edge         | In-app call |
| ---------------------------- | ----------------------- | ----------- |
| IP enrichment                | Yes                     | Yes         |
| Email enrichment             | Yes                     | Yes         |
| Phone enrichment             | Yes                     | Yes         |
| Device fingerprint           | Yes                     | Yes         |
| `velocity.ip.*`              | Yes                     | Yes         |
| `velocity.user.*`            | No (no user ID)         | Yes         |
| `ip.is_new_country_for_user` | No                      | Yes         |
| `ip.impossible_travel`       | No                      | Yes         |
| `device.is_new_for_user`     | No                      | Yes         |
| Confirmed `status`           | Inferred from HTTP code | Exact       |

## Related

* [Cloudflare integration](/guides/cloudflare): set this up first
* [POST /trace reference](/api-reference/trace): full request and response documentation
* [Verdicts](/concepts/verdicts): what `allow`, `challenge`, and `deny` mean and when each applies
