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

# Trace

> Fan-out enrichment across all signals with policy evaluation and a verdict.

`POST https://api.tracenow.io/trace`

The trace endpoint runs all provided signals (IP, email, phone, device token) in parallel, evaluates your configured policies against the enriched result, tracks velocity counters, and returns a single **verdict**: `allow`, `challenge`, or `deny`.

<Note>
  The [Cloudflare Worker](/guides/cloudflare) calls this endpoint automatically for every monitored
  route. This reference documents the request and response shape for your **in-app call** (where you
  add the real `subject_user_id` and confirmed `status`) and explains how to read the response in
  either context.
</Note>

## Authentication

All requests require a Bearer token using your secret key:

```
Authorization: Bearer tn_live_...
```

Secret keys are available in your dashboard under **Settings → API keys**. Never expose them client-side.

## Request

### Fields

| Field             | Type   | Required | Notes                                                                                                                                              |
| ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event`           | string | Yes      | One of `signup`, `login`, `otp`, `transaction`.                                                                                                    |
| `status`          | string | Yes      | One of `attempted`, `succeeded`, `failed`.                                                                                                         |
| `ip`              | string | No       | IPv4 or IPv6 address.                                                                                                                              |
| `email`           | string | No       | Email address to enrich.                                                                                                                           |
| `phone`           | string | No       | Phone number in any format.                                                                                                                        |
| `device_token`    | string | No       | Device token from the `x-tn-device-token` header (attached by the Cloudflare Worker).                                                              |
| `subject_user_id` | string | No       | Your internal user ID. Providing it unlocks per-user velocity signals (`velocity.user.*`, `ip.is_new_country_for_user`, `device.is_new_for_user`). |
| `visitor_id`      | string | No       | Persistent visitor identifier, if your app tracks one separately.                                                                                  |
| `properties`      | object | No       | Arbitrary key/value pairs stored with the trace.                                                                                                   |

Send whatever you have: not all fields are required, but the more you provide, the richer the signals.

### Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.tracenow.io/trace \
    -H "Authorization: Bearer tn_live_xxxxxxxxxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "event": "login",
      "status": "succeeded",
      "ip": "1.2.3.4",
      "email": "user@example.com",
      "device_token": "dt_eyJ...",
      "subject_user_id": "usr_abc123"
    }'
  ```

  ```typescript TypeScript theme={null}
  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: "succeeded",
      ip: clientIp,
      email: user.email,
      device_token: req.headers["x-tn-device-token"],
      subject_user_id: user.id,
    }),
  });

  const trace = await response.json();
  ```

  ```python Python theme={null}
  import httpx
  import os

  trace = httpx.post(
      "https://api.tracenow.io/trace",
      headers={"Authorization": f"Bearer {os.environ['TRACENOW_SECRET_KEY']}"},
      json={
          "event": "login",
          "status": "succeeded",
          "ip": client_ip,
          "email": user.email,
          "device_token": request.headers.get("x-tn-device-token"),
          "subject_user_id": str(user.id),
      },
  ).json()
  ```
</CodeGroup>

## Response

```json theme={null}
{
  "trace_id": "tr_...",
  "verdict": "allow",
  "event_type": "login",
  "status": "attempted",
  "signals": {
    "ip": {
      "is_tor": false,
      "is_vpn": false,
      "is_proxy": false,
      "is_hosting": false,
      "is_new_country_for_user": false,
      "impossible_travel": false,
      "geo": { "country": "United States", "country_code": "US" },
      "as": { "asn": "AS15169", "name": "Google LLC", "type": "hosting" }
    },
    "email": {
      "is_disposable": false,
      "domain_age_days": 4012,
      "email_randomness_score": 1,
      "suspicious_tld": false
    },
    "phone": {
      "valid": true,
      "is_voip": false,
      "is_burner": false,
      "carrier_country_risk": "low"
    },
    "device": {
      "visitor_id": "dv_...",
      "is_headless": false,
      "automation_detected": false,
      "timezone_ip_mismatch": false,
      "is_new_for_user": null
    },
    "velocity": {
      "ip": { "trace_count_5m": 1, "trace_count_1h": 2, "failed_count_1h": 0 },
      "user": { "trace_count_5m": 1, "trace_count_1h": 1, "failed_count_1h": 0, "distinct_ips_24h": 1 },
      "device": { "distinct_user_ids_24h": 1 }
    },
    "decision": {
      "rule_id": "...",
      "rule_name": "Block credential stuffing",
      "logic": "AND",
      "matched_conditions": []
    }
  },
  "signals_complete": true,
  "duration_ms": 38,
  "policy": {
    "id": "...",
    "name": "Account takeover — login",
    "action": "deny",
    "rule_id": "...",
    "rule_name": "Block credential stuffing"
  }
}
```

### Top-level fields

