La Server API consente al suo backend di leggere i dati di identificazione che TRACIO ha già raccolto per il suo workspace: la cronologia di un visitatore, le singole sessioni e i contatori di velocity su finestre brevi.
Integra i Webhook invece di sostituirli:
| Webhook | Server API | |
|---|---|---|
| Direzione | TRACIO invia al suo endpoint | Il suo backend interroga su richiesta |
| Tempistica | Nel momento di ogni identificazione | In qualsiasi momento, entro la sua finestra di conservazione |
| Ideale per | Reagire a un evento | Consultare i dati durante una decisione, backfill, indagini |
Entrambe le superfici sono disponibili dal piano Pro in su.
https://api.tracio.ai/v1Si tratta di un host diverso da quello dell'endpoint del browser (edge.tracio.ai) e
dalla dashboard (app.tracio.ai). Tutti e tre sono separati: il browser parla con
l'edge usando la sua chiave pubblica, il suo backend parla con la Server API usando
la sua chiave segreta.
Ogni richiesta porta la sua chiave segreta come bearer token:
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"La Server API è solo server-to-server. Gli header CORS non vengono deliberatamente restituiti, quindi un browser non può chiamarla — è questo che tiene la sua chiave segreta fuori dal codice lato client. Non spedisca mai la chiave segreta al browser.
La crei nella dashboard, sotto API Keys, scegliendo il tipo secret.
tracio_sk_ seguito da 43 caratteri, 53 in totale. La
dashboard la elenca con i suoi primi caratteri, così può distinguere le chiavi fra
loro.La rotazione emette una nuova chiave e mantiene funzionante la vecchia per 7 giorni, così può distribuirla senza interruzioni di servizio. Distribuisca la nuova chiave, verifichi che il traffico si sia spostato e lasci scadere la vecchia. Le chiavi pubbliche non sono ruotabili — non sono segreti e sono visibili nel sorgente della sua pagina per scelta progettuale.
Ogni rotta è un GET. Nella Server API non esistono operazioni di scrittura: legge i
dati, mentre la sua configurazione risiede nella dashboard.
| Metodo | Percorso | Restituisce |
|---|---|---|
GET | /v1/visitors/{visitorId} | Cronologia aggregata di un visitatore, più la sua ultima sessione |
GET | /v1/visitors/{visitorId}/sessions | Elenco paginato delle sessioni di quel visitatore |
GET | /v1/visitors/{visitorId}/sessions/latest | La singola sessione più recente |
GET | /v1/visitors/{visitorId}/velocity | Contatori di attività su una finestra breve |
GET | /v1/sessions/{requestId} | Una sessione tramite il suo identificatore di richiesta |
GET | /.well-known/webhook-keys | Chiavi pubbliche per la firma di piattaforma dei webhook (senza autenticazione) |
Una barra finale viene accettata e ignorata. Un percorso sconosciuto o un metodo errato restituisce lo stesso envelope JSON di errore di tutto il resto, mai una pagina HTML o di testo semplice.
Ogni lettura è delimitata da una finestra temporale, controllata da due parametri di query facoltativi:
| Parametro | Accetta |
|---|---|
from | YYYY-MM-DD oppure un timestamp RFC 3339 completo |
to | YYYY-MM-DD oppure un timestamp RFC 3339 completo |
to include l'intera giornata.400 invalid_request e il messaggio
time must be YYYY-MM-DD or RFC3339.meta, quindi controlli meta.from e meta.to invece di dare per
scontato che la sua richiesta sia stata rispettata alla lettera.curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"La risposta porta la cronologia aggregata e incorpora l'ultima sessione, così il caso comune richiede una richiesta invece di due:
{ "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 | Significato |
|---|---|
visits, incognitoVisits | Visite totali nella finestra e quante di esse in una finestra privata |
uniqueIps, uniqueCountries | Indirizzi e paesi distinti visti nella finestra |
browsers, os, devices | Gli ambienti distinti in cui questo visitatore è comparso |
risk.maxRiskScore | Il punteggio di rischio più alto registrato nella finestra, 0..100 |
risk.lastDecision | La decisione registrata per la visita più recente |
risk.avgBotScore, risk.botSessions | Media del punteggio di bot e numero di sessioni bot — da Business in su |
network.*Seen | Se per questo visitatore siano mai stati visti una VPN, un proxy, un nodo di uscita Tor o un indirizzo di datacenter |
network.lastIsp | L'ISP più recente — da Business in su |
lastSession | L'oggetto sessione completo della visita più recente |
meta | Il piano, la sua conservazione in giorni e la finestra effettivamente applicata |
Un visitatore senza dati all'interno della finestra di conservazione restituisce
404 not_found con il messaggio visitor not found in the retention window — non è un
errore della sua integrazione, significa che il visitatore è nuovo oppure è uscito dalla
finestra per anzianità.
La sessione porta due verdetti, che rispondono a domande diverse — se il client fosse automatizzato e che cosa abbia concluso complessivamente il motore di rischio:
| Campo | Valori |
|---|---|
bot.result | human, bot, uncertain |
decision.action | real, fake, suspicious |
bot.score e decision.riskScore vanno entrambi da 0..100. Da Business in su,
guidance li trasforma in indicazioni per singolo scenario sulla scala
allow → challenge → review → deny — veda
Guidance per il significato di ciascun
gradino.
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y/sessions?limit=100&minRiskScore=50"| Parametro | Predefinito | Note |
|---|---|---|
limit | 50 | Limitato a 500; un valore maggiore viene ridotto, non rifiutato |
from, to | Conservazione del piano | La finestra temporale condivisa descritta sopra |
cursor | — | Cursore di paginazione opaco della pagina precedente |
botResult | — | Mantiene solo le sessioni con questo verdetto di bot |
minRiskScore | — | Mantiene solo le sessioni con punteggio di rischio pari o superiore, 0..100 |
Le sessioni tornano dalla più recente:
{ "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 paginazione è basata su cursore. Non esiste un parametro page o offset: rimandi
il nextCursor ricevuto come cursor e prosegua finché hasMore è 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}Tratti il cursore come opaco — il suo contenuto è un dettaglio implementativo e può
cambiare. Un cursore che è stato modificato viene rifiutato con 400 invalid_request e
il messaggio malformed cursor.
La più recente:
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y/sessions/latest"Restituisce un oggetto sessione nudo — non un array e non avvolto in un envelope. Un
visitatore senza sessioni nella finestra restituisce 404 not_found con
no sessions for this visitor in the retention window.
Oppure tramite requestId, l'identificatore che compare anche nel payload del webhook:
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/sessions/9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31?visitorId=X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"Qui visitorId è facoltativo, ma passarlo quando lo conosce rende la ricerca nettamente
più veloce.
Velocity risponde alla domanda «quanto ha fatto ultimamente questo visitatore» — la forma del credential stuffing, del card testing e delle registrazioni di massa.
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y/velocity?window=1h"window accetta 1h, 24h o 7d e il valore predefinito è 24h. Qualsiasi altro
valore viene rifiutato con 400 invalid_request e
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 conta i valori distinti di linkedId che ha inviato per questo
dispositivo — veda Collegamento degli account. botEvents è da
Business in su.
Un campo assente significa «nessun dato», mai zero. I campi senza valore vengono
omessi del tutto invece di essere inviati come 0, "" o null: un visitatore
nuovissimo non ha matchConfidence, una visita pulita non ha antidetectScore né
suspectScore. L'unica eccezione deliberata è bot.score, sempre presente anche quando
vale zero. Legga i campi in modo difensivo.
Il payload dipende dal suo piano. Ogni piano con accesso all'API riceve la sessione
di base — identificatori, timestamp, URL, IP, user agent, browser, sistema operativo,
dispositivo, geo, rete, bot, identificazione e decisione. Pro aggiunge
identification.matchType, identification.matchConfidence e bot.antidetectScore.
Business ed Enterprise aggiungono geo.isp, network.asn, decision.suspectScore,
identification.driftScore, reasons, behavior, guidance, deviceInfo e i campi a
livello di persona (personId, reputation, linkedAccountsCount,
linkedVisitorsCount). L'assenza di un campo Business su un piano Pro non è un errore.
Gli interni a livello di segnale non vengono mai restituiti, su nessun piano: i nomi dei singoli segnali, i loro pesi, le soglie dietro un verdetto, i valori grezzi dei segnali e la scomposizione dei punteggi restano dalla nostra parte. Un punteggio che può essere sottoposto a reverse engineering fino ai suoi input smette di essere utile come difesa.
Ogni errore usa un unico envelope:
{ "error": { "code": "unauthorized", "message": "missing Authorization: Bearer <secret key>", "requestId": "8f14e45fceea167a5a36dedd" }}Questo requestId non è l'identificatore della visita. Due valori diversi
condividono il nome: dentro il payload di una sessione requestId è l'UUID della
visita, lo stesso che recapita il webhook; dentro un envelope di errore è un
identificatore di traccia di 24 caratteri, generato per ogni chiamata HTTP.
L'identificatore di traccia torna anche nell'header X-Request-Id di ogni risposta,
riuscita o meno. Lo includa quando contatta il supporto — è così che troviamo la sua
chiamata esatta.
| HTTP | code | Significato |
|---|---|---|
| 400 | invalid_request | Un parametro manca o è malformato |
| 401 | unauthorized | La chiave è assente, non valida, revocata o scaduta |
| 402 | upgrade_required | Il suo piano non include l'accesso all'API |
| 404 | not_found | Nulla ha trovato corrispondenza dentro la finestra di conservazione |
| 405 | method_not_allowed | La rotta esiste, ma non per quel metodo |
| 429 | rate_limited | Superate le richieste al secondo o la quota giornaliera |
| 500 | internal | Qualcosa è fallito dalla nostra parte |
| 503 | unavailable | Uno store di supporto è temporaneamente irraggiungibile |
I controlli avvengono in un ordine fisso — chiave, poi piano, poi limiti — così una richiesta con una chiave errata segnala sempre prima la chiave, mai un problema di quota.
Due casi di 401 si leggono in modo diverso di proposito:
missing Authorization: Bearer <secret key> significa che l'header non è mai arrivato,
mentre invalid or revoked API key significa che è arrivato e non ha trovato
corrispondenza. 402 porta Data API requires the Pro plan or higher.
Ogni risposta autenticata porta la sua situazione corrente:
| Header | Significato |
|---|---|
X-RateLimit-Limit | La sua quota giornaliera |
X-RateLimit-Remaining | Chiamate rimaste oggi |
X-RateLimit-Reset | Orario Unix del reset — mezzanotte UTC |
Retry-After | Secondi da attendere, inviato solo con un 429 |
| Piano | Richieste al secondo | Richieste al giorno | Profondità della cronologia |
|---|---|---|---|
| Free | Nessun accesso API | — | 7 giorni |
| Pro | 10 | 10.000 | 30 giorni |
| Business | 50 | 100.000 | 90 giorni |
| Enterprise | 200 | Illimitate | 365 giorni |