Aller au contenu

Référence

API pour les intégrateurs

L'API publique de Tiqo est celle qu'utilise le plugin GLPI. Elle est documentée pour écrire votre propre client : un autre outil de tickets, un script, un connecteur.

Vue d'ensemble

JSON sur HTTPS, encodé en UTF-8. Les erreurs des points d'accès documentés ci-dessous suivent la même enveloppe JSON ; un chemin inexistant ou une méthode non prise en charge répond 404 ou 405 sans cette enveloppe. URL de base :

URL de base
https://tiqo.fr/api/v1

Les identifiants (clé API et secret HMAC) sont créés dans l'application Tiqo et affichés une seule fois. Les exemples ci-dessous les lisent dans les variables TIQO_API_KEY et TIQO_HMAC_SECRET : ne les écrivez jamais en clair dans un script partagé.

Authentification

Authentification
En-têteValeurQuand
AuthorizationBearer tiqo_live_…toujours (sauf /health)
X-Tiqo-Timestampheure Unix en secondes, à ±300 s de l’horloge du serveurtoujours (sauf /health)
X-Tiqo-SignatureHMAC-SHA256 en hexadécimal (64 caractères, préfixe sha256= accepté)toujours (sauf /health)
Idempotency-Keychaîne unique par requête, par exemple un UUID ; seuls les 200 premiers caractères sont pris en compte (au-delà, la clé est tronquée)obligatoire sur POST /analyze ; facultative sur les autres POST (toujours couverte par la signature, vide si absente). Utilisez une nouvelle clé à chaque tentative. Sur /analyze, une clé dont l’analyse est terminée ou en cours est refusée (409) tant qu’elle est conservée (7 jours au plus), et une clé dont l’analyse a échoué est libérée aussitôt. Sur /feedback, /kb/sync, /solutions et /tickets/events, la clé est consommée dès réception d’une requête authentifiée et valide, même si celle-ci échoue ensuite, puis refusée (409) pendant 24 heures.
Content-Typeapplication/jsonrequêtes POST
X-Tiqo-Instancenom de l'instance (120 caractères), affiché dans le tableau de bordfacultatif
X-Tiqo-Plugin-Versionversion de votre client (40 caractères)facultatif
X-Tiqo-Glpi-Versionversion de l'outil de tickets (40 caractères)facultatif
En-tête
Authorization
Valeur
Bearer tiqo_live_…
Quand
toujours (sauf /health)
En-tête
X-Tiqo-Timestamp
Valeur
heure Unix en secondes, à ±300 s de l’horloge du serveur
Quand
toujours (sauf /health)
En-tête
X-Tiqo-Signature
Valeur
HMAC-SHA256 en hexadécimal (64 caractères, préfixe sha256= accepté)
Quand
toujours (sauf /health)
En-tête
Idempotency-Key
Valeur
chaîne unique par requête, par exemple un UUID ; seuls les 200 premiers caractères sont pris en compte (au-delà, la clé est tronquée)
Quand
obligatoire sur POST /analyze ; facultative sur les autres POST (toujours couverte par la signature, vide si absente). Utilisez une nouvelle clé à chaque tentative. Sur /analyze, une clé dont l’analyse est terminée ou en cours est refusée (409) tant qu’elle est conservée (7 jours au plus), et une clé dont l’analyse a échoué est libérée aussitôt. Sur /feedback, /kb/sync, /solutions et /tickets/events, la clé est consommée dès réception d’une requête authentifiée et valide, même si celle-ci échoue ensuite, puis refusée (409) pendant 24 heures.
En-tête
Content-Type
Valeur
application/json
Quand
requêtes POST
En-tête
X-Tiqo-Instance
Valeur
nom de l'instance (120 caractères), affiché dans le tableau de bord
Quand
facultatif
En-tête
X-Tiqo-Plugin-Version
Valeur
version de votre client (40 caractères)
Quand
facultatif
En-tête
X-Tiqo-Glpi-Version
Valeur
version de l'outil de tickets (40 caractères)
Quand
facultatif

Signature HMAC

La signature est le HMAC-SHA256, en hexadécimal minuscule, calculé avec le secret HMAC de l'espace sur les octets suivants :

Entrée
<X-Tiqo-Timestamp>.<Idempotency-Key>.<raw body>
  • le corps est signé exactement tel qu'envoyé : signez la chaîne JSON que vous transmettez, sans la re-sérialiser ;
  • pour un GET, la clé d'idempotence et le corps sont vides : l'entrée est <timestamp>.. ;
  • l'horodatage doit être à moins de 300 secondes de l'horloge du serveur ; la signature couvre la clé d'idempotence, donc une requête capturée ne peut pas être rejouée avec une autre clé, et une clé déjà reçue est refusée (409). Sur /analyze seulement, une clé dont l'analyse a échoué est libérée pour permettre une nouvelle tentative ; sur les autres POST, la clé reste consommée même quand la requête échoue : chaque nouvelle tentative utilise une nouvelle clé.

Vecteur de référence

Vecteur de référence
ChampValeur
secrettest-secret
X-Tiqo-Timestamp1793385000
Idempotency-Keyevt-1
corps{"a":1}
entrée signée1793385000.evt-1.{"a":1}
X-Tiqo-Signature7a8306e38b71005597b83dbc4c7e1facccd25781bd92a60874d1f72b40126be4
entrée signée (GET)1793385000..
X-Tiqo-Signature (GET)7d8f6ad13048c38fe1c610352696777d8218234b5a39ea3c4d1c474ab42e354b
Champ
secret
Valeur
test-secret
Champ
X-Tiqo-Timestamp
Valeur
1793385000
Champ
Idempotency-Key
Valeur
evt-1
Champ
corps
Valeur
{"a":1}
Champ
entrée signée
Valeur
1793385000.evt-1.{"a":1}
Champ
X-Tiqo-Signature
Valeur
7a8306e38b71005597b83dbc4c7e1facccd25781bd92a60874d1f72b40126be4
Champ
entrée signée (GET)
Valeur
1793385000..
Champ
X-Tiqo-Signature (GET)
Valeur
7d8f6ad13048c38fe1c610352696777d8218234b5a39ea3c4d1c474ab42e354b
openssl
printf '%s' '1793385000.evt-1.{"a":1}' | openssl dgst -sha256 -hmac 'test-secret'
# 7a8306e38b71005597b83dbc4c7e1facccd25781bd92a60874d1f72b40126be4

