L'API REST est destinée aux pipelines, aux scripts et à tout ce qui n'est pas un agent. Si vous connectez plutôt un modèle, utilisez le serveur MCP — il expose les mêmes opérations sous forme d'outils avec le même modèle de permissions.

URL de base

fastsite est auto-hébergé, donc l'URL de base est votre propre hôte de moteur. Tout réside sous une même origine : le tableau de bord, l'API, le point de terminaison MCP et les webhooks entrants.

https://your-host/api/v1

Authentification

Chaque requête transporte une clé API dans un en-tête. Il n'y a pas de jeton bearer ni de session sur cette interface — l'authentification bearer appartient à MCP.

X-API-Key: fs_<key_id>_<secret>

Une clé est une chaîne de 44 caractères : le préfixe littéral fs_, un identifiant public de 8 caractères, un tiret bas et un secret de 32 caractères. Le secret est haché avec argon2id au repos et affiché exactement une fois, lors de la création de la clé.

php bin/fastsite key:create you@example.com \
  --name="Deploy pipeline" \
  --scopes=content:write,media:write,publish \
  --sites=blog

Utilisez --sites=* pour autoriser tous les sites. Les clés sont également gérées depuis le tableau de bord et peuvent être renouvelées ou révoquées à tout moment — une clé révoquée cesse de fonctionner immédiatement plutôt qu'à la fin d'une fenêtre de cache.

Chaque échec a la même apparence

Un en-tête manquant, une clé malformée, une clé inconnue, un mauvais secret, une clé révoquée et un utilisateur désactivé renvoient tous le même 401 avec le même message. C'est délibéré — cela signifie qu'une clé ne peut pas être sondée pour vérifier son existence.

L'enveloppe de réponse

Chaque réponse JSON a la même structure, ce qui permet à un client de se brancher sur un seul champ.

{ "success": true, "data": { … } }

{ "success": false, "error": { "message": "…" } }

Les points de terminaison de liste ajoutent un bloc pagination à côté de data, et data est un tableau simple plutôt qu'un objet :

{
  "success": true,
  "data": [ … ],
  "pagination": { "total": 42, "page": 1, "per_page": 20, "total_pages": 3 }
}

Deux points de terminaison s'écartent délibérément de l'enveloppe, car les y encapsuler serait inutile : la lecture d'un fichier shell renvoie les octets bruts en text/plain, et sa suppression renvoie un 204 sans corps.

Créer un article, puis le publier

curl -X POST https://your-host/api/v1/sites/blog/content \
  -H "X-API-Key: $FASTSITE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 2f1c9a7e-…" \
  -d '{
    "type": "post",
    "title": "Hello, world",
    "body_html": "<p>Shipped in a single call.</p>",
    "status": "published"
  }'

Écrire du contenu ne le publie pas. Les deux sont des opérations distinctes à dessein — un pipeline peut créer, réviser et corriger autant que nécessaire sans que rien n'atteigne les visiteurs. Lorsque le contenu est prêt, publiez le site :

curl -X POST https://your-host/api/v1/sites/blog/publish \
  -H "X-API-Key: $FASTSITE_KEY"
# → { "success": true, "data": { "job": "0f3c…" } }

Cela renvoie un 202 avec un identifiant de tâche ; la compilation s'exécute dans la file d'attente. Interrogez GET /api/v1/jobs/0f3c… pour connaître le résultat.

title est le seul champ obligatoire lors d'une création. type vaut post par défaut, status vaut draft par défaut, et le slug est dérivé du titre lorsque vous n'en envoyez pas. Si ce slug est déjà pris, le moteur ajoute un compteur et renvoie le slug qu'il a réellement utilisé — lisez-le toujours en retour plutôt que de le supposer.

Points de terminaison

Contenu

MéthodeCheminPortée
GET/sites/{site}/contentcontent:read
POST/sites/{site}/contentcontent:write
GET/sites/{site}/content/{id}content:read
PATCH/sites/{site}/content/{id}content:write
DELETE/sites/{site}/content/{id}content:write
GET/sites/{site}/content/trashcontent:read
POST/sites/{site}/content/{id}/restorecontent:write
DELETE/sites/{site}/content/{id}/purgecontent:write
POST/sites/{site}/content/{id}/fact-checkcontent:read

