Pagination, erreurs et polling
Les enveloppes de pagination, le contrat d'erreur uniforme et comment détecter les changements sans webhooks
Cette page couvre les contrats transverses partagés par tous les endpoints : la pagination, la forme d'erreur uniforme et — l'API n'ayant pas de webhooks — la façon d'interroger l'API pour détecter les changements.
Pagination par offset
Presque tous les endpoints de liste utilisent le même contrat de pagination par offset, avec deux paramètres de requête :
| Paramètre | Type | Défaut | Comportement |
|---|---|---|---|
limit | entier | 50 | Taille de page. Bornée à l'intervalle [1, 200] — une valeur inférieure à 1 devient 1, une valeur supérieure à 200 devient 200. |
offset | entier | 0 | Nombre d'éléments à sauter. Plancher à 0 — une valeur négative devient 0. |
Les valeurs hors bornes sont silencieusement ramenées dans l'intervalle, jamais rejetées. ?limit=10000 ne renvoie pas de 400 — c'est traité comme limit=200. Le plafond absolu est toujours de 200 éléments par appel.
Une réponse de liste paginée ressemble à ceci :
{
"data": [ ],
"pagination": {
"limit": 50,
"offset": 0,
"total": 342
}
}data— la page d'éléments.pagination.limit/pagination.offset— renvoient les valeurs effectives (après bornage) qui ont été appliquées.pagination.total— le nombre total d'éléments correspondant à la requête, toutes pages confondues. Utilisez-le pour savoir quand arrêter de paginer.
Pour parcourir toutes les pages, incrémentez offset de limit à chaque appel jusqu'à ce que offset >= total :
async function listAll(path, apiKey, limit = 200) {
const items = [];
let offset = 0;
let total = Infinity;
while (offset < total) {
const url = new URL(`https://app.kabeen.io/public/v1${path}`);
url.searchParams.set("limit", limit);
url.searchParams.set("offset", offset);
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
items.push(...body.data);
total = body.pagination.total;
offset += limit;
}
return items;
}Exception : catalogue applicatif — pagination par numéro de page
GET /application-catalog recherche dans le catalogue de référence global, indépendant de l'espace de travail, qui pagine par numéro de page plutôt que par offset/limit :
{
"data": [ ],
"total": 1204,
"page": 1
}Demandez la page suivante avec ?page=2, ?page=3, etc. (base 1, défaut 1). Il n'y a pas de paramètre limit pour cet endpoint — la taille de page est fixée par le service de catalogue. Arrêtez-vous dès que data revient vide.
Exception : journal d'audit — pagination par curseur
GET /audit-log est en ajout continu et à fort volume : il utilise donc une pagination par jeu de clés (curseur) plutôt que par offset, du plus récent au plus ancien :
{
"data": [ ],
"total": 58213,
"nextCursor": "2026-07-21T09:12:03.441Z,3f9c...",
"hasMore": true
}nextCursorest un jeton opaque — traitez-le comme une boîte noire, ne le parsez pas et ne le construisez pas vous-même.- Renvoyez-le via
?cursor=...pour obtenir la page suivante. hasMoreindique s'il faut continuer ;nextCursorn'a de sens que tant quehasMorevauttrue(il est nullable et absent sur la dernière page).totalreste le décompte global correspondant à vos filtres, mais pilotez la boucle avechasMore/nextCursor, pas en comparant un offset àtotal.
Exception : listes de sous-ressources — tableaux nus
Certains endpoints de sous-ressources renvoient un simple tableau JSON sans aucune enveloppe — pas de wrapper data, pas de pagination. Il s'agit de petites collections bornées rattachées à une ressource parente unique. Exemples : GET /applications/{id}/owners, GET /applications/{id}/tags, GET /applications/{id}/teams, GET /applications/{id}/comments, GET /servers/{id}/owners, GET /servers/{id}/tags, GET /networks/{id}/routers, GET /routers/{id}/networks, GET /routers/{id}/tags. Un appel à l'un d'entre eux renvoie directement [ ... ].
Gestion des erreurs
Chaque réponse d'erreur émise par le traitement des requêtes de l'API elle-même — chaque 400, 403, 404, 409, 422 et 5xx, sur chaque endpoint — a la même forme JSON :
{
"code": "not_found",
"message": "server not found",
"status": 404
}| Champ | Type | Description |
|---|---|---|
code | chaîne | Identifiant d'erreur stable et lisible par machine (par exemple not_found, insufficient_permission). Fiable pour brancher votre code dessus. |
message | chaîne | Explication lisible par un humain. Destinée aux journaux et au débogage, pas aux instructions switch. |
status | entier | Le code de statut HTTP, dupliqué dans le corps. |
Codes de statut
| Statut | Signification | Quand cela arrive |
|---|---|---|
400 Bad Request | Entrée malformée | Une valeur de chemin ou de requête ne peut pas être analysée — un UUID non analysable, une valeur d'enum/de filtre inconnue, une date-heure ISO-8601 invalide, ou un champ obligatoire absent/vide. |
401 Unauthorized | Identifiants absents ou invalides | Pas d'en-tête Authorization/X-Api-Key, ou clé non reconnue, révoquée ou expirée. Cas particulier : voir ci-dessous. |
403 Forbidden | Non autorisé | La clé ne porte pas le scope de permission requis par l'endpoint (insufficient_permission), ou la clé n'est liée à aucun espace de travail (forbidden). |
404 Not Found | La ressource n'existe pas dans votre espace de travail | L'id n'existe pas, ou il appartient à un autre espace de travail. L'API ne distingue jamais les deux — les deux renvoient le même 404, une clé ne peut donc pas utiliser les réponses d'erreur pour énumérer des ressources qu'elle ne possède pas. |
409 Conflict | La requête entre en conflit avec l'état courant | Réservé par le contrat d'erreur ; aucun endpoint ne le produit actuellement. |
422 Unprocessable Entity | Bien formé mais sémantiquement invalide | La requête est analysée correctement mais viole une règle métier — par exemple tenter de modifier name/os/manufacturer sur un serveur remonté par un agent (automatique). |
5xx | Erreur serveur inattendue | Hors du flux de contrôle normal — traitez-la comme transitoire et réessayez avec un backoff. |
Le cas particulier du 401
Chaque statut ci-dessus revient avec le corps JSON uniforme — sauf 401. L'authentification est appliquée avant que la requête n'atteigne la gestion d'erreurs de l'API : un véritable 401 revient donc avec :
- Statut
401, corps vide (pas de JSON — n'essayez pas d'en extrairecode/message/status). - Un en-tête de réponse
WWW-Authenticate: Bearer realm="kbine", error="invalid_token".
Si votre client attend toujours un corps d'erreur JSON, traitez 401 à part (ou branchez simplement sur le statut seul).
400 et 422 se recouvrent dans l'esprit mais pas dans l'intention : 400 signifie que l'API n'a même pas pu analyser ce que vous avez envoyé ; 422 signifie que l'analyse a réussi mais que le sens est invalide compte tenu de l'état actuel de la ressource.
Valeurs de code connues
De nouveaux codes pourront être ajoutés dans de futures versions — prévoyez toujours un cas par défaut.
code | status typique | Remarques |
|---|---|---|
bad_request | 400 | Cas générique par défaut d'entrée malformée. |
invalid_uuid | 400 | Une valeur de chemin ou de requête censée être un UUID n'a pas pu être analysée. |
invalid_datetime | 400 | Une valeur de date-heure n'est pas de l'ISO-8601 valide. |
unauthorized | 401 | Défini par le contrat, mais inatteignable en pratique — les véritables échecs d'authentification n'ont pas de corps JSON. |
forbidden | 403 | La clé n'est liée à aucun espace de travail (ou autre échec d'autorisation générique). |
insufficient_permission | 403 | La clé est liée à un espace de travail mais ne porte pas le scope requis. |
not_found | 404 | La ressource n'existe pas dans l'espace de travail de la clé. |
conflict | 409 | Réservé — actuellement émis par aucun endpoint. |
unprocessable_entity | 422 | La requête est bien formée mais sémantiquement invalide pour la ressource cible. |
Bonnes pratiques : vérifiez d'abord le statut HTTP, branchez sur code (jamais sur message), journalisez message pour le diagnostic, et repliez-vous sur status pour tout code que vous ne reconnaissez pas.
Pas de webhooks — interrogez l'API
L'API n'offre pas de webhooks, d'abonnements à des événements ni de mécanisme de push/callback dans cette version. Elle fonctionne uniquement en requête/réponse (pull). Deux approches complémentaires couvrent presque tous les cas d'usage « détecter un changement » :
1. Interroger le journal d'audit (ce qui se rapproche le plus d'un flux de changements)
GET /public/v1/audit-log (scope audit_log:read) est aujourd'hui la meilleure approximation d'un flux d'événements offerte par l'API. Il renvoie les événements d'audit de l'espace de travail, du plus récent au plus ancien, et prend en charge le filtrage par plage de dates (from/to), par acteur (actorId), par catégorie/action (category, actions répétable), par ressource (resourceType, resourceId), la recherche en texte libre (query) et la pagination par curseur (limit max 200 + cursor).
Interrogez-le à intervalle régulier, en avançant avec nextCursor tant que hasMore vaut true, et persistez le dernier curseur (ou le timestamp du dernier événement traité) entre les exécutions :
cursor = load_last_cursor()
loop:
page = GET /audit-log?limit=200&cursor={cursor}
for event in page.data:
handle(event)
if page.nextCursor:
cursor = page.nextCursor
save_cursor(cursor)
if not page.hasMore:
sleep(poll_interval)2. Comparer les horodatages des ressources
La plupart des ressources portent un horodatage que vous pouvez comparer à un passage précédent :
| Ressource | Champ |
|---|---|
Applications (GET /applications/{id}) | updatedAt |
Objets de données (GET /data, GET /data/{id}) | updatedAt |
Contrats (GET /contracts) | updatedAt |
Équipes (GET /teams, GET /teams/{id}) | updatedAt |
Annonces (GET /announcements) | updatedAt |
Applications découvertes (GET /discovered-applications) | lastUpdate |
Serveurs (GET /servers, GET /servers/{id}) | lastCheckTime |
C'est plus grossier que le journal d'audit — cela vous dit que quelque chose a changé, pas quoi ni qui — mais c'est simple et cela fonctionne même pour les ressources sans catégorie d'audit dédiée.
Polling efficace
- Respectez le plafond de taille de page (200) et demandez la plus grande page que votre intervalle peut confortablement traiter.
- Choisissez un intervalle raisonnable. Un intervalle de 1 à 5 minutes est un bon défaut pour la plupart des intégrations.
- Persistez toujours votre curseur/horodatage avant de traiter la page, afin qu'un crash ou un redémarrage reprenne au bon endroit plutôt que de retraiter ou de sauter des événements.
- Préférez le journal d'audit quand vous avez besoin de savoir « ce qui a changé » — il connaît l'acteur, l'action et la ressource.