Exemples

bash · POST /analyze
API_URL="https://tiqo.fr/api/v1"
BODY='{"glpi_ticket_id":1042,"title":"Impossible de me connecter au Wi-Fi","categories":[]}'
TIMESTAMP=$(date +%s)
IDEMPOTENCY_KEY=$(uuidgen | tr 'A-Z' 'a-z')
SIGNATURE=$(printf '%s.%s.%s' "$TIMESTAMP" "$IDEMPOTENCY_KEY" "$BODY" \
  | openssl dgst -sha256 -hmac "$TIQO_HMAC_SECRET" -hex | sed 's/^.*= //')

curl -sS -X POST "$API_URL/analyze" \
  -H "Authorization: Bearer $TIQO_API_KEY" \
  -H "X-Tiqo-Timestamp: $TIMESTAMP" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "X-Tiqo-Signature: $SIGNATURE" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY"
Node.js 20+
import { createHmac, randomUUID } from 'node:crypto'

const API_URL = 'https://tiqo.fr/api/v1'
const body = JSON.stringify({ glpi_ticket_id: 1042, title: 'Impossible de me connecter au Wi-Fi', categories: [] })
const timestamp = String(Math.floor(Date.now() / 1000))
const idempotencyKey = randomUUID()
const signature = createHmac('sha256', process.env.TIQO_HMAC_SECRET)
  .update(`${timestamp}.${idempotencyKey}.${body}`)
  .digest('hex')

