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ètreTypeDéfautComportement
limitentier50Taille de page. Bornée à l'intervalle [1, 200] — une valeur inférieure à 1 devient 1, une valeur supérieure à 200 devient 200.
offsetentier0Nombre 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
}
  • nextCursor est 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.
  • hasMore indique s'il faut continuer ; nextCursor n'a de sens que tant que hasMore vaut true (il est nullable et absent sur la dernière page).
  • total reste le décompte global correspondant à vos filtres, mais pilotez la boucle avec hasMore/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
}
ChampTypeDescription
codechaîneIdentifiant d'erreur stable et lisible par machine (par exemple not_found, insufficient_permission). Fiable pour brancher votre code dessus.
messagechaîneExplication lisible par un humain. Destinée aux journaux et au débogage, pas aux instructions switch.
statusentierLe code de statut HTTP, dupliqué dans le corps.

Codes de statut

StatutSignificationQuand cela arrive
400 Bad RequestEntrée malforméeUne 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 UnauthorizedIdentifiants absents ou invalidesPas d'en-tête Authorization/X-Api-Key, ou clé non reconnue, révoquée ou expirée. Cas particulier : voir ci-dessous.
403 ForbiddenNon 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 FoundLa ressource n'existe pas dans votre espace de travailL'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 ConflictLa requête entre en conflit avec l'état courantRéservé par le contrat d'erreur ; aucun endpoint ne le produit actuellement.
422 Unprocessable EntityBien formé mais sémantiquement invalideLa 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).
5xxErreur serveur inattendueHors 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 extraire code/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.

codestatus typiqueRemarques
bad_request400Cas générique par défaut d'entrée malformée.
invalid_uuid400Une valeur de chemin ou de requête censée être un UUID n'a pas pu être analysée.
invalid_datetime400Une valeur de date-heure n'est pas de l'ISO-8601 valide.
unauthorized401Défini par le contrat, mais inatteignable en pratique — les véritables échecs d'authentification n'ont pas de corps JSON.
forbidden403La clé n'est liée à aucun espace de travail (ou autre échec d'autorisation générique).
insufficient_permission403La clé est liée à un espace de travail mais ne porte pas le scope requis.
not_found404La ressource n'existe pas dans l'espace de travail de la clé.
conflict409Réservé — actuellement émis par aucun endpoint.
unprocessable_entity422La 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 :

RessourceChamp
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.