API Parménion
Modèle pull : votre site interroge Parménion pour récupérer les tâches de contenu SEO planifiées (titre, brief, HTML prêt à publier), les pousse sur votre CMS interne, puis confirme la publication. Vous ne donnez aucun accès sortant à Crawlers — votre site reste en contrôle total.
Architecture pull
Contrairement au mode push (où Crawlers se connecte à votre CMS via une App Password ou clé REST), le mode pull inverse la relation : votre serveur appelle Crawlers, lit ses tâches, les exécute en local, puis renvoie le résultat. Idéal si votre infra interdit les accès sortants ou si vous avez un WAF/ModSecurity strict.
┌─────────────┐ 1. GET /tasks/pending ┌────────────────┐
│ Votre CMS / │ ─────────────────────────────────► │ Parménion │
│ Cron │ │ (Crawlers.fr) │
│ │ ◄──── { tasks: [{ id, payload }] } │ │
│ │ │ │
│ │ 2. POST /tasks/{id}/ack │ │
│ │ ──────────────────────────────────► │ │
│ │ 3. POST /tasks/{id}/published │ │
│ │ ──────────────────────────────────► │ │
└─────────────┘ └────────────────┘Cadence recommandée : 1 poll toutes les 5 minutes (cron) ou toutes les 60 secondes (worker continu). Aucune limite côté Crawlers tant que vous restez sous 60 requêtes/minute.
1. Quickstart
Quatre appels suffisent : (1) récupérer votre token Parménion, (2) GET les tâches, (3) publier en local, (4) POST le résultat.
# 1. Token fourni par Crawlers (admin → Mes Sites → Parménion → Générer un token)
export PRM_TOKEN="prm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 2. Récupérer les tâches en attente
curl -s "https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1/tasks/pending?limit=5" \
-H "Authorization: Bearer $PRM_TOKEN"
# 3. Pour chaque tâche : ack avant traitement
curl -s -X POST "https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1/tasks/<TASK_ID>/ack" \
-H "Authorization: Bearer $PRM_TOKEN"
# 4. Confirmer publication (ou échec)
curl -s -X POST "https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1/tasks/<TASK_ID>/published" \
-H "Authorization: Bearer $PRM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://votresite.com/blog/nouvel-article","cms_post_id":1234}'2. Authentification
Chaque site (domaine) a son propre token au format prm_live_ + 40 caractères hex. Deux entêtes acceptés (équivalents) :
# Option A (recommandée)
Authorization: Bearer prm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Option B
x-parmenion-key: prm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxLe token est lié à un domaine unique. Il ne donne accès qu'aux tâches de ce domaine. Régénération via le dashboard Crawlers : Console → Mes Sites → Parménion → Rotate token. La rotation invalide instantanément l'ancien token.
Stockage : seul le hash SHA-256 du token est gardé côté Crawlers. Si vous le perdez, il faut le régénérer.
3. Lister les tâches en attente
Query params
| Param | Type | Défaut |
|---|---|---|
| limit | int 1-50 | 10 |
Réponse 200
{
"target": {
"id": "f1a2b3c4-...",
"domain": "votresite.com"
},
"count": 2,
"tasks": [
{
"id": "8f3a9b2c-1234-5678-90ab-cdef01234567",
"action_type": "publish_post",
"goal": "Publier l'article pilier sur 'maintenance toiture'",
"goal_type": "content_creation",
"phase": "execute",
"cycle": 12,
"created_at": "2026-05-26T08:14:02Z",
"payload": {
"title": "Maintenance toiture : guide complet 2026",
"slug": "maintenance-toiture-guide-2026",
"html": "<h2>Pourquoi entretenir...</h2><p>...</p>",
"excerpt": "Tout savoir sur l'entretien de votre toiture.",
"category": "Conseils",
"tags": ["toiture", "entretien", "maintenance"],
"meta_title": "Maintenance toiture 2026 — Guide complet",
"meta_description": "Guide expert pour entretenir...",
"featured_image_url": "https://...",
"internal_links": [{"anchor": "couverture zinc", "url": "/services/zinc"}]
}
}
]
}Les tâches sont renvoyées en FIFO (plus anciennes d'abord). Une tâche reste planned tant qu'aucun ack n'est reçu — elle apparaîtra donc à chaque poll. Faites un ack dès réception pour éviter les doublons si plusieurs workers polls en parallèle.
4. Accuser réception
Marque la tâche en in_progress et enregistre execution_started_at. À appeler immédiatement après le GET pour verrouiller la tâche.
POST https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1/tasks/8f3a9b2c-.../ack
Authorization: Bearer prm_live_...
→ 200 OK
{ "ok": true, "id": "8f3a9b2c-...", "status": "in_progress" }5. Confirmer la publication
Body
| Champ | Type | Requis | Description |
|---|---|---|---|
| url | string | oui | URL publique finale de l'article publié. |
| cms_post_id | string | number | non | ID du post côté CMS (utile pour update/delete ultérieurs). |
| notes | string | non | Toute info libre (ex : "publié en brouillon, à valider"). |
POST https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1/tasks/8f3a9b2c-.../published
Authorization: Bearer prm_live_...
Content-Type: application/json
{
"url": "https://votresite.com/blog/maintenance-toiture-guide-2026",
"cms_post_id": 1234
}
→ 200 OK
{
"ok": true,
"id": "8f3a9b2c-...",
"status": "completed",
"published_url": "https://votresite.com/..."
}Parménion utilise l'URL retournée pour vérifier l'indexation, mesurer l'impact GSC/GA4 à J+30 et recalibrer son scoring d'urgence. Mettez la vraie URL publique, pas l'URL admin.
6. Signaler un échec
POST https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1/tasks/8f3a9b2c-.../failed
Authorization: Bearer prm_live_...
Content-Type: application/json
{
"error_message": "WordPress REST returned 403 — App Password expired",
"error_category": "auth_failed"
}
→ 200 OK
{ "ok": true, "id": "8f3a9b2c-...", "status": "error" }Catégories suggérées : auth_failed, cms_unavailable, validation_failed, duplicate_slug, client_failure (défaut).
Trois échecs successifs sur le même domaine déclenchent le backlog guard côté Parménion : les cycles suivants sont mis en pause jusqu'à intervention manuelle dans la console.
7. Structure du payload
Le champ payload est un objet JSON libre dont les clés varient selon action_type. Voici la convention pour les actions de contenu :
| action_type | Clés du payload |
|---|---|
| publish_post | title, slug, html, excerpt, category, tags[], meta_title, meta_description, featured_image_url, internal_links[] |
| update_post | cms_post_id, html, meta_title?, meta_description?, change_summary |
| create_seo_page | title, slug, html, meta_title, meta_description, schema_jsonld, page_type |
| add_internal_links | cms_post_id, links[]: { anchor, target_url, insert_after_paragraph } |
Le HTML fourni est déjà sanitisé (DOMPurify côté Crawlers), prêt à insérer dans le corps de l'article sans transformation. Conservez la structure des<h2>/<h3> : elle conditionne le scoring SEO.
8. Codes d'erreur
| Code | error | Cas |
|---|---|---|
| 400 | invalid_body | JSON malformé ou champ requis manquant (ex : `url` sur /published). |
| 401 | missing_token | Aucun header Authorization ni x-parmenion-key. |
| 401 | invalid_token | Token inconnu, révoqué, ou cible désactivée. |
| 404 | not_found | Task ID inconnu ou n'appartient pas au domaine du token. |
| 500 | db_error | Erreur interne — réessayer ; si persistant, contacter le support. |
9. Exemples
Node.js (zéro dépendance)
// parmenion-pull.ts — à exécuter en cron toutes les 5 min
const TOKEN = process.env.PRM_TOKEN!;
const BASE = "https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1";
const h = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
async function publishToMyCms(payload: any): Promise<{ url: string; id: number }> {
// → votre logique CMS interne ici
return { url: "https://votresite.com/blog/" + payload.slug, id: Date.now() };
}
const { tasks } = await fetch(`${BASE}/tasks/pending?limit=5`, { headers: h }).then((r) => r.json());
for (const task of tasks) {
await fetch(`${BASE}/tasks/${task.id}/ack`, { method: "POST", headers: h });
try {
const { url, id } = await publishToMyCms(task.payload);
await fetch(`${BASE}/tasks/${task.id}/published`, {
method: "POST", headers: h,
body: JSON.stringify({ url, cms_post_id: id }),
});
} catch (e: any) {
await fetch(`${BASE}/tasks/${task.id}/failed`, {
method: "POST", headers: h,
body: JSON.stringify({ error_message: e.message, error_category: "client_failure" }),
});
}
}PHP (compatible Laravel / WordPress)
<?php
$token = getenv('PRM_TOKEN');
$base = 'https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1';
$opts = ['http' => ['header' => "Authorization: Bearer $token\r\nContent-Type: application/json\r\n"]];
$res = json_decode(file_get_contents("$base/tasks/pending?limit=5", false, stream_context_create($opts)), true);
foreach ($res['tasks'] as $task) {
// 1. ack
file_get_contents("$base/tasks/{$task['id']}/ack", false, stream_context_create([
'http' => ['method' => 'POST'] + $opts['http']
]));
// 2. publier localement (wp_insert_post, Eloquent, etc.)
$url = my_cms_publish($task['payload']); // ← votre fonction
// 3. confirmer
$body = json_encode(['url' => $url]);
file_get_contents("$base/tasks/{$task['id']}/published", false, stream_context_create([
'http' => ['method' => 'POST', 'content' => $body] + $opts['http']
]));
}Python (requests)
import os, requests
TOKEN = os.environ["PRM_TOKEN"]
BASE = "https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1"
H = {"Authorization": f"Bearer {TOKEN}"}
tasks = requests.get(f"{BASE}/tasks/pending?limit=5", headers=H).json()["tasks"]
for t in tasks:
requests.post(f"{BASE}/tasks/{t['id']}/ack", headers=H)
try:
url, post_id = my_cms_publish(t["payload"]) # ← votre fonction
requests.post(f"{BASE}/tasks/{t['id']}/published",
headers=H, json={"url": url, "cms_post_id": post_id})
except Exception as e:
requests.post(f"{BASE}/tasks/{t['id']}/failed",
headers=H, json={"error_message": str(e), "error_category": "client_failure"})10. Cron WordPress prêt à coller
Coller dans wp-content/mu-plugins/parmenion-pull.php — aucun plugin externe requis. Tourne toutes les 5 min via WP-Cron.
<?php
/**
* Plugin Name: Parménion Pull (Crawlers)
* Description: Poll Parménion toutes les 5 min et publie les articles planifiés.
*/
define('PRM_TOKEN', 'prm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
define('PRM_BASE', 'https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1');
add_filter('cron_schedules', fn($s) => $s + ['every_5min' => ['interval' => 300, 'display' => '5 min']]);
register_activation_hook(__FILE__, fn() =>
wp_next_scheduled('prm_pull_cron') || wp_schedule_event(time(), 'every_5min', 'prm_pull_cron'));
add_action('prm_pull_cron', function () {
$h = ['headers' => ['Authorization' => 'Bearer ' . PRM_TOKEN, 'Content-Type' => 'application/json']];
$r = wp_remote_get(PRM_BASE . '/tasks/pending?limit=5', $h);
if (is_wp_error($r)) return;
$tasks = json_decode(wp_remote_retrieve_body($r), true)['tasks'] ?? [];
foreach ($tasks as $t) {
wp_remote_post(PRM_BASE . "/tasks/{$t['id']}/ack", $h);
$p = $t['payload'];
$post_id = wp_insert_post([
'post_title' => $p['title'],
'post_name' => $p['slug'],
'post_content' => $p['html'],
'post_excerpt' => $p['excerpt'] ?? '',
'post_status' => 'publish',
'post_type' => 'post',
'tags_input' => $p['tags'] ?? [],
], true);
if (is_wp_error($post_id)) {
wp_remote_post(PRM_BASE . "/tasks/{$t['id']}/failed", $h + [
'body' => json_encode(['error_message' => $post_id->get_error_message(), 'error_category' => 'cms_unavailable']),
]);
continue;
}
if (!empty($p['meta_title'])) update_post_meta($post_id, '_yoast_wpseo_title', $p['meta_title']);
if (!empty($p['meta_description'])) update_post_meta($post_id, '_yoast_wpseo_metadesc', $p['meta_description']);
wp_remote_post(PRM_BASE . "/tasks/{$t['id']}/published", $h + [
'body' => json_encode(['url' => get_permalink($post_id), 'cms_post_id' => $post_id]),
]);
}
});Besoin d'un autre modèle ? Crawlers supporte aussi le mode push (Parménion se connecte directement à votre CMS via App Password WordPress, REST custom, Webflow, Shopify…) — voir la liste complète des intégrations.