Aller au contenu

Référence de l'API HTTP

Tout ce que font l’admin, le CLI et le loader passe par la même API REST. Ce que tu peux cliquer, tu peux le scripter.

  • Base URL : https://api.menestrel.dev
  • Préfixe : toutes les routes commencent par /v1
  • Format : JSON en entrée comme en sortie

Cette page documente les patterns : authentification, grands groupes d’endpoints, format d’erreur, limites de débit. La liste exhaustive des routes vit dans le document OpenAPI, généré depuis le code du serveur, donc jamais en retard sur lui.

Fenêtre de terminal
curl -s https://api.menestrel.dev/v1/openapi.json

Aucune authentification requise. Importe-le dans Bruno, Postman ou un générateur de client : chaque route y décrit ses paramètres, ses corps de requête et ses réponses.

Les routes de contenu attendent un token de projet dans l’en-tête Authorization, au format Bearer :

Fenêtre de terminal
curl -s https://api.menestrel.dev/v1/tokens/me \
-H "Authorization: Bearer cmt_adm_xxx"

Un token appartient à un seul projet et porte une portée (read_published, read_draft ou admin) qui borne ce qu’il autorise. GET /v1/tokens/me renvoie l’identité du token courant : pratique pour vérifier un .env sans rien modifier. Les portées et leur cycle de vie sont détaillés dans Jetons d’API et sécurité.

Trois réponses d’échec, volontairement avares en détail :

Statut Code Situation
401 auth.missing_token Pas d’en-tête Authorization: Bearer.
401 auth.invalid_token Token inconnu, révoqué ou expiré : même réponse dans les trois cas.
403 auth.insufficient_scope La portée du token ne suffit pas pour cette route.

Un token valide qui cible un autre projet que le sien reçoit 404 project.not_found, jamais un 403 : l’API ne confirme pas l’existence de ce qu’elle ne montre pas.

GET /v1/projects/{projectId}/snapshot/latest renvoie le pointeur vers le dernier snapshot publié. C’est la route qu’utilise le loader en mode MENESTREL_TOKEN ; elle accepte toutes les portées, read_published suffit.

Fenêtre de terminal
curl -s https://api.menestrel.dev/v1/projects/prj_xxx/snapshot/latest \
-H "Authorization: Bearer cmt_pk_xxx"
{
"snapshot_id": "snap_xxx",
"published_at": "2026-07-08T14:03:21.000Z",
"content_base_url": "https://content.menestrel.dev/p/prj_xxx/pk_xxx",
"manifest": { "manifest_format": 1, "...": "..." }
}

Le contenu lui-même n’est pas servi par l’API : content_base_url pointe vers le CDN, où chaque snapshot est immuable. Ce découplage permet au build de passer même quand l’application est éteinte ; voir la référence du loader. Si rien n’a jamais été publié, la route répond 404 snapshot.none_published.

GET /v1/projects/{projectId}/snapshots/{snapshotId}/{collection}.{locale}.json redirige (302) vers le fichier de la collection sur le CDN, pour un snapshot précis.

Les routes vivent sous /v1/projects/{projectId}/entries. Par token, elles exigent la portée admin, lecture comprise.

Route Rôle
GET /entries Liste paginée par curseur ; filtres collection, status, search.
POST /entries Crée une entrée : collection_key, values optionnel.
GET /entries/{entryId} Détail d’une entrée.
PUT /entries/{entryId}/draft Enregistre un brouillon : values, base_version.
POST /entries/{entryId}/publish Publie une ou plusieurs locales.
POST /entries/{entryId}/duplicate Duplique une entrée.
DELETE /entries/{entryId} Met à la corbeille (purge après 30 jours).
POST /entries/{entryId}/restore Restaure depuis la corbeille.
GET /entries/{entryId}/versions Historique des versions.
POST /entries/order Réordonne une collection triée à la main.

Exemple : publier la locale fr d’une entrée.

