API des bases de connaissance
Chercher dans une base, demander une réponse rédigée et renvoyer son issue depuis un module Odoo ou tout autre logiciel.
11 min de lectureMis à jour le 13 novembre 2026
L'API donne à un logiciel tiers ce que fait la page Bases de connaissance : chercher des passages, demander une réponse rédigée, puis dire ce qu'il en est advenu. Elle ne dépend pas de votre connexion Odoo : n'importe quel logiciel capable d'un appel HTTPS peut s'en servir.
Authentification
Créez une clé dans l'onglet Clés d'API d'une base. Elle commence par aid_kb_ et n'est affichée qu'une fois.
- Par défaut, une clé est limitée à la base où elle a été créée et ne peut que chercher.
- Cochez Autoriser la rédaction de réponses pour lui donner le droit de demander des réponses.
- Cochez Donner accès à toutes les bases de l'espace seulement si le logiciel en a besoin.
Envoyez la clé dans l'en-tête Authorization de chaque appel :
curl https://api.aidoo.ai/v1/knowledge/bases \
-H "Authorization: Bearer aid_kb_votre_cle"Une clé reste côté serveur
Ne placez jamais une clé aid_kb_ dans une page web ou une application mobile. Dans Odoo, rangez-la dans un paramètre système lisible par les seuls administrateurs.
Lister les bases
GET /v1/knowledge/bases rend les bases que la clé peut lire, avec leurs étiquettes.
{
"bases": [
{
"id": "6734c0f2a1b2c3d4e5f60718",
"name": "Support",
"description": "",
"use_case": "support",
"labels": [
{ "key": "product", "name": "Produit", "values": ["Compta", "Paie"], "required": false }
],
"entry_count": 412
}
]
}Chercher
POST /v1/knowledge/search
| Champ | Type | Rôle |
|---|---|---|
base | texte, obligatoire | identifiant de la base |
query | texte, obligatoire | la question (2 000 caractères au plus) |
labels | objet | filtre, par exemple {"product": ["Paie"]} |
limit | entier | nombre de passages (20 au plus), celui de la base par défaut |
curl https://api.aidoo.ai/v1/knowledge/search \
-H "Authorization: Bearer aid_kb_votre_cle" \
-H "Content-Type: application/json" \
-d '{"base": "6734c0f2a1b2c3d4e5f60718", "query": "erreur ERR-1045 à la clôture"}'{
"results": [
{
"entry_id": "6734c1aa...",
"title": "Clôture de l'exercice",
"url": "https://aide.exemple.com/cloture",
"section": "Clôture de l'exercice > Erreurs fréquentes",
"excerpt": "Le message ERR-1045 apparaît quand un journal contient encore des écritures en brouillard.",
"score": 0.71,
"exact_match": true,
"labels": { "product": ["Compta"] },
"stale": false
}
]
}score est la proximité de sens (0 à 1). exact_match vaut true quand le passage contient les mots exacts de la question. stale signale une entrée dont la date de revalidation est dépassée. Une liste vide signifie que la base n'a rien d'assez proche.
La recherche est gratuite.
Demander une réponse
POST /v1/knowledge/answers (clé avec le droit de rédaction)
| Champ | Type | Rôle |
|---|---|---|
base | texte, obligatoire | identifiant de la base |
body | texte, obligatoire | la demande du client (12 000 caractères au plus) |
subject | texte | l'objet du ticket ou du message |
labels | objet | filtre de recherche |
external_ref | texte | votre référence, par exemple ticket-482:message-9051 |
clarify | booléen | false pour interdire les questions de précision sur cet appel (vrai par défaut) |
curl https://api.aidoo.ai/v1/knowledge/answers \
-H "Authorization: Bearer aid_kb_votre_cle" \
-H "Content-Type: application/json" \
-d '{
"base": "6734c0f2a1b2c3d4e5f60718",
"subject": "Clôture impossible",
"body": "Bonjour, la clôture échoue avec le message ERR-1045. Que faire ?",
"external_ref": "ticket-482:message-9051"
}'{
"answer": {
"id": "6734d2bb...",
"base": "6734c0f2a1b2c3d4e5f60718",
"action": "propose",
"reason": "review_required",
"body": "Le message ERR-1045 signale qu'un journal contient encore des écritures en brouillard. Validez ces écritures, puis relancez la clôture.",
"body_html": "<p>Le message ERR-1045 signale qu'un journal contient encore des écritures en brouillard. Validez ces écritures, puis relancez la clôture.</p>",
"confidence": 1,
"checks": { "citations": true, "quotes": true, "links": true, "numbers": true, "grounded": 1 },
"missing_info": [],
"questions": [],
"sources": [
{ "index": 1, "entry_id": "6734c1aa...", "title": "Clôture de l'exercice", "url": "https://aide.exemple.com/cloture", "section": "Erreurs fréquentes", "stale": false }
],
"credits": 3,
"outcome": null,
"created_at": "2026-11-13T10:12:44.000Z"
}
}Le champ action
C'est lui que votre logiciel doit suivre.
action | Ce que fait votre logiciel |
|---|---|
propose | affiche body à une personne, qui relit, corrige et envoie |
send | peut envoyer body sans relecture |
clarify | body pose au client les questions listées dans questions : la demande est trop vague pour y répondre |
acknowledge | body est un message de prise en charge : la base ne contient pas la réponse |
none | rien à envoyer ; sources peut lister les entrées les plus proches |
send n'est rendu que si l'envoi automatique est activé dans les réglages de la base et que tous les contrôles passent. Tant que vous ne l'avez pas activé, vous ne recevrez jamais send.
Le champ reason dit pourquoi : review_required, auto_send, checks_failed, missing_info, stale_source, below_threshold, no_passage, abstained, not_grounded, unclear_request.
body est en Markdown simple (paragraphes, listes, gras) ; body_html en est la version HTML, déjà échappée. Les liens vers les articles ne sont pas dans le texte : ils sont dans sources, que vous affichez comme vous le souhaitez.
Les questions de précision
Quand la demande ne dit pas assez clairement ce qui est attendu (« ça ne marche pas », sans logiciel, écran ni message d'erreur), la base ne devine pas : action vaut clarify, questions porte une à trois questions précises et body les présente dans un message prêt à relire, dans la langue du client. Aucune source n'est rendue, aucun fait n'est avancé.
Une demande claire à laquelle la base ne sait pas répondre ne donne jamais clarify : elle suit le réglage « Quand la base ne contient pas la réponse ».
Quand le client répond, rappelez l'API avec :
- dans
body, la demande d'origine suivie de sa réponse ; - une nouvelle
external_ref(celle du nouveau message) ; "clarify": false, pour que la base réponde ou passe la main au lieu de poser de nouvelles questions.
Une réponse rédigée peut aussi se terminer par une question, quand un détail changerait la marche à suivre : action vaut alors propose et questions porte cette question. Les questions se désactivent pour toute la base dans ses réglages (« Quand la demande n'est pas assez claire »).
Une même référence, une seule réponse
Avec external_ref, un second appel pour la même référence rend la même réponse, sans rien recalculer ni refacturer. Donnez une référence qui change à chaque nouveau message du client (identifiant du ticket et du message), pas seulement l'identifiant du ticket.
Durée et crédits
L'appel est synchrone et dure de quelques secondes à une minute. Appelez-le depuis une tâche de fond, jamais pendant l'enregistrement d'une fiche. Il consomme des crédits de l'espace (champ credits), sauf quand rien n'est trouvé. Quand les crédits sont épuisés, la réponse est 402.
Relire une réponse
GET /v1/knowledge/answers/{id} rend la même forme que ci-dessus.
Renvoyer l'issue
POST /v1/knowledge/answers/{id}/outcome
Dites à Aidoo ce qu'il est advenu de la réponse. C'est ce qui alimente les indicateurs de la base, et ce qui permettra de régler le seuil d'envoi automatique sur des faits.
| Champ | Type | Rôle |
|---|---|---|
outcome | texte, obligatoire | sent_unchanged, sent_edited, rejected, auto_sent ou reopened |
final_body | texte | le texte réellement envoyé |
reason | texte | motif d'un rejet (200 caractères au plus) |
curl https://api.aidoo.ai/v1/knowledge/answers/6734d2bb.../outcome \
-H "Authorization: Bearer aid_kb_votre_cle" \
-H "Content-Type: application/json" \
-d '{"outcome": "sent_edited", "final_body": "Bonjour, validez vos écritures en brouillard puis relancez la clôture."}'Si vous envoyez final_body, Aidoo compare les deux textes et corrige lui-même entre sent_unchanged et sent_edited. Le texte envoyé sert à cette mesure puis il est jeté : il n'est ni conservé, ni versé dans la base.
reopened se renvoie quand le client rouvre le ticket après une réponse : c'est le meilleur signal qu'une réponse était fausse.
Lire les indicateurs
GET /v1/knowledge/stats?base={id}&days=30 (clé avec le droit de rédaction)
Rend les indicateurs de l'onglet Indicateurs sur 7, 30 ou 90 jours : nombre de demandes, décisions prises (by_action), issues renvoyées (by_outcome), part envoyée sans retouche (unchanged_rate), part sans réponse dans la base (no_answer_rate), crédits consommés, et les entrées les plus citées.
curl "https://api.aidoo.ai/v1/knowledge/stats?base=6734c0f2a1b2c3d4e5f60718&days=30" \
-H "Authorization: Bearer aid_kb_votre_cle"unchanged_rate ne porte que sur les réponses dont vous avez renvoyé l'issue : sans appel à outcome, il reste à zéro.
Erreurs et limites
| Code | Sens |
|---|---|
400 | champ manquant ou invalide (KNOWLEDGE_INVALID_INPUT, KNOWLEDGE_INVALID_LABELS) |
401 | clé absente, inconnue ou révoquée |
402 | crédits de l'espace épuisés (KNOWLEDGE_CREDITS_EXHAUSTED) |
403 | la clé n'a pas ce droit, ou pas sur cette base (KNOWLEDGE_SCOPE_REQUIRED) |
404 | base ou réponse introuvable |
409 | une réponse est déjà en cours pour cette external_ref : réessayez un peu plus tard (KNOWLEDGE_ANSWER_IN_PROGRESS) |
429 | trop d'appels : attendez le délai de l'en-tête Retry-After |
503 | service momentanément indisponible, réessayez |
Limites par clé : 60 appels par minute, dont 20 demandes de réponse.
Brancher un module Odoo
Voici le chemin le plus court pour proposer une réponse sur un ticket, que ce soit avec le Helpdesk d'Odoo Enterprise, le module helpdesk_mgmt de l'OCA ou tout modèle qui porte un fil de discussion.
1. Ranger les réglages
Deux paramètres système (ir.config_parameter), lisibles par les administrateurs seulement :
aidoo_knowledge.api_key: la cléaid_kb_;aidoo_knowledge.base_id: l'identifiant de la base.
2. Ajouter les champs au ticket
from odoo import fields, models
class HelpdeskTicket(models.Model):
_inherit = "helpdesk.ticket"
aidoo_answer_id = fields.Char(copy=False)
aidoo_answer_action = fields.Selection(
[("propose", "À relire"), ("send", "Envoi automatique"),
("clarify", "Questions au client"), ("acknowledge", "Prise en charge"),
("none", "Rien à envoyer")],
copy=False,
)
aidoo_answer_body = fields.Html(copy=False, sanitize=True)
aidoo_answer_sources = fields.Json(copy=False)
aidoo_answer_pending = fields.Boolean(default=True, copy=False, index=True)Un ticket créé a aidoo_answer_pending à vrai : c'est la tâche planifiée qui le traitera. Pour proposer aussi une réponse à chaque nouveau message du client, remettez ce champ à vrai dans message_post quand l'auteur est le client.
3. Appeler l'API depuis une tâche planifiée
import requests
from odoo import api, models
API = "https://api.aidoo.ai/v1/knowledge"
class HelpdeskTicket(models.Model):
_inherit = "helpdesk.ticket"
@api.model
def _cron_aidoo_answers(self, limit=20):
params = self.env["ir.config_parameter"].sudo()
key = params.get_param("aidoo_knowledge.api_key")
base = params.get_param("aidoo_knowledge.base_id")
if not key or not base:
return
for ticket in self.search([("aidoo_answer_pending", "=", True)], limit=limit):
last = ticket.message_ids.filtered(lambda m: m.message_type == "email")[:1]
payload = {
"base": base,
"subject": ticket.name or "",
"body": ticket.description or "",
"external_ref": f"ticket-{ticket.id}:message-{last.id or 0}",
}
try:
response = requests.post(
f"{API}/answers",
json=payload,
headers={"Authorization": f"Bearer {key}"},
timeout=90,
)
except requests.RequestException:
continue # la prochaine passe réessaiera
if response.status_code == 402:
break # crédits épuisés : inutile d'insister
if response.status_code in (409, 429, 503):
continue # déjà en cours, ou trop d'appels : la prochaine passe réessaiera
if response.status_code != 201:
ticket.aidoo_answer_pending = False
continue
answer = response.json()["answer"]
ticket.write({
"aidoo_answer_pending": False,
"aidoo_answer_id": answer["id"],
"aidoo_answer_action": answer["action"],
"aidoo_answer_body": answer["body_html"],
"aidoo_answer_sources": answer["sources"],
})
self.env.cr.commit()Planifiez _cron_aidoo_answers toutes les minutes ou toutes les cinq minutes. L'appel dure plusieurs secondes : il ne doit jamais partir de create ou de write.
Filtrer par étiquette
Si votre base a une étiquette product, déduisez sa valeur de l'équipe ou du type du ticket et passez "labels": {"product": ["Paie"]}. La recherche ne regarde alors que les entrées de ce produit, ce qui améliore nettement les réponses d'un support multi-produits.
4. Montrer la réponse sur la fiche
Affichez aidoo_answer_body dans un encart du formulaire du ticket, avec la liste des sources et deux boutons :
- Envoyer : poste le texte (éventuellement corrigé) dans le fil de discussion avec
message_post, puis renvoie l'issue ; - Ignorer : renvoie l'issue
rejected.
def action_aidoo_send(self):
self.ensure_one()
self.message_post(
body=self.aidoo_answer_body,
message_type="comment",
subtype_xmlid="mail.mt_comment",
partner_ids=self.partner_id.ids,
)
self._aidoo_outcome("sent_unchanged", final_body=self.aidoo_answer_body)
def _aidoo_outcome(self, outcome, final_body=None, reason=None):
params = self.env["ir.config_parameter"].sudo()
key = params.get_param("aidoo_knowledge.api_key")
if not key or not self.aidoo_answer_id:
return
payload = {"outcome": outcome}
if final_body:
payload["final_body"] = final_body
if reason:
payload["reason"] = reason
try:
requests.post(
f"{API}/answers/{self.aidoo_answer_id}/outcome",
json=payload,
headers={"Authorization": f"Bearer {key}"},
timeout=20,
)
except requests.RequestException:
pass # l'issue est un indicateur, elle ne doit pas bloquer l'envoiEnvoyez toujours sent_unchanged avec final_body : Aidoo le requalifie en sent_edited si le texte a été retouché.
5. L'envoi automatique, plus tard
Quand action vaut send, votre module peut poster la réponse sans attendre, puis renvoyer l'issue auto_sent. Ne l'activez dans les réglages de la base qu'après avoir observé, pendant plusieurs semaines, la part des réponses envoyées sans retouche.
Le message de prise en charge
Quand action vaut acknowledge, body n'apporte aucune réponse : c'est un accusé de réception. Décidez dans votre module s'il part tout seul ou s'il attend une personne, et ne le présentez jamais comme une réponse au problème.