La suppression est une suppression temporaire — l'élément est déplacé dans la corbeille et peut être restauré. purge est l'opération irréversible.

Médias

MéthodeCheminPortée
GET/sites/{site}/mediamedia:read
POST/sites/{site}/mediamedia:write
DELETE/sites/{site}/media/{id}media:write
POST/sites/{site}/media/{id}/restoremedia:write
DELETE/sites/{site}/media/{id}/purgemedia:write

L'envoi accepte soit un formulaire multipart avec un champ nommé file, soit du JSON avec une url à récupérer. Pas les deux — si un fichier est présent, l'URL est ignorée.

curl -X POST https://your-host/api/v1/sites/blog/media \
  -H "X-API-Key: $FASTSITE_KEY" \
  -F file=@hero.jpg \
  -F alt="Sunrise over the harbour"

Le texte alternatif est obligatoire pour les images sauf si vous passez explicitement decorative=true. Ce n'est pas une préférence de style — un texte alternatif manquant fait échouer l'audit de compilation, donc l'API le refuse à l'entrée plutôt que de le laisser bloquer une publication ultérieure. Les imports par URL sont protégés contre les attaques SSRF : les plages d'adresses privées sont rejetées, les redirections sont revalidées et le téléchargement est limité en taille.

Filtrez la bibliothèque avec ?kind=image,video,file. Les images et les vidéos passent par le pipeline de variantes ; tout le reste est stocké en tant que fichier téléchargeable.

Shell

MéthodeCheminPortée
GET/sites/{site}/shell/filesshell:read
GET/sites/{site}/shell/file/{path}shell:read
PUT/sites/{site}/shell/file/{path}shell:write
DELETE/sites/{site}/shell/file/{path}shell:write
POST/sites/{site}/shell/snapshotshell:write
POST/sites/{site}/shell/restore/{rev}shell:write

Le corps est le fichier, pas du JSON. Il n'y a pas d'objet enveloppant ni de nom de champ — vous envoyez les octets et vous récupérez les octets.

curl -X PUT https://your-host/api/v1/sites/blog/shell/file/templates/base.html.twig \
  -H "X-API-Key: $FASTSITE_KEY" \
  --data-binary @base.html.twig

Le chemin peut contenir des barres obliques. Les écritures sont limitées au répertoire shell, plafonnées à 5 Mo, et restreintes à une liste blanche d'extensions (twig, css, js, json, woff2, svg, et les types d'images habituels). Contrairement au contenu, une écriture shell déclenche bien une recompilation — le design a changé, donc la sortie doit l'être aussi. Un enchaînement rapide de modifications est regroupé en une seule compilation plutôt qu'une par fichier.

Prenez un instantané avant une modification risquée. La restauration prend d'abord un instantané, donc une restauration est elle-même annulable.

Composants, redirections, scripts

MéthodeCheminPortée
PUT/sites/{site}/components/{handle}components:write
GET · POST/sites/{site}/redirectssites:read · sites:write
DELETE/sites/{site}/redirects/{id}sites:write
GET · POST/sites/{site}/scriptssites:read · sites:write
DELETE/sites/{site}/scripts/{id}sites:write

Un composant est {type: "html"|"react", source_code, css?, variables_schema?}. React compile au moment de l'envoi, donc un composant cassé génère un 422 sur votre requête plutôt qu'une page cassée ultérieurement.

Langues, publication, sites

MéthodeCheminPortée
POST/sites/{site}/languagestranslations:manage
DELETE/sites/{site}/languages/{locale}translations:manage
POST/sites/{site}/publishpublish
GET/jobs/{uuid}jobs:read
GET/sitessites:read
POST/sitesadmin
PATCH/sites/{site}sites:write
GET · POST/sites/{site}/webhookssites:read · sites:write
GET/sites/{site}/webhooks/{id}/deliveriessites:read
DELETE/sites/{site}/webhooks/{id}sites:write
GET/authoring-guidesites:read
GET/sites/{site}/authoring-guidesites:read

Récupérez d'abord le guide de rédaction

GET /api/v1/authoring-guide renvoie les règles lisibles par machine pour le HTML du corps, l'intégration d'images et le contrat shell. Passez un site pour obtenir ses paramètres régionaux et sa syntaxe d'intégration inclus. C'est la différence entre un contenu qui se compile et un contenu que l'audit rejette.

