Skip to main content
This is the in-app layer of the Cloudflare integration. The Cloudflare Worker 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.
Complete the Cloudflare integration 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.

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
Python

Full login example

1

Read the device token from the header

The Worker has already attached it. Extract it before you do anything else.
2

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

Switch on the verdict

allow → proceed, challenge → require step-up, deny → block.

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

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

What the in-app call adds