const response = await fetch(`${API_URL}/analyze`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TIQO_API_KEY}`,
    'X-Tiqo-Timestamp': timestamp,
    'Idempotency-Key': idempotencyKey,
    'X-Tiqo-Signature': signature,
    'Content-Type': 'application/json',
  },
  body, // exactly the bytes that were signed
})
PHP 8
$body = json_encode(
    ['glpi_ticket_id' => 1042, 'title' => 'Impossible de me connecter au Wi-Fi', 'categories' => []],
    JSON_UNESCAPED_UNICODE
);
$timestamp      = (string) time();
$idempotencyKey = bin2hex(random_bytes(16));
$signature      = hash_hmac('sha256', $timestamp . '.' . $idempotencyKey . '.' . $body, getenv('TIQO_HMAC_SECRET'));
// Send $body unchanged with Authorization, X-Tiqo-Timestamp, Idempotency-Key and X-Tiqo-Signature.
bash · GET /usage
TIMESTAMP=$(date +%s)
SIGNATURE=$(printf '%s..' "$TIMESTAMP" | openssl dgst -sha256 -hmac "$TIQO_HMAC_SECRET" -hex | sed 's/^.*= //')

curl -sS "https://tiqo.fr/api/v1/usage" \
  -H "Authorization: Bearer $TIQO_API_KEY" \
  -H "X-Tiqo-Timestamp: $TIMESTAMP" \
  -H "X-Tiqo-Signature: $SIGNATURE"

POST /analyze

Analyse un ticket et renvoie résumé, type, catégorie et urgence (ces trois derniers avec un niveau de confiance entre 0 et 1), brouillon de réponse, mots-clés, doublons et articles de la base de connaissances utilisés. Les catégories ne sont choisies que dans categories ; en cas de doute, category.id vaut null avec une confiance de 0. L'urgence va de 1 à 5.

POST /analyze
ChampTypeRequisDescription
glpi_ticket_identier de 1 à 2 147 483 647ouiIdentifiant du ticket dans votre outil.
glpi_entity_identier ≥ 0nonEntité du ticket (0 par défaut). Les articles de la base de connaissances et les solutions proposés sont limités à cette entité.
titletexte, 2 000 max.nonTitre, pseudonymisé de préférence.
contenttexte, 200 000 max.nonDescription, tronquée à 6 000 caractères pour le modèle. Lue comme du HTML (balises retirées, entités décodées) sauf avec content_format = text.
content_formathtml ou textnonEncodage de title et content : html par défaut (clients anciens). Avec text, le texte est pris tel quel : les espaces et sauts de ligne sont normalisés, rien n'est retiré, si bien qu'un texte entre chevrons (<nom-du-serveur>) est conservé. Le plugin GLPI envoie du texte.
requester_typetexte, 50 max.nonType de demandeur, par exemple user.
glpi_created_atdatenonDate de création du ticket, ISO 8601 ou AAAA-MM-JJ HH:MM:SS (UTC). Une valeur invalide est ignorée. Un ticket n'est proposé comme doublon que s'il a été créé ces 7 derniers jours ; sans cette date, la période court à partir de sa première analyse par Tiqo.
currentobjetnonValeurs actuelles : type (1 = incident, 2 = demande, ou incident / request), category_id, urgency (1 à 5) et status (open, solved ou closed). Avec status, Tiqo enregistre le statut du ticket en même temps que l'analyse : un ticket solved ou closed n'est pas proposé comme doublon, et un ticket open qui était résolu ou clos le redevient. Le plugin GLPI l'envoie toujours.
plugin_optionsobjetnon{ kb_sync, send_solutions } (booléens) : options de votre client. Avec kb_sync à false, l'analyse ne s'appuie pas sur la base de connaissances ; avec send_solutions à false, elle n'utilise pas les solutions mémorisées. Absent, les deux valent true (clients anciens). Ces options s'ajoutent à l'offre et aux réglages de l'espace, qui restent nécessaires. Le plugin GLPI y reprend ses options « Synchroniser la base de connaissances » et « Envoyer les solutions à la clôture ».
categoriestableau, 2 000 max.non[{ id, name, completename }] : les seules catégories que Tiqo peut proposer. Seules les 400 premières, dans l'ordre envoyé, sont proposées au modèle (moins si leurs libellés dépassent environ 40 000 caractères au total) ; les suivantes sont ignorées. Liste vide = aucune catégorie suggérée.
languagefr ou ennonLangue de la réponse quand celle du ticket ne peut pas être détectée.
Champ
glpi_ticket_id
Type
entier de 1 à 2 147 483 647
Requis
oui
Description
Identifiant du ticket dans votre outil.
Champ
glpi_entity_id
Type
entier ≥ 0
Requis
non
Description
Entité du ticket (0 par défaut). Les articles de la base de connaissances et les solutions proposés sont limités à cette entité.
Champ
title
Type
texte, 2 000 max.
Requis
non
Description
Titre, pseudonymisé de préférence.
Champ
content
Type
texte, 200 000 max.
Requis
non
Description
Description, tronquée à 6 000 caractères pour le modèle. Lue comme du HTML (balises retirées, entités décodées) sauf avec content_format = text.
Champ
content_format
Type
html ou text
Requis
non
Description
Encodage de title et content : html par défaut (clients anciens). Avec text, le texte est pris tel quel : les espaces et sauts de ligne sont normalisés, rien n'est retiré, si bien qu'un texte entre chevrons (<nom-du-serveur>) est conservé. Le plugin GLPI envoie du texte.
Champ
requester_type
Type
texte, 50 max.
Requis
non
Description
Type de demandeur, par exemple user.
Champ
glpi_created_at
Type
date
Requis
non
Description
Date de création du ticket, ISO 8601 ou AAAA-MM-JJ HH:MM:SS (UTC). Une valeur invalide est ignorée. Un ticket n'est proposé comme doublon que s'il a été créé ces 7 derniers jours ; sans cette date, la période court à partir de sa première analyse par Tiqo.
Champ
current
Type
objet
Requis
non
Description
Valeurs actuelles : type (1 = incident, 2 = demande, ou incident / request), category_id, urgency (1 à 5) et status (open, solved ou closed). Avec status, Tiqo enregistre le statut du ticket en même temps que l'analyse : un ticket solved ou closed n'est pas proposé comme doublon, et un ticket open qui était résolu ou clos le redevient. Le plugin GLPI l'envoie toujours.
Champ
plugin_options
Type
objet
Requis
non
Description
{ kb_sync, send_solutions } (booléens) : options de votre client. Avec kb_sync à false, l'analyse ne s'appuie pas sur la base de connaissances ; avec send_solutions à false, elle n'utilise pas les solutions mémorisées. Absent, les deux valent true (clients anciens). Ces options s'ajoutent à l'offre et aux réglages de l'espace, qui restent nécessaires. Le plugin GLPI y reprend ses options « Synchroniser la base de connaissances » et « Envoyer les solutions à la clôture ».
Champ
categories
Type
tableau, 2 000 max.
Requis
non
Description
[{ id, name, completename }] : les seules catégories que Tiqo peut proposer. Seules les 400 premières, dans l'ordre envoyé, sont proposées au modèle (moins si leurs libellés dépassent environ 40 000 caractères au total) ; les suivantes sont ignorées. Liste vide = aucune catégorie suggérée.
Champ
language
Type
fr ou en
Requis
non
Description
Langue de la réponse quand celle du ticket ne peut pas être détectée.
Requête · JSON
{
  "glpi_ticket_id": 1042,
  "glpi_entity_id": 0,
  "title": "Impossible de me connecter au Wi-Fi depuis ce matin",
  "content": "Depuis ce matin, mon portable (DELL-7420, 192.168.10.57) ne se connecte plus au Wi-Fi CORP-STAFF. Joignable au <phone_1> ou sur <email_1>.",
  "content_format": "text",
  "requester_type": "user",
  "glpi_created_at": "2026-10-30 08:41:12",
  "current": { "type": 1, "category_id": null, "urgency": 3, "status": "open" },
  "plugin_options": { "kb_sync": true, "send_solutions": true },
  "categories": [
    { "id": 1, "name": "Réseau", "completename": "Réseau" },
    { "id": 2, "name": "Wi-Fi", "completename": "Réseau > Wi-Fi" },
    { "id": 4, "name": "Poste de travail", "completename": "Poste de travail" }
  ],
  "language": "fr"
}
Réponse 200 · JSON
{
  "analysis_id": "6f1c2a9e-8f0b-4c55-9d1e-2b7c3a4d5e6f",
  "summary": "Le portable DELL-7420 du demandeur ne se connecte plus au Wi-Fi CORP-STAFF depuis ce matin, le mot de passe est refusé. Les collègues du même étage ne sont pas touchés.",
  "type": { "value": "incident", "confidence": 0.93 },
  "category": { "id": 2, "name": "Wi-Fi", "confidence": 0.88 },
  "urgency": { "value": 3, "confidence": 0.71 },
  "draft_reply": "Bonjour,\n\nMerci pour votre message. Pouvez-vous nous indiquer ...",
  "tags": ["wifi", "authentification", "portable"],
  "duplicates": [
    { "glpi_ticket_id": 1039, "score": 0.91, "title": "Plus de Wi-Fi CORP-STAFF, écrire à [adresse e-mail]" }
  ],
  "similar_solved": [
    { "glpi_ticket_id": 987, "excerpt": "Certificat RADIUS expiré sur le poste : réinstallé depuis le portail, puis oubli et reconnexion au réseau CORP-STAFF.", "score": 0.892 }
  ],
  "knowledge": [
    { "glpi_kb_id": 12, "title": "Wi-Fi CORP-STAFF : renouveler le certificat du poste", "score": 0.884, "audience": "internal" },
    { "glpi_kb_id": 5, "title": "Se connecter au Wi-Fi CORP-STAFF", "score": 0.871, "audience": "faq" }
  ],
  "usage": { "tickets_this_month": 42, "tickets_total": 42, "limit": 100, "remaining": 58 },
  "model": "mistral-medium-latest",
  "latency_ms": 1840
}

Valeurs données à titre d'exemple. usage.limit est la limite de l'offre (essai : tickets au total, Starter : tickets par mois, Pro : null) ; usage.remaining est calculé sur le bon compteur.

Avec l'offre Pro et pendant l'essai, et sinon ces tableaux sont vides :

  • duplicates liste au plus 5 tickets de l'espace déjà analysés et créés dans GLPI ces 7 derniers jours (à défaut de glpi_created_at, analysés pour la première fois dans cette période) dont la similarité atteint 0,91, hors tickets résolus, clos ou supprimés, d'après current.status ou POST /tickets/events. Une nouvelle analyse ne prolonge pas cette période. Leur titre arrive avec des mentions neutres à la place des données personnelles ([adresse e-mail]…). Ils ne sont pas filtrés par entité : comme le plugin, vérifiez que l'utilisateur a le droit de voir un ticket avant de l'afficher ;
  • similar_solved liste au plus 3 solutions mémorisées proches, de la même entité que le ticket (glpi_entity_id), avec un extrait de 300 caractères au plus, quand la mémoire des tickets résolus est activée dans l'espace et que plugin_options.send_solutions ne vaut pas false ;
  • le résumé et le brouillon s'appuient sur au plus 5 passages de FAQ publiques (is_faq), les plus proches parmi les articles visibles dans l'entité du ticket, et le brouillon cite l'article utilisé, quand la synchronisation est activée dans l'espace et que plugin_options.kb_sync ne vaut pas false. Seules les FAQ sont fournies au modèle : un article interne n'est jamais envoyé au modèle pour rédiger les suggestions, il est seulement retrouvé par similarité (son texte est indexé à la synchronisation) et signalé aux techniciens dans knowledge. Le contexte retrouvé est transmis au modèle comme une simple donnée de référence, jamais comme une instruction ;
  • knowledge liste les articles retrouvés, FAQ et articles internes, du plus proche au moins proche : un seul élément par article (au plus 5), avec son numéro GLPI glpi_kb_id, son titre title tel qu'il a été synchronisé (mentions neutres à la place des données personnelles), le score de son passage le plus proche (au moins 0,86, arrondi à 3 décimales) et audience : faq (FAQ publique, dont les passages les plus proches peuvent être fournis au modèle) ou internal (article réservé aux techniciens, jamais fourni au modèle). Cette liste ne dépend pas du modèle : elle comprend chaque FAQ dont un passage lui a été fourni, et le résumé ne parle de la base de connaissances que s'il s'appuie sur un article. Elle est vide quand la base de connaissances n'a pas servi. Comme le plugin, vérifiez que l'utilisateur a le droit de voir un article avant de l'afficher.

POST /feedback

Ce que le technicien a fait d'une analyse, pour le taux d'acceptation. Ne demande pas l'offre Pro. Les champs sont fusionnés dans une seule ligne par analyse : un champ absent ou null garde la valeur enregistrée, et au moins un des trois champs doit être renseigné (sinon 400 invalid_request). La réponse renvoie l'état fusionné. Le plugin GLPI met ces retours en file d'attente et les envoie avec son action automatique tiqoAnalyze : le tableau de bord peut les afficher avec quelques minutes de décalage.

POST /feedback
ChampTypeRequisDescription
analysis_idUUIDouianalysis_id renvoyé par POST /analyze, dans le même espace (sinon 404 not_found).
category_acceptedbooléen ou nullnontrue : catégorie suggérée appliquée (à la main ou automatiquement) ; false : un technicien l’a ensuite remplacée par une autre, même après une application automatique. Les changements faits sans session interactive (action automatique, collecteur de courriels) ne comptent pas.
urgency_acceptedbooléen ou nullnonMême chose pour l’urgence suggérée.
reply_usedused, edited, ignored ou nullnonSort du brouillon de réponse : used inséré (signalé à l'insertion), edited inséré puis modifié (signalé quand le texte du suivi change), ignored jamais inséré (signalé à la résolution ou à la clôture du ticket). Ne fait qu'avancer : ignored < used < edited.
Champ
analysis_id
Type
UUID
Requis
oui
Description
analysis_id renvoyé par POST /analyze, dans le même espace (sinon 404 not_found).
Champ
category_accepted
Type
booléen ou null
Requis
non
Description
true : catégorie suggérée appliquée (à la main ou automatiquement) ; false : un technicien l’a ensuite remplacée par une autre, même après une application automatique. Les changements faits sans session interactive (action automatique, collecteur de courriels) ne comptent pas.
Champ
urgency_accepted
Type
booléen ou null
Requis
non
Description
Même chose pour l’urgence suggérée.
Champ
reply_used
Type
used, edited, ignored ou null
Requis
non
Description
Sort du brouillon de réponse : used inséré (signalé à l'insertion), edited inséré puis modifié (signalé quand le texte du suivi change), ignored jamais inséré (signalé à la résolution ou à la clôture du ticket). Ne fait qu'avancer : ignored < used < edited.
Requête · JSON
{
  "analysis_id": "6f1c2a9e-8f0b-4c55-9d1e-2b7c3a4d5e6f",
  "category_accepted": true
}
Réponse 200 · JSON
{
  "analysis_id": "6f1c2a9e-8f0b-4c55-9d1e-2b7c3a4d5e6f",
  "category_accepted": true,
  "urgency_accepted": null,
  "reply_used": "edited"
}

POST /kb/sync

Synchronise les articles de la base de connaissances (offre Pro et essai, option « Synchronisation de la base de connaissances » activée dans l'espace ; sinon 402 plan_feature_unavailable ou 403 feature_disabled). Une requête qui ne fait que supprimer (items vide) est toujours acceptée, quels que soient l'offre et les réglages. Envoyez du texte brut et les données personnelles déjà remplacées par des mentions neutres ([adresse e-mail], [numéro de téléphone], [IBAN]), jamais par des jetons <email_1> ; l'API applique de nouveau ce remplacement à la réception. Chaque article est découpé en passages d'environ 1 200 caractères qui se recouvrent, 100 au plus par article, et chaque passage est conservé avec son embedding.

  • Une seule synchronisation à la fois par espace : un second appel simultané reçoit 429 rate_limited (details.reason = kb_sync_in_progress, Retry-After : 30 s).
  • Par requête : 50 articles, 600 passages (sinon 400 invalid_request, too_many_chunks) et 1 Mio au plus (sinon 413 payload_too_large).
  • Par espace, vérifié avant tout calcul (sinon 402 retrieval_limit_reached) : pendant l'essai 300 articles, 3 000 passages et 3 000 000 jetons d'embedding par mois ; avec l'offre Pro 5 000 articles, 50 000 passages et 30 000 000 jetons par mois. Le volume mensuel est partagé avec /solutions.
  • Ordre de traitement : les suppressions (deleted_ids, articles sans texte ou sans entité) sont appliquées avant toute vérification, même quand le reste de la requête est refusé ; les entités et le statut FAQ des articles déjà indexés sont mis à jour avant les limites, si bien qu'un changement de visibilité n'est jamais refusé. Quand seule la limite d'articles est atteinte, les articles déjà indexés de la requête sont recalculés, les nouveaux sont acceptés dans l'ordre de la requête tant qu'ils tiennent dans la limite, et seuls ceux qui la dépassent sont refusés (402 retrieval_limit_reached, details.refused_ids) : comme le plugin GLPI, envoyez les articles déjà synchronisés dans des lots séparés des nouveaux. La limite de passages et le volume mensuel refusent en revanche toute la requête, y compris les modifications d'articles déjà indexés, jusqu'à la suppression d'articles, le mois suivant ou un changement d'offre.
  • Chaque requête dispose de 50 s côté serveur : au-delà, aucun nouveau groupe d'articles n'est commencé, les articles déjà traités restent enregistrés et la réponse est 503 ai_unavailable (details.reason = deadline). Renvoyez le même lot avec une nouvelle clé : les articles terminés reviennent dans unchanged.
  • Désactiver l'option dans l'application, ou passer à l'offre Starter, supprime la base synchronisée.
POST /kb/sync
ChampTypeRequisDescription
itemstableau, 50 max.nonArticles nouveaux ou modifiés : glpi_kb_id (entier de 1 à 2 147 483 647), title (500 caractères au plus), content (texte brut, 60 000 caractères pris en compte au plus : rien n'est retiré, les espaces sont normalisés), updated_at (ISO 8601 ou AAAA-MM-JJ HH:MM:SS, facultatif), entity_ids (identifiants des entités GLPI où l'article est visible, sous-entités comprises, 2 000 au plus : l'article ne sert qu'aux tickets de ces entités, et un article sans entité n'est pas indexé) et is_faq (true pour une FAQ publique : seules les FAQ sont fournies au modèle d'IA, qui peut s'en servir dans le résumé et le brouillon de réponse ; un article interne n'est jamais envoyé au modèle pour rédiger les suggestions : il est seulement retrouvé par similarité, son texte étant indexé (embedding) à la synchronisation, et signalé aux techniciens dans knowledge). Un article dont le titre et le texte n'ont pas changé n'est pas recalculé : un changement de ses seules entités ou de son statut FAQ est enregistré sans nouvel embedding.
deleted_idstableau, 500 max.nonIdentifiants des articles supprimés, dépubliés ou sortis du périmètre : leurs passages et embeddings sont supprimés. Une requête qui ne fait que supprimer (items vide) est toujours acceptée, quels que soient l’offre et les réglages.
Champ
items
Type
tableau, 50 max.
Requis
non
Description
Articles nouveaux ou modifiés : glpi_kb_id (entier de 1 à 2 147 483 647), title (500 caractères au plus), content (texte brut, 60 000 caractères pris en compte au plus : rien n'est retiré, les espaces sont normalisés), updated_at (ISO 8601 ou AAAA-MM-JJ HH:MM:SS, facultatif), entity_ids (identifiants des entités GLPI où l'article est visible, sous-entités comprises, 2 000 au plus : l'article ne sert qu'aux tickets de ces entités, et un article sans entité n'est pas indexé) et is_faq (true pour une FAQ publique : seules les FAQ sont fournies au modèle d'IA, qui peut s'en servir dans le résumé et le brouillon de réponse ; un article interne n'est jamais envoyé au modèle pour rédiger les suggestions : il est seulement retrouvé par similarité, son texte étant indexé (embedding) à la synchronisation, et signalé aux techniciens dans knowledge). Un article dont le titre et le texte n'ont pas changé n'est pas recalculé : un changement de ses seules entités ou de son statut FAQ est enregistré sans nouvel embedding.
Champ
deleted_ids
Type
tableau, 500 max.
Requis
non
Description
Identifiants des articles supprimés, dépubliés ou sortis du périmètre : leurs passages et embeddings sont supprimés. Une requête qui ne fait que supprimer (items vide) est toujours acceptée, quels que soient l’offre et les réglages.
Requête · JSON
{
  "items": [
    {
      "glpi_kb_id": 12,
      "title": "Se connecter au Wi-Fi CORP-STAFF",
      "content": "Le réseau CORP-STAFF demande le certificat de l'organisation. En cas d'échec, écrire à [adresse e-mail] ...",
      "updated_at": "2026-10-28 16:05:00",
      "entity_ids": [0, 3, 4],
      "is_faq": true
    }
  ],
  "deleted_ids": [7]
}
Réponse 200 · JSON
{ "upserted": 1, "unchanged": 0, "deleted": 1, "chunks": 2, "kb_articles": 37, "truncated": 0, "truncated_ids": [], "kb_index_version": "3:mistral-embed" }

upserted : articles recalculés ; unchanged : titre et texte identiques, rien n'est recalculé (entités et statut FAQ mis à jour le cas échéant) ; deleted : articles supprimés ; chunks : passages écrits par cet appel ; kb_articles : articles synchronisés dans l'espace après l'appel ; truncated : nombre d'articles de la requête indexés en partie seulement (au-delà de 60 000 caractères ou de 100 passages) ; truncated_ids : leurs identifiants, pour signaler chacun de ces articles ; kb_index_version : version de l'indexation côté serveur, découpage et modèle d'embeddings (par exemple 3:mistral-embed ; quand elle change, renvoyez tous les articles). Chaque appel réussi, même vide, met à jour la « Dernière synchronisation » du tableau de bord.

POST /solutions

Mémorise la solution d'un ticket résolu ou clos (offre Pro et essai, option « Mémoire des tickets résolus » activée, désactivée par défaut ; sinon 402 plan_feature_unavailable ou 403 feature_disabled). Le ticket doit déjà être connu de Tiqo, c'est-à-dire analysé au moins une fois ; sinon 404 not_found (details.resource = ticket), à ne pas réessayer. Texte brut, avec les mêmes mentions neutres que pour la base de connaissances. Les solutions proches d'un nouveau ticket de la même entité reviennent dans similar_solved. Le plugin GLPI envoie ainsi la solution des tickets qu'il a analysés, quelques minutes après la résolution, avec son action automatique ; si l'analyse du ticket est encore en cours de reprise (après une erreur), la solution attend qu'elle aboutisse.

  • Par espace, au plus 1 000 solutions mémorisées pendant l'essai et 50 000 avec l'offre Pro (sinon 402 retrieval_limit_reached, details.reason = solutions) ; remplacer la solution d'un ticket déjà mémorisé ne compte pas. Les embeddings entrent dans le volume mensuel partagé avec /kb/sync.
  • Une solution est supprimée quand le ticket est supprimé définitivement (purged sur /tickets/events, qui la retire aussi des solutions similaires enregistrées avec les autres analyses), quand l'option est désactivée dans l'application ou au passage à l'offre Starter ; ces deux derniers cas vident aussi les solutions similaires enregistrées avec les analyses de l'espace.
POST /solutions
ChampTypeRequisDescription
glpi_ticket_identier de 1 à 2 147 483 647ouiTicket résolu ou clos, déjà analysé par Tiqo (sinon 404 not_found, details.resource = ticket).
glpi_entity_identier ≥ 0ouiEntité du ticket : la solution n’est proposée que pour les tickets de la même entité.
titletexte, 2 000 max.nonTitre du ticket.
solution_texttexte, 1 à 100 000ouiTexte brut de la solution, données personnelles remplacées par des mentions neutres.
Champ
glpi_ticket_id
Type
entier de 1 à 2 147 483 647
Requis
oui
Description
Ticket résolu ou clos, déjà analysé par Tiqo (sinon 404 not_found, details.resource = ticket).
Champ
glpi_entity_id
Type
entier ≥ 0
Requis
oui
Description
Entité du ticket : la solution n’est proposée que pour les tickets de la même entité.
Champ
title
Type
texte, 2 000 max.
Requis
non
Description
Titre du ticket.
Champ
solution_text
Type
texte, 1 à 100 000
Requis
oui
Description
Texte brut de la solution, données personnelles remplacées par des mentions neutres.
Requête · JSON
{
  "glpi_ticket_id": 987,
  "glpi_entity_id": 3,
  "title": "Wi-Fi CORP-STAFF refusé sur un portable",
  "solution_text": "Certificat RADIUS expiré sur le poste : réinstallé depuis le portail, puis oubli et reconnexion au réseau CORP-STAFF."
}
Réponse 200 · JSON
{ "glpi_ticket_id": 987, "stored": true }

POST /tickets/events

Signale le cycle de vie des tickets analysés, sans aucun texte : un ticket résolu ou clos n'est plus proposé comme doublon, un ticket rouvert le redevient, et un ticket supprimé définitivement est effacé de Tiqo avec ses analyses, ses retours et sa solution ; son numéro, son titre et l'extrait de sa solution sont aussi retirés des doublons et des solutions similaires enregistrés avec les autres analyses de l'espace. Accepté pour toutes les offres et quels que soient les réglages (une suppression doit toujours aboutir). Clé d'idempotence facultative ; une clé déjà reçue dans les 24 dernières heures répond 409 duplicate_idempotency_key, même si la première requête a échoué : utilisez une nouvelle clé à chaque tentative.

Le plugin GLPI met ces événements en file d'attente et les envoie par lots de 100 au plus avec son action automatique tiqoAnalyze. Une erreur de configuration ou d'authentification (401, 402, 403, 404, horloge décalée, plugin non configuré, serveur injoignable) met la file en pause, sans compter de tentative, jusqu'à ce qu'elle soit corrigée. Un événement purged n'est jamais abandonné sur une erreur passagère : il reste en file jusqu'à 30 jours après la suppression du ticket, durée au-delà de laquelle tout événement expire. Les autres événements sont abandonnés après 10 tentatives (délai dépassé, 429, 5xx). Seule une erreur 400 ou 413 fait abandonner un lot, après un dernier envoi de ses événements purged seuls.

POST /tickets/events
ChampTypeRequisDescription
eventstableau, 1 à 100oui[{ glpi_ticket_id, event }], traités dans l'ordre. event : solved ou closed (le ticket n'est plus proposé comme doublon), reopened (il le redevient), purged (Tiqo supprime le ticket, ses analyses, ses retours et sa solution mémorisée, et le retire des doublons et des solutions similaires enregistrés avec les autres analyses). Un ticket inconnu de Tiqo est ignoré.
Champ
events
Type
tableau, 1 à 100
Requis
oui
Description
[{ glpi_ticket_id, event }], traités dans l'ordre. event : solved ou closed (le ticket n'est plus proposé comme doublon), reopened (il le redevient), purged (Tiqo supprime le ticket, ses analyses, ses retours et sa solution mémorisée, et le retire des doublons et des solutions similaires enregistrés avec les autres analyses). Un ticket inconnu de Tiqo est ignoré.
Requête · JSON
{
  "events": [
    { "glpi_ticket_id": 1039, "event": "solved" },
    { "glpi_ticket_id": 1020, "event": "reopened" },
    { "glpi_ticket_id": 987, "event": "purged" }
  ]
}
Réponse 200 · JSON
{ "updated": 2, "deleted": 1 }

updated : tickets dont le statut a changé ; deleted : tickets supprimés.

GET /usage

Renvoie l'espace, l'offre, le quota, les réglages utiles au client (dont solved_memory et kb_sync, choisis dans l'application) et les fonctions de l'offre (features). C'est l'appel du bouton « Tester la connexion » du plugin. Signé comme un GET, sans clé d'idempotence.

Réponse 200 · JSON
{
  "workspace": { "id": "0b6c1d2e-3f40-4a5b-8c6d-7e8f9a0b1c2d", "name": "Mairie de Villeneuve — DSI", "slug": "mairie-de-villeneuve-dsi" },
  "plan": "trial",
  "trial_ends_at": "2026-11-14T09:00:00.000Z",
  "usage": { "tickets_this_month": 42, "tickets_total": 42, "limit": 100, "remaining": 58 },
  "settings": {
    "language": "fr",
    "auto_apply_category": false,
    "auto_apply_urgency": false,
    "confidence_threshold": 0.8,
    "pseudonymize": true,
    "solved_memory": true,
    "kb_sync": true
  },
  "features": { "kb_grounding": true, "solved_memory": true, "duplicates": true }
}

GET /health

Sans authentification. Répond 200 quand le service et la base de données fonctionnent, 503 sinon ("status": "degraded"). Utilisable par une sonde de supervision ; voir aussi la page d'état du service.

Réponse 200 · JSON
{ "status": "ok", "version": "0.1.0", "time": "2026-10-31T09:00:00.000Z", "checks": { "database": "ok" } }

Codes d'erreur

Les erreurs de l’API ont toutes la même forme (sauf 404 et 405 sur un chemin ou une méthode inconnus) :

JSON
{
  "error": {
    "code": "invalid_signature",
    "message": "Missing or invalid X-Tiqo-Signature or X-Tiqo-Timestamp header.",
    "details": { "reason": "stale_timestamp" }
  }
}
Codes d'erreur
HTTPCodeSignificationQue faire
400invalid_requestCorps invalide : details.issues, ou details.reason = empty_body, malformed_json, missing_idempotency_key, ou too_many_chunks (/kb/sync : plus de 600 passages dans une même requête, envoyez moins d'articles par lot).Corriger la requête ; ne pas réessayer telle quelle.
401invalid_api_keyClé absente, inconnue ou régénérée depuis.Vérifier la clé.
401invalid_signaturedetails.reason : missing_signature, malformed_signature, missing_timestamp, invalid_timestamp, stale_timestamp, mismatch.Vérifier le secret, les octets signés et l'horloge (NTP).
402plan_limit_reachedQuota de tickets de l’offre atteint, tolérance de 10 % comprise, ou plafond d’analyses de la période atteint.Changer d’offre ou attendre le mois suivant.
402trial_expiredL'essai est terminé.Choisir une offre.
402payment_requiredFacture impayée après le délai de grâce de 7 jours : les analyses sont suspendues jusqu’au paiement.Régler la facture ou mettre à jour le moyen de paiement (page Facturation de l’application).
402plan_canceledL'abonnement est résilié.Reprendre un abonnement.
402plan_feature_unavailableFonction absente de l'offre Starter : POST /kb/sync avec des articles et POST /solutions demandent l'offre Pro ou l'essai (details.feature). Un essai terminé, un abonnement résilié ou impayé répond avec son propre code 402. Les suppressions (/kb/sync sans article) et POST /tickets/events sont toujours acceptées.Passer à l’offre Pro, ou désactiver l’option dans le plugin.
402retrieval_limit_reachedLimite de l'espace atteinte, vérifiée avant tout calcul : details.reason = kb_articles ou kb_chunks (articles ou passages après la requête), embedding_tokens (volume mensuel d'embeddings, partagé par /kb/sync et /solutions, remplacement d'une solution compris) ou solutions (solutions mémorisées ; remplacer la solution d'un ticket déjà mémorisé ne compte pas), avec details.limit et details.plan. Essai : 300 articles, 3 000 passages, 3 000 000 jetons par mois et 1 000 solutions ; Pro : 5 000, 50 000, 30 000 000 et 50 000. Sur /kb/sync, quand seule la limite d'articles est atteinte, les articles déjà indexés de la requête sont tout de même traités, les nouveaux sont acceptés dans l'ordre de la requête tant qu'ils tiennent dans la limite et seuls ceux qui la dépassent sont refusés : leurs identifiants sont dans details.refused_ids, et details.truncated_ids liste les articles traités indexés en partie seulement, comme dans une réponse 200. Les limites de passages et de jetons refusent toute la requête, modifications d'articles déjà indexés comprises ; seuls les changements d'entités et de statut FAQ restent appliqués.Retirer des articles du périmètre, attendre le mois suivant (jetons) ou passer à l’offre Pro.
403feature_disabledOption désactivée dans les paramètres de l'espace : details.setting = kb_sync ou solved_memory. Les suppressions restent acceptées.Activer l’option dans l’application, ou la désactiver dans le plugin.
404not_foundRessource inconnue dans cet espace (details.resource) : analysis sur /feedback, par exemple après une purge des données, ou ticket sur /solutions, pour un ticket que Tiqo n'a jamais analysé.Ne pas réessayer.
409duplicate_idempotency_keyClé d'idempotence déjà reçue : sur /analyze, analyse terminée ou en cours (details.analysis_id) ; sur /feedback, /kb/sync, /solutions et /tickets/events, clé déjà reçue dans les 24 dernières heures, que la première requête ait abouti ou non.Ne jamais réutiliser une clé : un 409 signifie seulement que la clé a déjà servi. Si la première tentative n’a pas reçu de réponse 200, réessayer avec une nouvelle clé.
413payload_too_largeCorps de plus de 4 Mio (1 Mio pour /kb/sync).Réduire le contenu (jamais de pièces jointes).
429rate_limitedPlus de 60 requêtes par minute, plus de 5 analyses du même ticket en 24 h (details.reason = reanalysis_limit), ou une synchronisation de la base de connaissances déjà en cours pour l'espace (details.reason = kb_sync_in_progress, Retry-After : 30 s).Attendre la durée de Retry-After, puis réessayer avec une nouvelle clé d’idempotence.
500internal_errorErreur inattendue du service.Réessayer plus tard.
502ai_invalid_outputRéponse du modèle rejetée par la validation.Réessayer avec une nouvelle clé d’idempotence.
503ai_unavailableFournisseur d'IA indisponible (Retry-After : 30 s). Sur /kb/sync et /solutions, details.reason = embeddings_disabled quand le calcul des embeddings est coupé côté serveur. Sur /kb/sync, details.reason = deadline quand la requête a dépassé 50 s : les articles déjà traités restent enregistrés, et le même lot renvoyé les compte dans unchanged.Réessayer avec une nouvelle clé d’idempotence.
HTTP
400
Code
invalid_request
Signification
Corps invalide : details.issues, ou details.reason = empty_body, malformed_json, missing_idempotency_key, ou too_many_chunks (/kb/sync : plus de 600 passages dans une même requête, envoyez moins d'articles par lot).
Que faire
Corriger la requête ; ne pas réessayer telle quelle.
HTTP
401
Code
invalid_api_key
Signification
Clé absente, inconnue ou régénérée depuis.
Que faire
Vérifier la clé.
HTTP
401
Code
invalid_signature
Signification
details.reason : missing_signature, malformed_signature, missing_timestamp, invalid_timestamp, stale_timestamp, mismatch.
Que faire
Vérifier le secret, les octets signés et l'horloge (NTP).
HTTP
402
Code
plan_limit_reached
Signification
Quota de tickets de l’offre atteint, tolérance de 10 % comprise, ou plafond d’analyses de la période atteint.
Que faire
Changer d’offre ou attendre le mois suivant.
HTTP
402
Code
trial_expired
Signification
L'essai est terminé.
Que faire
Choisir une offre.
HTTP
402
Code
payment_required
Signification
Facture impayée après le délai de grâce de 7 jours : les analyses sont suspendues jusqu’au paiement.
Que faire
Régler la facture ou mettre à jour le moyen de paiement (page Facturation de l’application).
HTTP
402
Code
plan_canceled
Signification
L'abonnement est résilié.
Que faire
Reprendre un abonnement.
HTTP
402
Code
plan_feature_unavailable
Signification
Fonction absente de l'offre Starter : POST /kb/sync avec des articles et POST /solutions demandent l'offre Pro ou l'essai (details.feature). Un essai terminé, un abonnement résilié ou impayé répond avec son propre code 402. Les suppressions (/kb/sync sans article) et POST /tickets/events sont toujours acceptées.
Que faire
Passer à l’offre Pro, ou désactiver l’option dans le plugin.
HTTP
402
Code
retrieval_limit_reached
Signification
Limite de l'espace atteinte, vérifiée avant tout calcul : details.reason = kb_articles ou kb_chunks (articles ou passages après la requête), embedding_tokens (volume mensuel d'embeddings, partagé par /kb/sync et /solutions, remplacement d'une solution compris) ou solutions (solutions mémorisées ; remplacer la solution d'un ticket déjà mémorisé ne compte pas), avec details.limit et details.plan. Essai : 300 articles, 3 000 passages, 3 000 000 jetons par mois et 1 000 solutions ; Pro : 5 000, 50 000, 30 000 000 et 50 000. Sur /kb/sync, quand seule la limite d'articles est atteinte, les articles déjà indexés de la requête sont tout de même traités, les nouveaux sont acceptés dans l'ordre de la requête tant qu'ils tiennent dans la limite et seuls ceux qui la dépassent sont refusés : leurs identifiants sont dans details.refused_ids, et details.truncated_ids liste les articles traités indexés en partie seulement, comme dans une réponse 200. Les limites de passages et de jetons refusent toute la requête, modifications d'articles déjà indexés comprises ; seuls les changements d'entités et de statut FAQ restent appliqués.
Que faire
Retirer des articles du périmètre, attendre le mois suivant (jetons) ou passer à l’offre Pro.
HTTP
403
Code
feature_disabled
Signification
Option désactivée dans les paramètres de l'espace : details.setting = kb_sync ou solved_memory. Les suppressions restent acceptées.
Que faire
Activer l’option dans l’application, ou la désactiver dans le plugin.
HTTP
404
Code
not_found
Signification
Ressource inconnue dans cet espace (details.resource) : analysis sur /feedback, par exemple après une purge des données, ou ticket sur /solutions, pour un ticket que Tiqo n'a jamais analysé.
Que faire
Ne pas réessayer.
HTTP
409
Code
duplicate_idempotency_key
Signification
Clé d'idempotence déjà reçue : sur /analyze, analyse terminée ou en cours (details.analysis_id) ; sur /feedback, /kb/sync, /solutions et /tickets/events, clé déjà reçue dans les 24 dernières heures, que la première requête ait abouti ou non.
Que faire
Ne jamais réutiliser une clé : un 409 signifie seulement que la clé a déjà servi. Si la première tentative n’a pas reçu de réponse 200, réessayer avec une nouvelle clé.
HTTP
413
Code
payload_too_large
Signification
Corps de plus de 4 Mio (1 Mio pour /kb/sync).
Que faire
Réduire le contenu (jamais de pièces jointes).
HTTP
429
Code
rate_limited
Signification
Plus de 60 requêtes par minute, plus de 5 analyses du même ticket en 24 h (details.reason = reanalysis_limit), ou une synchronisation de la base de connaissances déjà en cours pour l'espace (details.reason = kb_sync_in_progress, Retry-After : 30 s).
Que faire
Attendre la durée de Retry-After, puis réessayer avec une nouvelle clé d’idempotence.
HTTP
500
Code
internal_error
Signification
Erreur inattendue du service.
Que faire
Réessayer plus tard.
HTTP
502
Code
ai_invalid_output
Signification
Réponse du modèle rejetée par la validation.
Que faire
Réessayer avec une nouvelle clé d’idempotence.
HTTP
503
Code
ai_unavailable
Signification
Fournisseur d'IA indisponible (Retry-After : 30 s). Sur /kb/sync et /solutions, details.reason = embeddings_disabled quand le calcul des embeddings est coupé côté serveur. Sur /kb/sync, details.reason = deadline quand la requête a dépassé 50 s : les articles déjà traités restent enregistrés, et le même lot renvoyé les compte dans unchanged.
Que faire
Réessayer avec une nouvelle clé d’idempotence.

Limites et nouvelles tentatives

  • 60 requêtes par minute et par espace (429 avec Retry-After).
  • 5 analyses par ticket et par jour ; les ré-analyses ne consomment pas de ticket supplémentaire.
  • Au plus 3 fois la limite de tickets, tolérance de 10 % comprise, en analyses par période, ré-analyses comprises (soit 990 par mois en Starter et 330 sur toute la durée de l'essai, pas de plafond en Pro) : au-delà, 402 plan_limit_reached.
  • Corps de 4 Mio au plus, 1 Mio pour /kb/sync (413).
  • Une synchronisation de la base de connaissances à la fois par espace (429 kb_sync_in_progress, réessayez après Retry-After avec une nouvelle clé d'idempotence).
  • Après une erreur 5xx ou un dépassement de délai, réessayez au plus 3 fois avec un délai croissant et une nouvelle clé d'idempotence : une requête interrompue a pu aboutir côté serveur. Chaque nouvelle tentative, quelle que soit l'erreur, utilise une nouvelle clé : sur /feedback, /kb/sync, /solutions et /tickets/events, un 409 signifie seulement que la clé a déjà été reçue, pas que la requête a abouti.
  • Ne réessayez jamais une erreur 400, 401, 402, 403, 404 ou 409 sans changer la requête ou le réglage concerné, et toujours avec une nouvelle clé d'idempotence.

Ne bloquez jamais votre outil de tickets

Comme le plugin GLPI, votre client doit traiter chaque erreur comme « pas de suggestion pour ce ticket » et laisser l’outil fonctionner normalement.
API pour les intégrateurs · Tiqo