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
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.
POST to /trace
Send the signals to the API. The response includes the enriched data and a verdict.
Act on the verdict
allow — proceed normally. challenge — step up (e.g. 2FA, CAPTCHA). deny — block the request.
Making a trace request
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..."
}'
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();
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:
| Verdict | Meaning | Typical action |
|---|
allow | No policy matched | Continue normally |
challenge | A policy matched with a soft action | Require 2FA or CAPTCHA |
deny | A policy matched with a hard block | Return 403 / show block page |
policy_triggered tells you which policy fired. Use it for logging and user-facing messaging.
Handling the verdict
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.
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.