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.