La Data API (documentada aquí anteriormente como Server API) permite que su backend lea los datos de identificación que TRACIO ya ha recogido para su workspace: el historial de un visitante, las sesiones individuales y los contadores de velocity de ventana corta.
Complementa a los webhooks en lugar de sustituirlos:
| Webhooks | Data API | |
|---|---|---|
| Dirección | TRACIO envía a su endpoint | Su backend consulta cuando lo necesita |
| Momento | A medida que ocurre cada identificación | En cualquier momento, dentro de su ventana de retención |
| Ideal para | Reaccionar a un evento | Consultar datos durante una decisión, rellenar históricos, investigaciones |
Ambas superficies están disponibles a partir del plan Pro.
https://api.tracio.ai/v1Es un host distinto del endpoint del navegador (edge.tracio.ai) y del panel
(app.tracio.ai). Los tres están separados: el navegador habla con el edge usando su
clave pública, y su backend habla con la Data API usando su clave secreta.
Cada petición lleva su clave secreta como bearer token:
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"La Data API es exclusivamente de servidor a servidor. Las cabeceras CORS deliberadamente no se devuelven, de modo que un navegador no puede llamarla: eso es lo que mantiene su clave secreta fuera del código de cliente. No envíe nunca la clave secreta al navegador.
Créela en el panel, en API Keys, eligiendo el tipo secret.
tracio_sk_ seguido de 43 caracteres, 53 en
total. El panel la lista por sus primeros caracteres para que pueda distinguir unas
claves de otras.La rotación emite una clave nueva y mantiene la antigua en funcionamiento durante 7 días, de modo que pueda desplegarla sin caídas. Despliegue la clave nueva, confirme que el tráfico se ha movido y deje que la antigua expire. Las claves públicas no son rotables: no son secretos y están visibles en el código fuente de sus páginas por diseño.
Toda ruta es un GET. En la Data API no hay operaciones de escritura: lee datos, y
su configuración vive en el panel.
| Método | Ruta | Devuelve |
|---|---|---|
GET | /v1/visitors/{visitorId} | Historial agregado de un visitante, más su sesión más reciente |
GET | /v1/visitors/{visitorId}/sessions | Lista paginada de las sesiones de ese visitante |
GET | /v1/visitors/{visitorId}/sessions/latest | La única sesión más reciente |
GET | /v1/visitors/{visitorId}/velocity | Contadores de actividad en una ventana corta |
GET | /v1/sessions/{requestId} | Una sesión, por su identificador de petición |
GET | /.well-known/webhook-keys | Claves públicas de la firma de plataforma de webhooks (sin autenticación) |
Una barra final se acepta y se ignora. Una ruta desconocida o un método equivocado devuelven el mismo sobre de error JSON que todo lo demás, nunca una página HTML o de texto plano.
Toda lectura está acotada por una ventana temporal, controlada por dos parámetros de consulta opcionales:
| Parámetro | Acepta |
|---|---|
from | YYYY-MM-DD o una marca de tiempo RFC 3339 completa |
to | YYYY-MM-DD o una marca de tiempo RFC 3339 completa |
to incluye ese día entero.400 invalid_request y el mensaje
time must be YYYY-MM-DD or RFC3339.meta, así que compruebe meta.from y meta.to en lugar de dar por hecho que su
petición se respetó al pie de la letra.curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"La respuesta lleva el historial agregado e incrusta la sesión más reciente, así que el caso habitual necesita una petición y no dos:
{ "visitorId": "X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y", "firstSeenAt": "2026-05-02T10:11:12Z", "lastSeenAt": "2026-07-25T08:00:00Z", "visits": 42, "incognitoVisits": 3, "uniqueIps": 5, "uniqueCountries": 2, "browsers": ["Chrome"], "os": ["macOS"], "devices": ["desktop"], "risk": { "maxRiskScore": 63, "avgBotScore": 12.5, "botSessions": 7, "lastDecision": "real" }, "network": { "vpnSeen": false, "proxySeen": false, "torSeen": false, "datacenterSeen": true, "proxyDetectedSeen": true, "lastIsp": "Deutsche Telekom", "lastRealIp": "203.0.113.7" }, "lastSession": { "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "visitorId": "X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y", "accountId": "user_8842", "timestamp": "2026-07-25T08:00:00Z", "tag": "checkout", "url": "https://shop.example.com/checkout", "ip": "203.0.113.42", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", "browser": { "name": "Chrome", "version": "126.0" }, "os": { "name": "macOS", "version": "14.5" }, "device": "desktop", "gpu": "Apple M2", "geo": { "country": "DE", "city": "Berlin", "timezone": "Europe/Berlin", "isp": "Deutsche Telekom" }, "network": { "vpn": false, "proxy": false, "tor": false, "datacenter": false, "proxyDetected": true, "realIp": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" }, "asn": 3320 }, "screen": { "width": 2560, "height": 1600, "colorDepth": 30, "pixelRatio": 2 }, "locale": { "languages": ["en-US", "de"], "timezone": "Europe/Berlin" }, "clientHints": { "architecture": "arm", "bitness": "64", "platformVersion": "14.5.0" }, "extensions": [ { "slug": "ublock-origin", "name": "uBlock Origin", "category": "adblock", "risky": false, "storeUrl": "https://chromewebstore.google.com/detail/cjpalhdlnbpafiamejdnhcphjbkeiagm" } ], "bot": { "result": "human", "score": 4.5, "antidetectScore": 2.1 }, "identification": { "confidence": 0.97, "incognito": false, "matchType": "exact", "matchConfidence": 0.99 }, "deviceInfo": { "deviceId": "d_4f9c2e", "crossBrowser": true, "confidence": 0.88, "linkedBrowsers": 2 }, "decision": { "action": "real", "riskScore": 12 } }, "meta": { "plan": "business", "retentionDays": 90, "from": "2026-04-26T00:00:00Z", "to": "2026-07-25T12:00:00Z" }}| Campo | Significado |
|---|---|
visits, incognitoVisits | Visitas totales en la ventana, y cuántas fueron en una ventana privada |
uniqueIps, uniqueCountries | Direcciones y países distintos observados en la ventana |
browsers, os, devices | Los entornos distintos en los que ha aparecido este visitante |
risk.maxRiskScore | La puntuación de riesgo más alta registrada en la ventana, 0..100 |
risk.lastDecision | La decisión registrada para la visita más reciente |
risk.avgBotScore, risk.botSessions | Media de la puntuación de bot y número de sesiones de bot — Business y superiores |
network.*Seen | Si alguna vez se vio un VPN, un proxy, un nodo de salida Tor o una dirección de datacenter para este visitante |
network.proxyDetectedSeen | Si al menos una visita de la ventana salió a través de un proxy o una VPN por delante del navegador — consulte network.proxyDetected en Datos del dispositivo |
network.lastIsp | El ISP más reciente — Business y superiores |
network.lastRealIp | La dirección más reciente observada detrás de un proxy o una VPN — Business y superiores; ausente cuando no se observó ninguna |
lastSession | El objeto de sesión completo de la visita más reciente |
meta | El plan, su retención en días y la ventana realmente aplicada |
Un visitante sin datos dentro de la ventana de retención devuelve 404 not_found con
el mensaje visitor not found in the retention window: eso no es un error de su
integración, significa que el visitante es nuevo o que ha quedado fuera por antigüedad.
La sesión lleva dos veredictos y responden a preguntas distintas: si el cliente estaba automatizado y qué concluyó el motor de riesgo en conjunto:
| Campo | Valores |
|---|---|
bot.result | human, bot, uncertain |
bot.type | Presente cuando bot.result es bot: o bien una herramienta concreta (playwright, puppeteer, selenium, jsdom, claude_computer_use…), o bien una familia cuando la herramienta no se identifica — automation, headless, antidetect, extension, privacy_browser, other |
decision.action | real, fake, suspicious |
bot.score y decision.riskScore van ambos de 0..100. En Business y superiores,
guidance los convierte en recomendaciones por escenario sobre la escala
allow → challenge → review → deny; consulte
Guidance para saber qué significa cada
peldaño.
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y/sessions?limit=100&minRiskScore=50"| Parámetro | Por defecto | Notas |
|---|---|---|
limit | 50 | Con tope en 500; un valor mayor se recorta, no se rechaza |
from, to | Retención del plan | La ventana temporal compartida descrita más arriba |
cursor | — | Cursor de paginación opaco de la página anterior |
botResult | — | Conservar solo las sesiones con este veredicto de bot |
minRiskScore | — | Conservar solo las sesiones con esta puntuación de riesgo o superior, 0..100 |
Las sesiones vuelven de la más reciente a la más antigua:
{ "items": [ { "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "visitorId": "X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y", "timestamp": "2026-07-25T08:00:00Z" } ], "nextCursor": "MTcyMTg5NDQwMDAwMDphYmMxMjM", "hasMore": true, "meta": { "plan": "business", "retentionDays": 90, "from": "2026-04-26T00:00:00Z", "to": "2026-07-25T12:00:00Z" }}La paginación se basa en cursores. No hay parámetro page ni offset: devuelva el
nextCursor que recibió como cursor y siga mientras hasMore sea true.
async function allSessions(visitorId: string, secretKey: string) { const sessions = [] let cursor: string | undefined
do { const url = new URL(`https://api.tracio.ai/v1/visitors/${visitorId}/sessions`) url.searchParams.set("limit", "500") if (cursor) url.searchParams.set("cursor", cursor)
const res = await fetch(url, { headers: { Authorization: `Bearer ${secretKey}` } }) if (!res.ok) throw new Error(`Data API: ${res.status}`)
const page = await res.json() sessions.push(...page.items) cursor = page.nextCursor } while (cursor)
return sessions}Trate el cursor como opaco: su contenido es un detalle de implementación y puede
cambiar. Un cursor que haya sido editado se rechaza con 400 invalid_request y el
mensaje malformed cursor.
Junto al navegador y al sistema operativo tomados del User-Agent, una sesión lleva lo que el navegador del visitante informa sobre la máquina, saneado por nuestra parte. Cada campo está ausente cuando la visita no traía esos datos, así que trate cada uno como opcional.
| Campo | Significado |
|---|---|
gpu | Modelo del adaptador de vídeo tal como lo informa el navegador (WebGL), normalizado a un nombre legible — Intel Iris Xe Graphics, Apple M1 Pro, Qualcomm Adreno 830; Software renderer significa que no hay GPU real (una máquina virtual o un entorno headless); Safari informa Apple GPU |
network.proxyDetected | El tráfico HTTP de la visita y sus rutas de red en bruto salen por redes distintas — un proxy o una VPN delante del navegador; dos direcciones del mismo proveedor (NAT del operador, una segunda salida de la misma VPN) no cuentan |
network.realIp.address, .country, .isp | La dirección pública observada en la ruta de red en bruto, es decir, la dirección detrás del proxy o de la VPN, con su país y su ISP — Business y superiores; ausente cuando no se observó ninguna dirección así (country e isp están ausentes cuando no se han podido resolver) |
deviceInfo.deviceId, .crossBrowser, .confidence, .linkedBrowsers | Presente cuando se ha resuelto una identidad de dispositivo: un id estable del dispositivo físico común a los navegadores que hay en él, si esta visita llegó por un navegador distinto al de antes, la confianza de esa coincidencia y cuántos visitantes (navegadores) distintos comparten el dispositivo — más de uno significa una sola máquina bajo varias identidades de navegador — Business y superiores |
osEnvironment | El entorno de escritorio medido en una máquina Linux (Mint 22+, Ubuntu, GNOME, KDE) — Business y superiores; ausente cuando no se determina |
spoofing | Lo que la visita afirmó frente a lo que midieron comprobaciones independientes (claimed, real, spoofedAxes de os, gpu, screen, network, browser; anonymousBrowser con nombres de producto) — Business y superiores; presente solo cuando se detectó una suplantación |
screen.width, .height, .colorDepth, .pixelRatio | Resolución de pantalla, profundidad de color y device pixel ratio tal como los informa el navegador — Business y superiores |
locale.languages, locale.timezone | Los idiomas preferidos y la zona horaria del propio navegador — a diferencia de geo.timezone, derivada de la dirección IP; una discrepancia entre ambos es una señal habitual de una ubicación falseada — Business y superiores |
clientHints.architecture, .bitness, .model, .deviceName, .platformVersion | User-Agent Client Hints: arquitectura y bitness de la CPU, código de modelo del dispositivo (Android, p. ej. SM-A556B) con su nombre comercial de la lista de dispositivos de Google Play (deviceName, p. ej. Samsung Galaxy A55 5G) y la versión exacta de la plataforma; solo navegadores basados en Chromium — Business y superiores |
environment.virtualMachine, environment.hypervisor | Presente solo cuando el adaptador de vídeo se identificó a sí mismo como virtual; hypervisor es un diccionario cerrado (vmware, virtualbox, parallels, qemu, hyperv, bochs, intel-gvt, vgpu). Un bloque ausente significa que no hay tal indicio — Business y superiores |
extensions enumera las extensiones del navegador detectadas durante la visita — Business y superiores. Cada entrada es un objeto:
| Campo | Significado |
|---|---|
slug | Identificador estable y legible por máquina de la extensión, el mismo valor que entrega el webhook |
name | Nombre legible por humanos |
category | Clase general — adblock, privacy, automation, wallet, vpn, devtools, other, etc. |
risky | true para las extensiones asociadas a automatización, suplantación o robo de credenciales |
storeUrl | Enlace a la ficha de la extensión en la tienda, cuando se conoce |
Un hallazgo solo se comunica después de superar nuestras comprobaciones de fiabilidad: un entorno que responde «instalada» a cada sondeo, o un lote de más de doce nombres, se descarta por poco fiable. Por tanto, una lista vacía o ausente significa «nada que hayamos podido confirmar», no «ninguna extensión instalada». Léala como indicio, no como inventario.
La más reciente:
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y/sessions/latest"Esto devuelve un objeto de sesión a secas: ni un array, ni envuelto en un sobre. Un
visitante sin sesiones en la ventana devuelve 404 not_found con
no sessions for this visitor in the retention window.
O por requestId, el identificador que también aparece en el payload del webhook:
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/sessions/9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31?visitorId=X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"Aquí visitorId es opcional, pero pasarlo cuando lo conoce hace la búsqueda
notablemente más rápida.
Velocity responde a «cuánta actividad ha tenido este visitante últimamente»: la forma que tienen el credential stuffing, las pruebas de tarjetas y los registros masivos.
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y/velocity?window=1h"window acepta 1h, 24h o 7d y por defecto es 24h. Cualquier otro valor se
rechaza con 400 invalid_request y window must be one of: 1h, 24h, 7d.
{ "window": "1h", "events": 37, "uniqueIps": 9, "uniqueCountries": 3, "uniqueAccounts": 12, "botEvents": 4, "meta": { "plan": "business", "retentionDays": 90, "from": "2026-07-25T11:00:00Z", "to": "2026-07-25T12:00:00Z" }}uniqueAccounts cuenta los valores linkedId distintos que ha enviado para este
dispositivo; consulte Vinculación de cuentas. botEvents es
de Business y superiores.
Un campo ausente significa «sin datos», nunca cero. Los campos sin valor se omiten
por completo en lugar de enviarse como 0, "" o null: un visitante recién llegado
no tiene matchConfidence, y una visita limpia no tiene antidetectScore ni
suspectScore. La única excepción deliberada es bot.score, que siempre está
presente incluso cuando vale cero. Lea los campos de forma defensiva.
El payload depende de su plan. Todo plan con acceso a la API recibe la sesión base: identificadores, timestamp, URL, IP, user agent, navegador, SO, dispositivo, geo, network, bot, identification y decision. Pro añade identification.matchType, identification.matchConfidence, bot.type, bot.antidetectScore, gpu y network.proxyDetected. Business y Enterprise añaden extensions, geo.isp, network.asn, network.realIp, decision.suspectScore, identification.driftScore, reasons, behavior, guidance, deviceInfo, spoofing, osEnvironment, screen, locale, clientHints y environment. Los campos a nivel de persona (personId, reputation, linkedAccountsCount, linkedVisitorsCount) están reservados a Business y Enterprise y aparecerán en cuanto se active la capa de persona: hoy funciona en modo de observación y esos campos no se entregan. Que falte un campo de Business en un plan Pro no es un error.
Los detalles internos a nivel de señal no se devuelven nunca, en ningún plan: los nombres de las señales individuales, sus pesos, los umbrales que hay detrás de un veredicto, los valores en bruto de las señales y el desglose de las puntuaciones se quedan de nuestro lado. Una puntuación que puede someterse a ingeniería inversa hasta sus entradas deja de ser útil como defensa.
Todo fallo usa un mismo sobre:
{ "error": { "code": "unauthorized", "message": "missing Authorization: Bearer <secret key>", "requestId": "8f14e45fceea167a5a36dedd" }}Este requestId no es el identificador de la visita. Dos valores distintos
comparten el nombre: dentro de un payload de sesión, requestId es el UUID de la
visita, el mismo que entrega el webhook; dentro de un sobre de error es un
identificador de traza de 24 caracteres, acuñado para cada llamada HTTP. El
identificador de traza vuelve también en la cabecera X-Request-Id en cada respuesta,
tenga éxito o no. Inclúyalo cuando contacte con soporte: es como encontramos su
llamada exacta.
| HTTP | code | Significado |
|---|---|---|
| 400 | invalid_request | Falta un parámetro o está mal formado |
| 401 | unauthorized | La clave falta, no es válida, está revocada o caducada |
| 402 | upgrade_required | Su plan no incluye acceso a la API |
| 404 | not_found | Nada coincidió dentro de la ventana de retención |
| 405 | method_not_allowed | La ruta existe, pero no para ese método |
| 429 | rate_limited | Se superaron las peticiones por segundo o la cuota diaria |
| 500 | internal | Algo falló de nuestro lado |
| 503 | unavailable | Un almacén de datos está temporalmente inaccesible |
Las comprobaciones se ejecutan en un orden fijo —clave, luego plan, luego límites—, de modo que una petición con una clave incorrecta siempre informa primero de la clave, nunca de un problema de cuota.
Dos casos de 401 se leen distinto a propósito: missing Authorization: Bearer <secret key>
significa que la cabecera nunca llegó, mientras que invalid or revoked API key
significa que llegó y no coincidió. El 402 lleva
Data API requires the Pro plan or higher.
Cada respuesta autenticada lleva su situación actual:
| Cabecera | Significado |
|---|---|
X-RateLimit-Limit | Su cuota diaria |
X-RateLimit-Remaining | Llamadas que le quedan hoy |
X-RateLimit-Reset | Hora Unix del reinicio — medianoche UTC |
Retry-After | Segundos de espera, se envía solo con un 429 |
| Plan | Peticiones por segundo | Peticiones por día | Profundidad del historial |
|---|---|---|---|
| Free | Sin acceso a la API | — | 7 días |
| Pro | 10 | 10.000 | 30 días |
| Business | 50 | 100.000 | 90 días |
| Enterprise | 200 | Sin límite | 365 días |