La 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 | Server 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 Server 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 Server 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 Server 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, "lastIsp": "Deutsche Telekom" }, "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", "geo": { "country": "DE", "city": "Berlin", "timezone": "Europe/Berlin", "isp": "Deutsche Telekom" }, "network": { "vpn": false, "proxy": false, "tor": false, "datacenter": false, "asn": 3320 }, "bot": { "result": "human", "score": 4.5, "antidetectScore": 2.1 }, "identification": { "confidence": 0.97, "incognito": false, "matchType": "exact", "matchConfidence": 0.99 }, "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.lastIsp | El ISP más reciente — Business y superiores |
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 |
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(`Server 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.
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 y bot.antidetectScore. Business y Enterprise añaden
geo.isp, network.asn, decision.suspectScore, identification.driftScore,
reasons, behavior, guidance, deviceInfo y los campos a nivel de persona
(personId, reputation, linkedAccountsCount, linkedVisitorsCount). 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 |