Skip to main content
The /trace endpoint is the primary integration point for server-side fraud evaluation. You send it the signals you have — an IP, an email, a phone number, a device token — it fans out enrichment in parallel, runs your policies, and returns a verdict you can act on immediately.

How it works

1

Collect signals at the event boundary

At login, signup, checkout, or any checkpoint, gather the signals you have available. You don’t need all four — pass whatever you have.
2

POST to /trace

Send the signals to the API. The response includes the enriched data and a verdict.
3

Act on the verdict

allow — proceed normally. challenge — step up (e.g. 2FA, CAPTCHA). deny — block the request.

Making a trace request

cURL
curl -X POST https://api.tracenow.io/trace \
  -H "Authorization: Bearer tn_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "login",
    "status": "attempted",
    "ip": "1.2.3.4",
    "email": "user@example.com",
    "device_token": "dt_eyJ..."
  }'
TypeScript (Node)
const response = 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: "attempted",
    ip: req.ip,
    email: user.email,
    device_token: req.body.deviceToken, // sent from browser
  }),
});

const result = await response.json();
Python
import httpx
import os

result = httpx.post(
    "https://api.tracenow.io/trace",
    headers={"Authorization": f"Bearer {os.environ['TRACENOW_SECRET_KEY']}"},
    json={
        "event": "login",
        "status": "attempted",
        "ip": request.remote_addr,
        "email": user.email,
        "device_token": request.json.get("device_token"),
    },
).json()

Reading the verdict

{
  "verdict": "challenge",
  "policy_triggered": "Tor exit node",
  "signals_complete": true,
  "privacy_mode": true,
  "ip": {
    "is_tor": true,
    "is_anonymous": true,
    "geo": { "country": "Germany", "country_code": "DE" }
  }
}
The verdict field drives your decision:
VerdictMeaningTypical action
allowNo policy matchedContinue normally
challengeA policy matched with a soft actionRequire 2FA or CAPTCHA
denyA policy matched with a hard blockReturn 403 / show block page
policy_triggered tells you which policy fired. Use it for logging and user-facing messaging.

Handling the verdict

TypeScript (Node)
switch (result.verdict) {
  case "allow":
    return issueSession(user);

  case "challenge":
    return res.status(200).json({
      requires_challenge: true,
      reason: result.policy_triggered,
    });

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

When signals_complete is false

On a first-time lookup, some async enrichment groups may not have resolved yet. The signals_complete: false flag tells you the verdict was made on partial data. For most use cases this is fine — your policies still evaluated on what was available. If you need complete data for high-stakes decisions, retry with the same payload after a short delay (500ms–1s) to get the warmed-cache result.

Getting the user’s IP

Pass the real client IP, not your server’s outbound IP. Extract it from the forwarded header set by your reverse proxy or load balancer.
TypeScript (Next.js)
import { headers } from "next/headers";

const ip =
  headers().get("x-forwarded-for")?.split(",")[0].trim() ??
  headers().get("x-real-ip");
Never use req.socket.remoteAddress in a proxied environment — it returns your load balancer’s IP, not the user’s.