Fenêtre de terminal
curl -s -X POST https://api.menestrel.dev/v1/projects/prj_xxx/entries/ent_xxx/publish \
-H "Authorization: Bearer cmt_adm_xxx" \
-H "Content-Type: application/json" \
-d '{ "locales": ["fr"] }'
{
"version": 4,
"locales_status": { "fr": "published" },
"entry_status": "published",
"published_at": "2026-07-10T09:12:45.000Z",
"unchanged": false
}

Si rien n’a changé depuis la dernière publication, unchanged vaut true et aucun snapshot n’est reconstruit. Pour une reprise de contenu en masse, ajoute "via": "import" au corps de POST /entries et de /publish : les versions créées sont marquées comme importées et la limite de création passe à 2 000 par heure. C’est exactement ce que fait menestrel import, qui reste la voie recommandée.

L’upload se fait en trois temps, l’API ne voyant jamais passer les octets :

  1. POST /v1/projects/{projectId}/assets/uploads déclare les fichiers (filename, mime, bytes) et renvoie une URL d’upload présignée par fichier, avec l’en-tête Content-Type à reprendre.
  2. PUT du fichier sur chaque uploadUrl, directement vers le stockage.
  3. POST /v1/projects/{projectId}/assets/{assetId}/complete vérifie l’objet reçu et lance le traitement : réponse 202 tant que l’asset est en cours, 200 quand il est prêt.

Autour de ce flux : GET /assets (liste, recherche, filtre par dossier), PATCH /assets/{assetId} (nom de fichier, texte alternatif, point focal, dossier), DELETE et POST .../restore (corbeille), POST .../crop (recadrage), les dossiers sous /asset-folders et GET /storage pour le quota.

Le schéma se définit en code et se pousse avec menestrel sync, qui appelle POST /v1/projects/{projectId}/schema/diff puis POST .../schema/sync. GET .../schema renvoie la version active, GET .../schema/versions l’historique. En pratique, passe par le CLI : il gère le diff, la confirmation et les migrations. Voir la référence CLI.

Les cibles de déploiement (webhooks Vercel, Netlify, Cloudflare Pages, repository_dispatch GitHub) se gèrent sous /v1/projects/{projectId}/deploy-targets : création, modification, test, reprise après pannes répétées. GET /v1/projects/{projectId}/deployments liste les derniers déclenchements avec leur état, et POST /v1/projects/{projectId}/snapshots/{snapshotId}/restore remet en ligne un snapshot antérieur.

Ces routes se pilotent en session depuis l’admin, elles ne sont pas accessibles par token de projet. Le fonctionnement côté produit est décrit dans Publication.

Toutes les erreurs, 500 comprises, partagent la même enveloppe :

{
"error": {
"code": "validation_failed",
"message": "Validation failed",
"details": [{ "path": ["locales"], "message": "..." }]
}
}
  • code : stable, fait pour être testé par une machine. Préfixé par domaine (auth.invalid_token, snapshot.not_found, media.asset_in_use) ou global (validation_failed, rate_limited, internal).
  • message : lisible, en anglais, susceptible de changer. Ne teste jamais dessus.
  • details : présent quand il y a plus à dire, absent sinon. Exemple : les erreurs de validation champ par champ d’un validation_failed (400).

Les limites s’appliquent par projet et par fenêtre de temps. Les principales :

Action Limite
Publication d’entrée 30 / minute
Enregistrement de brouillon 60 / minute, par entrée et par auteur
Création ou duplication d’entrée 120 / heure
Création en mode import ("via": "import") 2 000 / heure
Listage d’entrées 600 / heure
Déclaration d’uploads médias 300 fichiers / heure
schema/diff 120 / heure
schema/sync 30 / heure
Création de token 10 / heure
Restauration de snapshot 12 / heure

Au-delà, l’API répond 429 avec le code rate_limited et un en-tête Retry-After en secondes : attends ce délai, puis rejoue la requête.