Spunto CMS
L'API

Utiliser l'API

URL de base, authentification, erreurs et client généré — ce qui vaut pour toutes les routes de l'API REST du CMS headless Spunto.

L'API est du REST sur JSON. Chaque route est décrite dans la référence, générée à partir de la spécification servie à /openapi.json.

Cette page couvre ce qui vaut pour toutes.

URL de base

Sur cms.spunto.net, une seule adresse publique sert tout le produit et route /api/* vers l'API :

https://cms.spunto.net/api

Le préfixe est retiré avant que l'API ne voie la requête : une route documentée comme /workspaces s'appelle donc à https://cms.spunto.net/api/workspaces.

La spécification elle-même est servie à la racine, /openapi.json, sans le préfixe — c'est la seule exception, et elle existe pour que la page de référence puisse la charger.

Si vous l'hébergez vous-même

Le préfixe /api appartient au proxy, pas à l'API. Lancez la stack en local et il n'y a pas de proxy : l'API écoute sans préfixe, sur son propre port, donc chaque exemple ici devient http://localhost:3001/workspaces/… — retirez le /api, changez l'hôte.

Deux exceptions répondent sur le port du front dans tous les environnements, parce qu'une adresse documentée qui ne répond pas là où vous travaillez est une adresse fausse : /api/docs et /openapi.json. Elles sont relayées, et rien d'autre sous /api ne l'est.

S'authentifier

Toutes les routes, sauf une poignée de portes d'entrée, exigent un jeton bearer :

Authorization: Bearer spk_...

Trois sortes de jetons passent, et une fois la porte franchie rien ne les distingue :

JetonQuiPortée
une sessionune personne connectéeses espaces de travail, son rôle dans chacun, 30 jours
spk_…une clé APIun espace de travail, un rôle fixe, jusqu'à révocation
spa_…une app installéeun espace de travail, un rôle et des scopes

Utilisez une clé API pour tout ce qui tourne sans surveillance. Créez-en une dans les réglages d'un espace de travail. Le secret n'est renvoyé qu'une fois, à la création — seul son SHA-256 est conservé.

L'espace de travail et le rôle d'une clé sont fixés à l'émission. Ils ne suivent pas l'appartenance de la personne qui l'a créée, et c'est tout l'intérêt : une clé ne doit ni gagner ni perdre de pouvoir parce qu'une personne a changé de poste.

Une clé API ne peut gérer ni les clés, ni les invitations, ni les membres, ni les installations d'apps. Tout cela exige une session humaine.

Ce qui est public

Neuf routes répondent sans jeton, pas une de plus : le contrôle de santé, la spécification, la page de référence, la politique d'inscription, l'aperçu d'une invitation, l'inscription, la connexion, l'échange de la connexion Google et le point d'accès de jeton OAuth.

Cette liste n'est pas tenue à jour en relisant le code et en espérant. Un test balaie toutes les routes de la spécification et exige un 401, avec les exceptions nommées une par ligne et justifiées. Une nouvelle route est protégée le jour même où elle est écrite, parce que les gardes sont montées sur le préfixe du chemin et non route par route.

Erreurs

Les erreurs sont du JSON, de la forme { "error": "..." }, et c'est le statut HTTP qui porte le sens :

StatutSignifie
400La requête est mal formée, ou une valeur a échoué à la validation
401Pas de bearer, ou un bearer qui n'est plus valide
404Cela n'existe pas — ou ce n'est pas à vous
409La requête entre en conflit avec l'état actuel

Le 404 a une double fonction, et c'est voulu

Il n'y a pas de 403 pour « vous n'êtes pas membre de cet espace de travail ». Un 403 confirmerait l'existence de l'espace à qui énumère des identifiants. L'API ne vous dira pas lequel des deux cas vous avez rencontré.

Pagination

Les listes se paginent par curseur keyset. Une réponse paginée porte nextCursor ; renvoyez-le comme cursor pour obtenir la page suivante. null signifie que vous êtes au bout.

Il n'y a ni OFFSET ni numéro de page, nulle part, et c'est une décision tranchée plutôt qu'un oubli — voir les vues sauvegardées.

Les curseurs sont opaques. Ne les analysez pas, ne les construisez pas, et n'en réutilisez pas un après un changement d'ordre de tri.

Le journal d'événements est la seule exception : son curseur est un identifiant d'événement, lisible à dessein.

Dates

Toutes les dates que renvoie l'API sont des chaînes ISO 8601. Pas de timestamps numériques, pas de valeurs formatées selon une locale.

Un client TypeScript généré

La spécification servie à /openapi.json est tout ce dont un client a besoin. Générez-en les types avec openapi-typescript et appelez l'API avec openapi-fetch :

npm install openapi-fetch
npx openapi-typescript https://cms.spunto.net/openapi.json -o spunto-api.d.ts
import createClient from "openapi-fetch"
import type { paths } from "./spunto-api"

const api = createClient<paths>({
  baseUrl: "https://cms.spunto.net/api",
  headers: { Authorization: `Bearer ${process.env.SPUNTO_TOKEN}` },
})

const { data, error } = await api.GET("/workspaces")

C'est exactement ainsi que le front de ce produit parle à l'API : son client est généré par les deux mêmes outils, depuis la même spécification commitée, si bien qu'une route qui change de forme devient une erreur de typage plutôt qu'une surprise à l'exécution. Ce paquet interne n'est pas publié sur npm ; générer le vôtre tient en une commande et vous fixe à la version de l'API contre laquelle vous l'avez généré.

Il existe aussi un client Swift, généré depuis la même spécification au build de l'app iOS.

Les champs de réponse sont ajoutés en optionnel

Un champ ajouté à un schéma de réponse est optionnel, toujours. Le générateur Swift décode strictement : un champ requis de plus ne dégrade pas un client qui n'a pas été mis à jour — il fait échouer la réponse entière. Comme les clients se déploient séparément de l'API, « l'app est en avance sur le serveur » est un état normal, pas un incident. Écrivez vos consommateurs en conséquence.

Limites de débit

Il n'y en a aucune aujourd'hui. C'est un constat sur l'implémentation actuelle, pas une promesse — ne construisez rien qui dépende de leur absence.

Sur cette page