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.
Le document OpenAPI
Section intitulée « Le document OpenAPI »curl -s https://api.menestrel.dev/v1/openapi.jsonAucune 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.
Authentification
Section intitulée « Authentification »Les routes de contenu attendent un token de projet dans l’en-tête Authorization, au format Bearer :
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.
Contenu publié : ce que lit le loader
Section intitulée « Contenu publié : ce que lit le loader »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.
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.
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 :
POST /v1/projects/{projectId}/assets/uploadsdéclare les fichiers (filename,mime,bytes) et renvoie une URL d’upload présignée par fichier, avec l’en-têteContent-Typeà reprendre.PUTdu fichier sur chaqueuploadUrl, directement vers le stockage.POST /v1/projects/{projectId}/assets/{assetId}/completevérifie l’objet reçu et lance le traitement : réponse202tant que l’asset est en cours,200quand 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.
Publication et cibles de déploiement
Section intitulée « Publication et cibles de déploiement »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.
Format d’erreur
Section intitulée « Format d’erreur »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’unvalidation_failed(400).
Limites de débit
Section intitulée « Limites de débit »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.