Référence
API pour les intégrateurs
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 :
https://tiqo.fr/api/v1Les 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
| En-tête | Valeur | Quand |
|---|---|---|
Authorization | Bearer tiqo_live_… | toujours (sauf /health) |
X-Tiqo-Timestamp | heure Unix en secondes, à ±300 s de l’horloge du serveur | toujours (sauf /health) |
X-Tiqo-Signature | HMAC-SHA256 en hexadécimal (64 caractères, préfixe sha256= accepté) | toujours (sauf /health) |
Idempotency-Key | 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) | 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-Type | application/json | requêtes POST |
X-Tiqo-Instance | nom de l'instance (120 caractères), affiché dans le tableau de bord | facultatif |
X-Tiqo-Plugin-Version | version de votre client (40 caractères) | facultatif |
X-Tiqo-Glpi-Version | version 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 :
<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
/analyzeseulement, 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
| Champ | Valeur |
|---|---|
| secret | test-secret |
| X-Tiqo-Timestamp | 1793385000 |
| Idempotency-Key | evt-1 |
| corps | {"a":1} |
| entrée signée | 1793385000.evt-1.{"a":1} |
| X-Tiqo-Signature | 7a8306e38b71005597b83dbc4c7e1facccd25781bd92a60874d1f72b40126be4 |
| 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
printf '%s' '1793385000.evt-1.{"a":1}' | openssl dgst -sha256 -hmac 'test-secret'
# 7a8306e38b71005597b83dbc4c7e1facccd25781bd92a60874d1f72b40126be4Exemples
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"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
})$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.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.
| Champ | Type | Requis | Description |
|---|---|---|---|
glpi_ticket_id | entier de 1 à 2 147 483 647 | oui | Identifiant du ticket dans votre outil. |
glpi_entity_id | entier ≥ 0 | non | Entité du ticket (0 par défaut). Les articles de la base de connaissances et les solutions proposés sont limités à cette entité. |
title | texte, 2 000 max. | non | Titre, pseudonymisé de préférence. |
content | texte, 200 000 max. | non | 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. |
content_format | html ou text | non | 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. |
requester_type | texte, 50 max. | non | Type de demandeur, par exemple user. |
glpi_created_at | date | non | 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. |
current | objet | non | 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. |
plugin_options | objet | non | { 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 ». |
categories | tableau, 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. |
language | fr ou en | non | Langue 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
titleetcontent:htmlpar défaut (clients anciens). Avectext, 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, ouincident/request),category_id,urgency(1 à 5) etstatus(open,solvedouclosed). Avecstatus, Tiqo enregistre le statut du ticket en même temps que l'analyse : un ticketsolvedouclosedn'est pas proposé comme doublon, et un ticketopenqui é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. Aveckb_syncàfalse, l'analyse ne s'appuie pas sur la base de connaissances ; avecsend_solutionsàfalse, elle n'utilise pas les solutions mémorisées. Absent, les deux valenttrue(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.
{
"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"
}{
"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 :
duplicatesliste au plus 5 tickets de l'espace déjà analysés et créés dans GLPI ces 7 derniers jours (à défaut deglpi_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èscurrent.statusouPOST /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_solvedliste 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 queplugin_options.send_solutionsne vaut pasfalse;- 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 queplugin_options.kb_syncne vaut pasfalse. 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 dansknowledge. Le contexte retrouvé est transmis au modèle comme une simple donnée de référence, jamais comme une instruction ; knowledgeliste 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 GLPIglpi_kb_id, son titretitletel qu'il a été synchronisé (mentions neutres à la place des données personnelles), lescorede son passage le plus proche (au moins 0,86, arrondi à 3 décimales) etaudience:faq(FAQ publique, dont les passages les plus proches peuvent être fournis au modèle) ouinternal(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.
| Champ | Type | Requis | Description |
|---|---|---|---|
analysis_id | UUID | oui | analysis_id renvoyé par POST /analyze, dans le même espace (sinon 404 not_found). |
category_accepted | booléen ou null | non | 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. |
urgency_accepted | booléen ou null | non | Même chose pour l’urgence suggérée. |
reply_used | used, edited, ignored ou null | non | 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. |
- Champ
analysis_id- Type
- UUID
- Requis
- oui
- Description
analysis_idrenvoyé parPOST /analyze, dans le même espace (sinon404 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 :
usedinséré (signalé à l'insertion),editedinséré puis modifié (signalé quand le texte du suivi change),ignoredjamais inséré (signalé à la résolution ou à la clôture du ticket). Ne fait qu'avancer :ignored<used<edited.
{
"analysis_id": "6f1c2a9e-8f0b-4c55-9d1e-2b7c3a4d5e6f",
"category_accepted": true
}{
"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 (sinon413 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 dansunchanged. - Désactiver l'option dans l'application, ou passer à l'offre Starter, supprime la base synchronisée.
| Champ | Type | Requis | Description |
|---|---|---|---|
items | tableau, 50 max. | non | 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. |
deleted_ids | tableau, 500 max. | non | 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. |
- 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 ouAAAA-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é) etis_faq(truepour 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 dansknowledge). 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.
{
"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]
}{ "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 (
purgedsur/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.
| Champ | Type | Requis | Description |
|---|---|---|---|
glpi_ticket_id | entier de 1 à 2 147 483 647 | oui | Ticket résolu ou clos, déjà analysé par Tiqo (sinon 404 not_found, details.resource = ticket). |
glpi_entity_id | entier ≥ 0 | oui | Entité du ticket : la solution n’est proposée que pour les tickets de la même entité. |
title | texte, 2 000 max. | non | Titre du ticket. |
solution_text | texte, 1 à 100 000 | oui | Texte 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.
{
"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."
}{ "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.
| Champ | Type | Requis | Description |
|---|---|---|---|
events | tableau, 1 à 100 | oui | [{ 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:solvedouclosed(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é.
{
"events": [
{ "glpi_ticket_id": 1039, "event": "solved" },
{ "glpi_ticket_id": 1020, "event": "reopened" },
{ "glpi_ticket_id": 987, "event": "purged" }
]
}{ "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.
{
"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.
{ "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) :
{
"error": {
"code": "invalid_signature",
"message": "Missing or invalid X-Tiqo-Signature or X-Tiqo-Timestamp header.",
"details": { "reason": "stale_timestamp" }
}
}| HTTP | Code | Signification | Que faire |
|---|---|---|---|
| 400 | invalid_request | 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). | Corriger la requête ; ne pas réessayer telle quelle. |
| 401 | invalid_api_key | Clé absente, inconnue ou régénérée depuis. | Vérifier la clé. |
| 401 | invalid_signature | details.reason : missing_signature, malformed_signature, missing_timestamp, invalid_timestamp, stale_timestamp, mismatch. | Vérifier le secret, les octets signés et l'horloge (NTP). |
| 402 | plan_limit_reached | Quota 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. |
| 402 | trial_expired | L'essai est terminé. | Choisir une offre. |
| 402 | payment_required | Facture 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). |
| 402 | plan_canceled | L'abonnement est résilié. | Reprendre un abonnement. |
| 402 | plan_feature_unavailable | 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. | Passer à l’offre Pro, ou désactiver l’option dans le plugin. |
| 402 | retrieval_limit_reached | 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. | Retirer des articles du périmètre, attendre le mois suivant (jetons) ou passer à l’offre Pro. |
| 403 | feature_disabled | Option 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. |
| 404 | not_found | 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é. | Ne pas réessayer. |
| 409 | duplicate_idempotency_key | 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. | 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é. |
| 413 | payload_too_large | Corps de plus de 4 Mio (1 Mio pour /kb/sync). | Réduire le contenu (jamais de pièces jointes). |
| 429 | rate_limited | 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). | Attendre la durée de Retry-After, puis réessayer avec une nouvelle clé d’idempotence. |
| 500 | internal_error | Erreur inattendue du service. | Réessayer plus tard. |
| 502 | ai_invalid_output | Réponse du modèle rejetée par la validation. | Réessayer avec une nouvelle clé d’idempotence. |
| 503 | ai_unavailable | 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. | Réessayer avec une nouvelle clé d’idempotence. |
- HTTP
- 400
- Code
invalid_request- Signification
- Corps invalide :
details.issues, oudetails.reason=empty_body,malformed_json,missing_idempotency_key, outoo_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/syncavec des articles etPOST /solutionsdemandent 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/syncsans article) etPOST /tickets/eventssont 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_articlesoukb_chunks(articles ou passages après la requête),embedding_tokens(volume mensuel d'embeddings, partagé par/kb/syncet/solutions, remplacement d'une solution compris) ousolutions(solutions mémorisées ; remplacer la solution d'un ticket déjà mémorisé ne compte pas), avecdetails.limitetdetails.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 dansdetails.refused_ids, etdetails.truncated_idsliste 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_syncousolved_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) :analysissur/feedback, par exemple après une purge des données, outicketsur/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,/solutionset/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/syncet/solutions,details.reason=embeddings_disabledquand le calcul des embeddings est coupé côté serveur. Sur/kb/sync,details.reason=deadlinequand 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 dansunchanged. - 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èsRetry-Afteravec 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,/solutionset/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