| Field              | Type                             | Description                                                                                 |
| ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------- |
| `trace_id`         | string                           | Unique identifier for this trace (`tr_...`).                                                |
| `verdict`          | `allow` \| `challenge` \| `deny` | Policy evaluation result. `allow` if no policy matched.                                     |
| `event_type`       | string                           | Echo of the `event` you sent.                                                               |
| `status`           | string                           | Echo of the `status` you sent.                                                              |
| `signals`          | object                           | All enrichment results. See sub-objects below.                                              |
| `signals_complete` | boolean                          | `false` if any async enrichment group had not resolved yet. See [below](#signals_complete). |
| `duration_ms`      | number                           | Wall-clock time for the enrichment fan-out, in milliseconds.                                |
| `policy`           | object \| null                   | Present only when a rule matched. Contains the matched policy and rule identifiers.         |

### signals.ip

| Field                     | Type            | Description                                                                           |
| ------------------------- | --------------- | ------------------------------------------------------------------------------------- |
| `is_tor`                  | boolean         | IP is a known Tor exit node.                                                          |
| `is_vpn`                  | boolean         | IP is associated with a commercial VPN.                                               |
| `is_proxy`                | boolean         | IP is an open or anonymous proxy.                                                     |
| `is_hosting`              | boolean         | IP belongs to a data center or hosting provider.                                      |
| `is_new_country_for_user` | boolean \| null | Country differs from this user's previous activity. Requires `subject_user_id`.       |
| `impossible_travel`       | boolean \| null | Geo jump is physically impossible given the time elapsed. Requires `subject_user_id`. |
| `geo`                     | object          | `{ country, country_code }`: human-readable country name and ISO 3166-1 alpha-2 code. |
| `as`                      | object          | `{ asn, name, type }`: autonomous system number, name, and category.                  |

### signals.email

| Field                    | Type    | Description                                               |
| ------------------------ | ------- | --------------------------------------------------------- |
| `is_disposable`          | boolean | Address belongs to a disposable/temporary email service.  |
| `domain_age_days`        | number  | Age of the email domain in days.                          |
| `email_randomness_score` | number  | 0–10 score of how machine-generated the local part looks. |
| `suspicious_tld`         | boolean | Domain uses a TLD commonly associated with abuse.         |

### signals.phone

| Field                  | Type    | Description                                                      |
| ---------------------- | ------- | ---------------------------------------------------------------- |
| `valid`                | boolean | Number is structurally valid and dialable.                       |
| `is_voip`              | boolean | Number is a VoIP line.                                           |
| `is_burner`            | boolean | Number is from a known burner/prepaid provider.                  |
| `carrier_country_risk` | string  | Risk level of the carrier's country: `low`, `medium`, or `high`. |

### signals.device

| Field                  | Type            | Description                                                                        |
| ---------------------- | --------------- | ---------------------------------------------------------------------------------- |
| `visitor_id`           | string          | Stable device identifier (`dv_...`). Populated when a `device_token` was provided. |
| `is_headless`          | boolean         | Browser was running headless (e.g. Puppeteer, Playwright).                         |
| `automation_detected`  | boolean         | Automation framework fingerprint detected.                                         |
| `timezone_ip_mismatch` | boolean         | Browser timezone does not match IP geolocation.                                    |
| `is_new_for_user`      | boolean \| null | This device has not been seen for this user before. Requires `subject_user_id`.    |

### signals.velocity

| Field                          | Type   | Description                                                                       |
| ------------------------------ | ------ | --------------------------------------------------------------------------------- |
| `ip.trace_count_5m`            | number | Traces from this IP in the last 5 minutes.                                        |
| `ip.trace_count_1h`            | number | Traces from this IP in the last hour.                                             |
| `ip.failed_count_1h`           | number | Failed-status traces from this IP in the last hour.                               |
| `user.trace_count_5m`          | number | Traces for this user in the last 5 minutes. Requires `subject_user_id`.           |
| `user.trace_count_1h`          | number | Traces for this user in the last hour. Requires `subject_user_id`.                |
| `user.failed_count_1h`         | number | Failed-status traces for this user in the last hour. Requires `subject_user_id`.  |
| `user.distinct_ips_24h`        | number | Distinct IPs seen for this user in the last 24 hours. Requires `subject_user_id`. |
| `device.distinct_user_ids_24h` | number | Distinct user IDs seen on this device in the last 24 hours.                       |

### signals.decision

Present when a rule matched. Contains the rule that drove the verdict.

| Field                | Type          | Description                                     |
| -------------------- | ------------- | ----------------------------------------------- |
| `rule_id`            | string        | Internal ID of the matched rule.                |
| `rule_name`          | string        | Display name of the matched rule.               |
| `logic`              | `AND` \| `OR` | How the rule's conditions were combined.        |
| `matched_conditions` | array         | The specific conditions that evaluated to true. |

### policy

Present only when a rule matched.

| Field       | Type                  | Description                                |
| ----------- | --------------------- | ------------------------------------------ |
| `id`        | string                | Policy ID.                                 |
| `name`      | string                | Policy display name.                       |
| `action`    | `challenge` \| `deny` | The action the matched rule specified.     |
| `rule_id`   | string                | ID of the specific rule within the policy. |
| `rule_name` | string                | Display name of the matched rule.          |

## Verdicts

| Verdict     | When it occurs                    | Recommended action                          |
| ----------- | --------------------------------- | ------------------------------------------- |
| `allow`     | No policy matched                 | Proceed normally.                           |
| `challenge` | A rule matched with a soft action | Require step-up: MFA, CAPTCHA, OTP.         |
| `deny`      | A rule matched with a hard action | Block the request (return 403 or redirect). |

`allow` is the implicit default: it means your policies evaluated and nothing triggered.

<Note>
  At the Cloudflare edge, only `deny` blocks. `challenge` and `allow` both forward to your origin
  because the Worker cannot render your MFA flow. Enforcing `challenge` requires the in-app call
  described in [Server-side integration](/guides/server-side).
</Note>

## signals\_complete

`signals_complete: false` means some enrichment group had not fully resolved before the response was returned, typically on the first request for a new IP or email (a cache miss). The verdict was still evaluated against whatever data was available.

For most use cases this is acceptable. If you need complete data for a high-stakes decision, retry with the same payload after \~500 ms to get the warm-cache result.