Portées

Les clés et les autorisations OAuth partagent un même vocabulaire de portées, ce qui permet à un point de terminaison et à un outil d'appliquer les accès de manière identique.

PortéeAccorde
sites:readVoir les sites et leurs paramètres
sites:writeModifier les paramètres du site, les redirections, les scripts, les webhooks
content:readLire les articles et les pages
content:writeCréer et modifier des articles et des pages
media:readConsulter la bibliothèque de médias
media:writeEnvoyer des images, des vidéos et des fichiers
components:writeCréer et mettre à jour des composants
shell:readLire les modèles et le thème
shell:writeModifier les modèles et le thème
translations:manageGérer les paramètres régionaux et les traductions
publishCompiler et publier
jobs:readLire l'état des compilations et des tâches
backup:read · backup:run · backup:restoreOpérations de sauvegarde
adminTout

Seul admin implique les autres portées. Il n'y a pas de hiérarchie lecture/écriture — content:write n'accorde pas content:read, donc demandez les deux si vous avez besoin des deux. Les clés sont également épinglées à une liste de sites, et une requête pour un site en dehors de celle-ci est rejetée avant d'atteindre un gestionnaire.

Idempotence

Envoyez un en-tête Idempotency-Key sur tout POST et le moteur enregistre le résultat contre celui-ci.

  • La même clé avec la même méthode, le même chemin et le même corps rejoue la réponse stockée, avec Idempotency-Replayed: true dessus.
  • La même clé avec un corps différent renvoie un 422 — c'est ainsi que vous découvrez que votre logique de nouvelle tentative a modifié la charge utile.
  • Une nouvelle tentative qui arrive pendant que la première est encore en cours reçoit un 409 et un Retry-After.
  • Si le gestionnaire lève une erreur, ou si la réponse est un 5xx ou un 429, la clé est libérée pour qu'une véritable nouvelle tentative puisse réussir.

Les autres méthodes ignorent entièrement l'en-tête.

Limites de débit

Deux limites à fenêtre fixe, toutes deux par minute : 60 requêtes par IP et 120 par clé API. Dépasser l'une ou l'autre renvoie un 429 avec un Retry-After. Le seuil par IP est celui que vous atteindrez généralement en premier, puisqu'un client unique correspond à une seule adresse.

Les corps de requêtes JSON sont limités à 4 Mio. Les envois de fichiers sont exemptés de cette vérification et bornés par les limites de médias par type à la place.

Codes de statut

CodeSignification
200 · 201 · 202 · 204Succès. 202 signifie mis en file d'attente — interrogez la tâche.
401Clé manquante, malformée, inconnue ou révoquée.
403Clé valide, mais la portée ou le site n'est pas autorisé.
404Route, site ou enregistrement introuvable.
409Doublon, ou requête idempotente toujours en cours.
413Corps au-dessus de la limite.
422Validation échouée — lisez error.message.
429Limite de débit atteinte.

Détails à connaître avant de développer contre cette API

  • Les horodatages sont stricts. scheduled_at et published_at doivent être au format YYYY-MM-DDTHH:MM:SSZ — UTC, T majuscule, Z final. Les décalages comme +02:00 sont rejetés plutôt que convertis.
  • Les valeurs de statut sont draft, scheduled, published et archived. La planification nécessite scheduled_at, et le planificateur le publie à l'heure venue.
  • La pagination utilise ?page= et ?per_page=, avec une valeur par défaut de 20 et un plafond de 100.
  • Le HTML du corps est assaini à chaque étape. Les styles en ligne sont supprimés, les scripts et les iframes sont retirés, et le résultat est renvoyé avec un tableau warnings. Les problèmes de contenu n'échouent jamais votre requête — ils sont signalés, normalisés et publiés.
  • Certaines choses ne sont pas disponibles sur REST. Les sauvegardes, les équipes et la gestion des clés API sont des opérations en ligne de commande et via le tableau de bord ; les sauvegardes et les restaurations sont également disponibles via MCP.

Ensuite : le serveur MCP pour la publication pilotée par des agents, ou les webhooks pour pousser du contenu depuis un système amont.