La Server API permet à votre backend de lire les données d'identification que TRACIO a déjà collectées pour votre espace de travail : l'historique d'un visiteur, les sessions individuelles et les compteurs de velocity sur une courte fenêtre.
Elle complète les webhooks plutôt qu'elle ne les remplace :
| Webhooks | Server API | |
|---|---|---|
| Direction | TRACIO pousse vers votre endpoint | Votre backend interroge à la demande |
| Moment | À chaque identification | À tout moment, sur votre fenêtre de rétention |
| Idéal pour | Réagir à un événement | Consulter des données pendant une décision, des reprises, des investigations |
Les deux surfaces sont disponibles à partir de l'offre Pro.
https://api.tracio.ai/v1C'est un hôte différent de l'endpoint navigateur (edge.tracio.ai) et du tableau de
bord (app.tracio.ai). Les trois sont distincts : le navigateur s'adresse à l'edge
avec votre clé publique, votre backend s'adresse à la Server API avec votre clé
secrète.
Chaque requête porte votre clé secrète sous forme de bearer token :
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"La Server API est strictement de serveur à serveur. Les en-têtes CORS ne sont délibérément pas renvoyés, si bien qu'un navigateur ne peut pas l'appeler — c'est ce qui garde votre clé secrète hors du code côté client. Ne livrez jamais la clé secrète au navigateur.
Créez-la dans le tableau de bord, sous API Keys, en choisissant le type secret.
tracio_sk_ suivi de 43 caractères, 53 au total. Le
tableau de bord la liste par ses premiers caractères, ce qui vous permet de
distinguer les clés entre elles.La rotation émet une nouvelle clé et laisse l'ancienne fonctionner pendant 7 jours, ce qui vous permet de la déployer sans interruption de service. Déployez la nouvelle clé, vérifiez que le trafic est passé dessus, puis laissez l'ancienne expirer. Les clés publiques ne sont pas rotatives — ce ne sont pas des secrets et elles sont visibles dans le source de vos pages, par conception.
Chaque route est un GET. Il n'y a aucune opération d'écriture dans la Server API :
elle lit des données, et votre configuration vit dans le tableau de bord.
| Méthode | Chemin | Renvoie |
|---|---|---|
GET | /v1/visitors/{visitorId} | Historique agrégé d'un visiteur, plus sa session la plus récente |
GET | /v1/visitors/{visitorId}/sessions | Liste paginée des sessions de ce visiteur |
GET | /v1/visitors/{visitorId}/sessions/latest | L'unique session la plus récente |
GET | /v1/visitors/{visitorId}/velocity | Compteurs d'activité sur une courte fenêtre |
GET | /v1/sessions/{requestId} | Une session, par son identifiant de requête |
GET | /.well-known/webhook-keys | Clés publiques de la signature de plateforme des webhooks (sans auth) |
Une barre oblique finale est acceptée et ignorée. Un chemin inconnu ou une mauvaise méthode renvoie la même enveloppe d'erreur JSON que tout le reste, jamais une page HTML ou en texte brut.
Chaque lecture est bornée par une fenêtre temporelle, contrôlée par deux paramètres de requête optionnels :
| Paramètre | Accepte |
|---|---|
from | YYYY-MM-DD ou un horodatage RFC 3339 complet |
to | YYYY-MM-DD ou un horodatage RFC 3339 complet |
to inclut la journée entière.400 invalid_request et le message
time must be YYYY-MM-DD or RFC3339.meta : vérifiez donc meta.from et meta.to au lieu de
supposer que votre requête a été honorée telle quelle.curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"La réponse porte l'historique agrégé et embarque la session la plus récente : le cas courant ne demande donc qu'une requête au lieu de deux :
{ "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" }}| Champ | Signification |
|---|---|
visits, incognitoVisits | Total des visites dans la fenêtre, et combien l'ont été dans une fenêtre de navigation privée |
uniqueIps, uniqueCountries | Adresses et pays distincts observés dans la fenêtre |
browsers, os, devices | Les environnements distincts dans lesquels ce visiteur est apparu |
risk.maxRiskScore | Le score de risque le plus élevé enregistré dans la fenêtre, 0..100 |
risk.lastDecision | La décision enregistrée pour la visite la plus récente |
risk.avgBotScore, risk.botSessions | Moyenne du score de bot et nombre de sessions de bot — Business et au-delà |
network.*Seen | Si un VPN, un proxy, un nœud de sortie Tor ou une adresse de datacenter a déjà été vu pour ce visiteur |
network.lastIsp | L'ISP le plus récent — Business et au-delà |
lastSession | L'objet session complet de la visite la plus récente |
meta | L'offre, sa rétention en jours et la fenêtre réellement appliquée |
Un visiteur sans aucune donnée dans la fenêtre de rétention renvoie 404 not_found
avec le message visitor not found in the retention window — ce n'est pas une erreur
dans votre intégration : cela signifie que le visiteur est nouveau ou qu'il est sorti
de la fenêtre.
La session porte deux verdicts, et ils répondent à des questions différentes — si le client était automatisé, et ce que le moteur de risque a conclu globalement :
| Champ | Valeurs |
|---|---|
bot.result | human, bot, uncertain |
decision.action | real, fake, suspicious |
bot.score et decision.riskScore vont tous deux de 0..100. À partir de Business,
guidance les transforme en recommandations par scénario sur l'échelle
allow → challenge → review → deny — voir
Guidance pour la signification de chaque
barreau.
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y/sessions?limit=100&minRiskScore=50"| Paramètre | Défaut | Notes |
|---|---|---|
limit | 50 | Plafonné à 500 ; une valeur plus grande est ramenée à la limite, pas rejetée |
from, to | Rétention de l'offre | La fenêtre temporelle partagée décrite plus haut |
cursor | — | Curseur de pagination opaque issu de la page précédente |
botResult | — | Ne garder que les sessions portant ce verdict de bot |
minRiskScore | — | Ne garder que les sessions dont le score de risque atteint ce seuil, 0..100 |
Les sessions reviennent de la plus récente à la plus ancienne :
{ "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 pagination est fondée sur un curseur. Il n'y a ni paramètre page ni paramètre
offset : renvoyez le nextCursor reçu en tant que cursor et continuez tant que
hasMore vaut 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}Traitez le curseur comme opaque — son contenu est un détail d'implémentation et peut
changer. Un curseur qui a été modifié est rejeté avec 400 invalid_request et le
message malformed cursor.
La plus récente :
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y/sessions/latest"Cela renvoie un objet session nu — pas un tableau, et sans enveloppe autour. Un
visiteur sans aucune session dans la fenêtre renvoie 404 not_found avec
no sessions for this visitor in the retention window.
Ou bien par requestId, l'identifiant qui apparaît aussi dans le payload du webhook :
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/sessions/9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31?visitorId=X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"visitorId est optionnel ici, mais le passer quand vous le connaissez rend la
recherche nettement plus rapide.
Velocity répond à la question « quelle a été l'activité récente de ce visiteur » — la forme que prennent le credential stuffing, le test de cartes et les inscriptions en masse.
curl -H "Authorization: Bearer tracio_sk_XXXX...XXXX" \ "https://api.tracio.ai/v1/visitors/X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y/velocity?window=1h"window accepte 1h, 24h ou 7d et vaut 24h par défaut. Toute autre valeur est
rejetée avec 400 invalid_request et 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 compte les valeurs linkedId distinctes que vous avez envoyées pour
cet appareil — voir Liaison de comptes. botEvents est
réservé à Business et au-delà.
Un champ absent signifie « pas de donnée », jamais zéro. Les champs sans valeur
sont entièrement omis plutôt qu'envoyés à 0, "" ou null : un visiteur tout
nouveau n'a pas de matchConfidence, une visite propre n'a ni antidetectScore ni
suspectScore. La seule exception délibérée est bot.score, toujours présent même
lorsqu'il vaut zéro. Lisez les champs de manière défensive.
Le payload dépend de votre offre. Toute offre disposant d'un accès API reçoit la
session de base — identifiants, horodatage, URL, IP, user agent, navigateur, OS,
appareil, geo, network, bot, identification et decision. Pro ajoute
identification.matchType, identification.matchConfidence et
bot.antidetectScore. Business et Enterprise ajoutent geo.isp, network.asn,
decision.suspectScore, identification.driftScore, reasons, behavior,
guidance, deviceInfo et les champs au niveau de la personne (personId,
reputation, linkedAccountsCount, linkedVisitorsCount). L'absence d'un champ
Business sur une offre Pro n'est pas une erreur.
Les éléments internes au niveau des signaux ne sont jamais renvoyés, quelle que soit l'offre : les noms des signaux individuels, leurs poids, les seuils derrière un verdict, les valeurs brutes des signaux et la décomposition des scores restent de notre côté. Un score que l'on peut rétro-analyser jusqu'à ses entrées cesse d'être utile comme défense.
Tout échec utilise une seule et même enveloppe :
{ "error": { "code": "unauthorized", "message": "missing Authorization: Bearer <secret key>", "requestId": "8f14e45fceea167a5a36dedd" }}Ce requestId n'est pas l'identifiant de la visite. Deux valeurs différentes
portent le même nom : dans un payload de session, requestId est l'UUID de la visite,
celui-là même que livre le webhook ; dans une enveloppe d'erreur, c'est un identifiant
de trace de 24 caractères, créé pour chaque appel HTTP. Cet identifiant de trace
revient aussi dans l'en-tête X-Request-Id de chaque réponse, réussie ou non.
Joignez-le lorsque vous contactez le support — c'est ainsi que nous retrouvons votre
appel exact.
| HTTP | code | Signification |
|---|---|---|
| 400 | invalid_request | Un paramètre est absent ou mal formé |
| 401 | unauthorized | La clé est absente, invalide, révoquée ou expirée |
| 402 | upgrade_required | Votre offre n'inclut pas l'accès API |
| 404 | not_found | Rien ne correspond dans la fenêtre de rétention |
| 405 | method_not_allowed | La route existe, mais pas pour cette méthode |
| 429 | rate_limited | Requêtes par seconde, ou quota quotidien, dépassés |
| 500 | internal | Quelque chose a échoué de notre côté |
| 503 | unavailable | Un magasin de données est temporairement injoignable |
Les contrôles s'exécutent dans un ordre fixe — la clé, puis l'offre, puis les limites — de sorte qu'une requête avec une mauvaise clé signale toujours la clé en premier, jamais un problème de quota.
Deux cas de 401 se lisent différemment à dessein : missing Authorization: Bearer <secret key>
signifie que l'en-tête n'est jamais arrivé, tandis que invalid or revoked API key
signifie qu'il est arrivé et n'a pas correspondu. 402 porte
Data API requires the Pro plan or higher.
Chaque réponse authentifiée porte votre situation du moment :
| En-tête | Signification |
|---|---|
X-RateLimit-Limit | Votre quota quotidien |
X-RateLimit-Remaining | Appels restants aujourd'hui |
X-RateLimit-Reset | Heure Unix de la réinitialisation — minuit UTC |
Retry-After | Secondes à attendre, envoyé uniquement avec 429 |
| Offre | Requêtes par seconde | Requêtes par jour | Profondeur d'historique |
|---|---|---|---|
| Free | Pas d'accès API | — | 7 jours |
| Pro | 10 | 10 000 | 30 jours |
| Business | 50 | 100 000 | 90 jours |
| Enterprise | 200 | Non plafonné | 365 jours |