v1 · stable · pull model

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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Le 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

GET
https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1/tasks/pending?limit=10

Query params

ParamTypeDéfaut
limitint 1-5010

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

POST
https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1/tasks/{id}/ack

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

POST
https://tutlimtasnjabdfhpewu.functions.supabase.co/parmenion-api/v1/tasks/{id}/published

Body

ChampTypeRequisDescription
urlstringouiURL publique finale de l'article publié.
cms_post_idstring | numbernonID du post côté CMS (utile pour update/delete ultérieurs).
notesstringnonToute 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/{id}/failed
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_typeClés du payload
publish_posttitle, slug, html, excerpt, category, tags[], meta_title, meta_description, featured_image_url, internal_links[]
update_postcms_post_id, html, meta_title?, meta_description?, change_summary
create_seo_pagetitle, slug, html, meta_title, meta_description, schema_jsonld, page_type
add_internal_linkscms_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

CodeerrorCas
400invalid_bodyJSON malformé ou champ requis manquant (ex : `url` sur /published).
401missing_tokenAucun header Authorization ni x-parmenion-key.
401invalid_tokenToken inconnu, révoqué, ou cible désactivée.
404not_foundTask ID inconnu ou n'appartient pas au domaine du token.
500db_errorErreur 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.