Events API
Integrate cside Device Intelligence into your application. Collect browser signals, generate session tokens, and retrieve detailed visitor intelligence from your backend.
This guide walks you through integrating the cside Device Intelligence Events API end-to-end: loading the client script, generating a session token, sending it to your backend, and interpreting the response payload.
Integration flow
Add the script to your page
If your app is built with a bundler, install the npm package and call initDeviceIntelligence({ teamID }) as early as your app allows. Otherwise, include the fail-open browser bootstrap at the top of your page’s <head>, before other application scripts, and replace its script.src value with the team-specific URL from the cside dashboard. The bootstrap loads client.js asynchronously, queues early calls, and rejects them if cside fails or times out so the page remains usable.
Submit telemetry and receive a session token
Once the script has loaded, a global sendClientTelemetry function becomes available (the npm package returns the same function from initDeviceIntelligence). Loading the script does not create a session fingerprint by itself. Call sendClientTelemetry(externalIds?) to submit telemetry to https://edge.csidefd.com/client and receive a session token. Pass an optional externalIds object if you want to attach your own identifiers.
const result = await sendClientTelemetry({
email: "user@example.com",
userId: "1234567890",
});
// Or call with no arguments
// const result = await sendClientTelemetry();
if (result.errors) {
console.error(result.errors);
throw new Error("Telemetry request failed.");
}
const token = result.token;Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
externalIds | Record<string, string> | No | Optional identifiers you want attached to the fingerprint, such as userId, orderId, paymentCardId, or email. |
To receive the disposable email signal (fingerprint_enrichment.throwaway_email_detected on /token/v2/verify), pass the email address in this object using the key email. The account rules and the Identity graph key on userId (the Identity graph also accepts teamId or customerId), and the card testing rule keys on paymentCardId; see Retrieving datapoints for the format.
Return value
The function returns a JSON object (already parsed, so you do not need to call .json() on it):
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}The token field holds the fingerprint session token. If telemetry collection runs into problems, the object also includes an optional errors array you can log; with the npm package, sendClientTelemetry never rejects and resolves with { token: null, errors } instead. Extract result.token and send only that session token string to your backend.
Retrieve fingerprint data from your backend
Generate a backend-only Device Intelligence API key in the cside dashboard, store it as a server-side secret, and use it to exchange the session token with cside. The API key is shown only once and must not be exposed in browser code.
if (!token) {
throw new Error("Fingerprint session token was not returned.");
}
const response = await fetch("https://api.cside.com/token/v2/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CSIDE_FINGERPRINT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
});
const data = await response.json();Request body
Send JSON in the shape { "token": "<session token>" }, where the value is result.token from the previous step. Only the /token/v2/* endpoints take JSON; the /token/v1/* endpoints take the raw session token as a text/plain body.
Token lookup options
Use the authenticated endpoint for production server-to-server exchanges. The public endpoints are still available when an integration specifically needs the unauthenticated lookup flow.
| Endpoint | Auth | Request body | Use when |
|---|---|---|---|
/token/v2/verify | API key | JSON { "token" } | You need authenticated backend retrieval with a Device Intelligence API key. |
/token/v2/device/entries | API key | JSON { "token", "limit", "offset" } | You need completed evaluations for the same device ID as a session token. |
/token/v1/client | None | Raw token, text/plain | You need the public fingerprinting response payload. |
/token/v1/clientId | None | Raw token, text/plain | You only need the stable fingerprint ID from the public lookup flow. |
Every endpoint returns 202 Accepted with { "status": "pending" } while the evaluation is still processing, then 200 OK once it has finished. /token/v2/device/entries accepts { "token": "...", "limit": 100, "offset": 0 } and returns { "device_id": "...", "entries": [...], "next_offset": 100, "has_more": true }, where each entry uses the /token/v2/verify response shape. limit is optional, defaults to 500, and is capped at 500. /token/v1/clientId returns JSON { "status": "completed", "device_id": "..." }. If no fingerprint ID exists for the session token, it returns 404.
Errors and rate limits
Error responses carry a JSON body { "error": "<message>", "code": <HTTP status> }:
| Status | Meaning |
|---|---|
400 | Malformed body, or a session token that cannot be decoded. |
401 | Missing or invalid API key, an expired token, or a token issued for a different team than the API key. |
404 | No evaluation exists for the token (token not found), or, on /token/v1/clientId, no fingerprint ID (fingerprint id not found). |
429 | Rate limit exceeded. The response carries a retry-after: 60 header. The default limit is 1,000 requests per minute per team. |
A completed evaluation that failed is not an HTTP error: it returns 200 OK with "status": "failed" and an error_message field. Check status before reading enrichment fields.
Example backend request
const csideResponse = await fetch("https://api.cside.com/token/v2/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CSIDE_FINGERPRINT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
});
const data = await csideResponse.json();Interpret the backend response
Your backend returns a JSON response containing the identification result and associated signals. See the full response reference below.
/token/v2/verify response
The authenticated endpoint returns a flat JSON payload. Each entry in /token/v2/device/entries uses the same shape.
| Field | Type | Description |
|---|---|---|
status | string | pending, completed, or failed. Enrichment fields are only present when completed. |
error_message | string | Why the evaluation failed. Only present when status is failed. |
created_at | number | Unix timestamp (milliseconds) when the evaluation was created. |
updated_at | number | Unix timestamp (milliseconds) of the last update. |
fingerprint | object | The full device fingerprint: browser, hardware, rendering, environment, and automation signals, plus the external_ids you attached. Same fields as the S3 export fingerprint object. |
network_client_metrics | object | TCP and TLS characteristics of the connection. |
geo_data | object | City-level geolocation for the visitor IP. |
ip_enrichment | object | ASN, geo, and privacy data (hosting, proxy, relay, tor, vpn, datacenter signals) for the visitor IP. |
vpn_evaluation | object | is_vpn, vpn_probability, vpn_confidence, reasons, features, and context. |
bot | object | The bot verdict: is_bot, bot_probability, bot_confidence, bot_agent_id, reasons, per-model scores and thresholds, and session_event_count. See AI agent detection. |
fingerprint_enrichment | object | Signals derived from the fingerprint. throwaway_email_detected is true when the email external ID uses a disposable domain. |
rule_evaluations | object | Legacy rule outcomes (rules, policy_version). Empty on evaluations created after 2026-08-27; kept on the wire so older records still decode. |
rule_evaluations_v2 | object | Outcomes from the rule engine: a values array where each item wraps one evaluation with rule_slug, rule_version, outcome, evaluated_as (live or shadow), transition, and evaluated_at. |
The nested objects match the S3 export record structure, where each field is documented in detail.
rule_evaluations_v2
The table above summarizes this field. It carries the outcome of every rule that ran for the domain, nested three levels deep at rule_evaluations_v2.values[].evaluation, and the values are integers rather than strings, so they are worth spelling out. It is not part of the /token/v1/client payload documented below.
| Field | Type | Description |
|---|---|---|
rule_slug | string | The rule’s identifier, for example "account_shared_detected". |
outcome | number | The evaluation result. See the table below. |
evaluated_as | number | The rule’s lifecycle state at evaluation time. See the table below. |
transition | number | Whether the outcome changed on this evaluation, independent of what the outcome is. 2 means unchanged since the last evaluation, 3 means it changed on this one. Combined with outcome: 1, a 3 means the rule just started matching; combined with outcome: 2, it means the rule just stopped matching. 1 is never produced. |
key.key_hash | string | A hash of the value the rule keyed on. This is a JSON string, not a number, so it survives JSON.parse without precision loss. It does not carry the account id itself. |
event_time, evaluated_at | object | { seconds, nanos }, unlike the response’s top-level created_at and updated_at, which are millisecond timestamps. |
outcome and evaluated_as are raw integers, not strings:
outcome | Meaning | When you’ll observe it |
|---|---|---|
0 | Unspecified | Not produced |
1 | True | Continuously, on every evaluation while the condition holds |
2 | False | Continuously, on every evaluation while it doesn’t. This is the steady state for a healthy, non-matching rule, not a rare transition |
3 | Missing (structural) | Continuously, on every evaluation where the rule can’t read the field it needs, such as a wrong external-id key. Persistent, not a one-time blip |
4 | Pending (transient) | When a required measurement exists but is not yet warm. Distinct from a top-level status of pending: re-requesting the same token will not resolve it, but a later evaluation of the same entity may return 1 or 2 |
evaluated_as | Meaning |
|---|---|
1 | Draft |
2 | Shadow (an experiment; can appear at any time) |
3 | Active |
4 | Archived |
Every evaluated, published rule reports an entry on every event, at any outcome. rule_evaluations_v2 is a record of what was evaluated, not a notification stream, so outcome: 2 (false) and outcome: 3 (missing) are not suppressed: they ride in the array just as reliably as outcome: 1. transition is what tells you whether that outcome changed since the previous evaluation.
An absent entry for a given rule_slug means the rule wasn’t evaluated at all: it isn’t activated for the domain, it’s a measurement-only rule that doesn’t publish, or evaluation didn’t run for that event. It does not mean “evaluated to false” - that case is outcome: 2, present in the array like everything else. See Detect account sharing for a worked example that reads this field end to end.
Rule explanations
Published rules that evaluate true may include rule_evaluations_v2.values[].evaluation.explanation.nodes. Each visited node can describe its node_id, outcome, typed observed value, subject, and compared_to. Explanations are optional; false, pending, and missing outcomes do not include them. A missing explanation does not change the recorded outcome.
Treat subject and compared_to as display text, not a format to parse. Use node_id, typed observed values, and outcome for programmatic handling. Read these fields from rule_evaluations_v2, not the legacy rule_evaluations object. See the Events API for outcome values and the legacy distinction.
Top-level fields
| Field | Type | Description |
|---|---|---|
linked_id | string | The browser session ID, or the event ID when no session ID was captured. It is not an identifier you supplied; your externalIds appear under tags as external_id:<key>. |
tags | object | A cside diagnostics map, not customer metadata: domain_id, team_id, request_id, session_id, fingerprint_match, network_match, detection support flags such as incognito_support, and your externalIds as external_id:<key> entries. |
timestamp | number | Unix timestamp (milliseconds) of the event. |
event_id | string | Unique identifier for this fingerprinting event. |
url | string | The page URL where the fingerprint was collected. |
ip_address | string | The visitor’s IP address. |
user_agent | string | The raw User-Agent string from the visitor’s browser. |
client_referrer | string | Always "unknown". |
status | string | completed or failed. |
error_message | string | Present when status is failed. |
domain_id, team_id, session_id, request_id | string | Evaluation metadata copied from the internal record. |
created_at | string | Evaluation time as an RFC 3339 string (unlike /token/v2/verify, which uses milliseconds). |
The response also carries network_client_metrics, vpn_evaluation, proxy_details, tampering_details, tampering_anomaly_score, velocity, and sdk objects. velocity counts are derived from the current event alone (its events.24_hours window is the only historical value); sdk reports platform: "js" with version: "unknown". These are shown in the full example but are not part of the stable contract.
browser_details
Details about the visitor’s browser and operating system.
| Field | Type | Description |
|---|---|---|
browser_name | string | Browser name (e.g. "Chrome"). |
browser_major_version | string | Major version number. |
browser_full_version | string | Full version string. |
os | string | Operating system name. |
os_version | string | Operating system version. |
device | string | Device type (e.g. "Other", "Mobile"). |
identification
The primary identification result for the visitor.
| Field | Type | Description |
|---|---|---|
visitor_id | string | The stable device ID for this visitor. This is the only computed field in the object. |
confidence.score | number | Always 0.0. |
confidence.version | string | Always "unknown". |
visitor_found | boolean | Always false. |
first_seen_at | number | Always equals timestamp. |
last_seen_at | number | Always equals timestamp. |
supplementary_id_high_recall
A secondary identifier derived from your externalIds (preferring userId, then accountId) so one account can be linked across devices. It falls back to the device ID when no external ID was attached.
| Field | Type | Description |
|---|---|---|
visitor_id | string | Hash of the chosen external ID, or the device ID when none was attached. |
visitor_found | boolean | true when an external ID was attached, otherwise false. |
confidence.score | number | Always 0.0. |
confidence.version | string | Always "unknown". |
first_seen_at | number | Always equals timestamp. |
last_seen_at | number | Always equals timestamp. |
proximity
Not populated. Always { "id": "unknown", "precision_radius": 0, "confidence": 0.0 }. Use geo_data and ip_enrichment on /token/v2/verify for location.
ip_info
IP address information for the visitor.
| Field | Type | Description |
|---|---|---|
v4.address | string | IPv4 address. |
v6.address | string | IPv6 address. |
ip_blocklist
Whether the visitor’s IP appears on known blocklists.
| Field | Type | Description |
|---|---|---|
email_spam | boolean | Not populated; always false. |
attack_source | boolean | Not populated; always false. |
tor_node | boolean | IP is a Tor exit node. |
Fraud & environment signals
Signals indicating suspicious or notable visitor attributes.
root_apps, emulator, jailbroken, frida, mitm_attack, replayed, high_activity_device, cloned_app, privacy_settings, and location_spoofing are present in the response but are never set. They always return false (factory_reset_timestamp is always 0).
Do not build detection or fraud logic on them. Each is marked in the table below.
| Field | Type | Description |
|---|---|---|
bot | string | A plain string derived only from the User-Agent: "detected" when it contains bot, crawler, spider, or headless; "unknown" when no User-Agent was captured; otherwise "not_detected". It is not the bot model. Use bot.is_bot on /token/v2/verify for the real verdict. See AI agent detection. |
root_apps | boolean | Device has root/superuser apps installed. Not currently populated; always false. |
emulator | boolean | Device is an emulator. Not currently populated; always false. |
proxy | boolean | Visitor is using a proxy, relay, or datacenter IP. |
proxy_confidence | string | "low", "medium", or "high" when proxy is true; "unknown" otherwise. |
vpn | boolean | Visitor is using a VPN. |
vpn_confidence | string | "low", "medium", or "high" when the VPN model ran; "unknown" otherwise. |
vpn_origin_timezone | string | IANA timezone reported by the browser. |
vpn_origin_country | string | Country inferred from IP enrichment, or "unknown" when unavailable. |
vpn_methods | object | Method-level signals related to VPN and anonymized traffic. See the subsection below. |
incognito | boolean | Browser is in incognito/private mode. |
tampering | boolean | Browser attributes have been tampered with. |
jailbroken | boolean | Device is jailbroken. Not currently populated; always false. |
frida | boolean | Frida instrumentation toolkit detected. Not currently populated; always false. |
virtual_machine | boolean | Visitor is running in a virtual machine. |
developer_tools | boolean | Browser developer tools are open. |
mitm_attack | boolean | Man-in-the-middle attack detected. Not currently populated; always false. |
replayed | boolean | The request is a replay of a previous request. Not currently populated; always false. |
high_activity_device | boolean | Unusually high number of identifications from this device. Not currently populated; always false. |
The disposable email signal is not part of this response. Read fingerprint_enrichment.throwaway_email_detected from /token/v2/verify instead.
vpn_methods
vpn_methods contains method-level signals related to VPN and anonymized traffic. Use the top-level vpn field as the overall verdict. A method can be true when vpn is false, such as when cside detects proxy or relay traffic that the VPN model does not classify as a VPN.
| Field | Type | Description |
|---|---|---|
timezone_mismatch | boolean | The browser timezone does not match the IP geolocation. |
public_vpn | boolean | The IP address matches a known public VPN or data center signal. |
auxiliary_mobile | boolean | Reserved for mobile-specific detection. This field is currently false for browser events. |
os_mismatch | boolean | The operating system reported by the browser does not match the network signature. |
relay | boolean | cside detected anonymizing relay, proxy, or relay-like traffic. |
Some data pipelines prefix and flatten nested fields. In those datasets, vpn_methods.relay can appear as cside_fingerprint_vpn_methods_relay.
raw_device_attributes
Low-level browser fingerprint components.
| Field | Type | Description |
|---|---|---|
math | string | Not populated; always "unknown". |
vendor | string | GPU/graphics vendor string. |
/token/v1/client response reference
This reference applies to the public /token/v1/client endpoint. It is an older, flatter view over the same evaluation. Many of its fields are fixed placeholders that were kept for backwards compatibility; the callout below lists them. For new integrations, prefer /token/v2/verify.
These fields are present in every response but never computed:
identification.confidenceandsupplementary_id_high_recall.confidenceare always{ "score": 0.0, "version": "unknown" }identification.visitor_foundis alwaysfalse;first_seen_atandlast_seen_atequal the eventtimestampproximityis always{ "id": "unknown", "precision_radius": 0, "confidence": 0.0 }client_referreris always"unknown"ip_blocklist.email_spamandip_blocklist.attack_sourceare alwaysfalse(onlytor_nodeis populated)raw_device_attributes.mathis always"unknown"
Do not build detection or fraud logic on them.
Full response example
A completed /token/v1/client response. Nested objects that are not part of the stable contract (network_client_metrics, vpn_evaluation, velocity) are abbreviated.
{
"linked_id": "b3f1c9e2-4d7a-4b0e-9f1a-2c8d7e6f5a41",
"tags": {
"domain_id": "7371466449724252160",
"team_id": "7196353823991660544",
"request_id": "01J9ZK3Q8W",
"session_id": "b3f1c9e2-4d7a-4b0e-9f1a-2c8d7e6f5a41",
"fingerprint_match": "7371466449724252160",
"external_id:userId": "1234567890",
"incognito_support": "supported",
"developer_tools_support": "supported",
"virtual_machine_support": "heuristic",
"tor_support": "ipinfo_privacy",
"visit_summary_source": "recent_fingerprint.available"
},
"timestamp": 1708102555327,
"event_id": "289076244130249728",
"url": "http://www.example.com/login",
"ip_address": "61.127.217.15",
"user_agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) ...",
"client_referrer": "unknown",
"browser_details": {
"browser_name": "Chrome",
"browser_major_version": "74",
"browser_full_version": "74.0.3729",
"os": "Windows",
"os_version": "7",
"device": "Other"
},
"identification": {
"visitor_id": "Ibk1527CUFmcnjLwIs4A9",
"confidence": { "score": 0.0, "version": "unknown" },
"visitor_found": false,
"first_seen_at": 1708102555327,
"last_seen_at": 1708102555327
},
"supplementary_id_high_recall": {
"visitor_id": "3HNey93AkBW6CRbxV6xP",
"visitor_found": true,
"confidence": { "score": 0.0, "version": "unknown" },
"first_seen_at": 1708102555327,
"last_seen_at": 1708102555327
},
"proximity": {
"id": "unknown",
"precision_radius": 0,
"confidence": 0.0
},
"bot": "not_detected",
"root_apps": false,
"emulator": false,
"ip_info": {
"v4": { "address": "94.142.239.124" },
"v6": { "address": "2001:db8:3333:4444:5555:6666:7777:8888" }
},
"ip_blocklist": {
"email_spam": false,
"attack_source": false,
"tor_node": false
},
"proxy": true,
"proxy_confidence": "low",
"proxy_details": {
"proxy_type": "datacenter",
"last_seen_at": 1708102555327,
"provider": "Example Hosting"
},
"vpn": false,
"vpn_confidence": "high",
"vpn_origin_timezone": "Europe/Amsterdam",
"vpn_origin_country": "NL",
"vpn_methods": {
"timezone_mismatch": false,
"public_vpn": true,
"auxiliary_mobile": false,
"os_mismatch": false,
"relay": true
},
"vpn_evaluation": { "is_vpn": false, "vpn_probability": 0.04, "vpn_confidence": 0.91 },
"incognito": false,
"tampering": false,
"tampering_anomaly_score": 0.0,
"tampering_anti_detect_browser": false,
"tampering_details": { "anomaly_score": 0.0, "anti_detect_browser": false },
"cloned_app": false,
"factory_reset_timestamp": 0,
"jailbroken": false,
"frida": false,
"privacy_settings": false,
"virtual_machine": false,
"location_spoofing": false,
"velocity": {
"distinct_ip": { "5_minutes": 1, "1_hour": 1, "24_hours": 1 },
"distinct_country": { "5_minutes": 0, "1_hour": 0, "24_hours": 0 },
"events": { "5_minutes": 1, "1_hour": 1, "24_hours": 3 },
"ip_events": { "5_minutes": 1, "1_hour": 1, "24_hours": 1 },
"distinct_ip_by_linked_id": { "5_minutes": 1, "1_hour": 1, "24_hours": 1 },
"distinct_visitor_id_by_linked_id": { "5_minutes": 1, "1_hour": 1, "24_hours": 1 }
},
"developer_tools": false,
"mitm_attack": false,
"sdk": { "platform": "js", "version": "unknown", "integrations": [] },
"replayed": false,
"high_activity_device": false,
"raw_device_attributes": {
"math": "unknown",
"vendor": "Google Inc."
},
"network_client_metrics": { "client_ip": "61.127.217.15", "tls_ja4": "t13d1516h2_8daaf6152771_b186095e22b6" },
"status": "completed",
"domain_id": "7371466449724252160",
"team_id": "7196353823991660544",
"session_id": "b3f1c9e2-4d7a-4b0e-9f1a-2c8d7e6f5a41",
"request_id": "01J9ZK3Q8W",
"created_at": "2024-02-16T17:35:55.327Z"
} Thanks for your feedback!