Content Autopilot API

Integriere den Content Autopilot in dein CMS mit der Pull API und Webhooks. Diese Seite enthält alles, was du brauchst, um eine vollständige Integration zu bauen.

Schnellstart

1

API-Key erstellen

Kontoweit: Einstellungen > API-Key & MCP, Key beginnt mit lc_. Projekt-gebunden: im Projekt unter dem Werkzeug Content Autopilot > CMS-Zugänge > "Contentpilot-API-Key (Pull)", Key beginnt mit cp_. Beide Formate sind im Klartext nur einmal sichtbar, sofort kopieren.

2

Webhook konfigurieren

Trage im selben CMS-Zugang deine Webhook-URL und ein eigenes, geteiltes HMAC-Secret ein (wird nicht automatisch erzeugt, du legst es selbst fest und hinterlegst es identisch auf beiden Seiten). Wähle die Events, die du empfangen willst.

3

Artikel abrufen

Empfange Webhooks, verifiziere die HMAC-Signatur, rufe den Artikel über die Pull API ab und bestätige die Veröffentlichung.

Basis-URL für alle Endpunkte auf dieser Seite: https://app.visibly-ai.com. Die Pfade /api/v1/articles... bleiben exakt wie unten dokumentiert.

Integrationsablauf

Die Integration folgt einem Push- und Pull-Muster. Webhooks informieren dein System über neue Artikel, die Pull API liefert den vollständigen Inhalt.

Schritt 1: Artikel wird im Visibly Content Autopilot freigegeben
   |
   v
Schritt 2: Webhook feuert an deinen Endpunkt
   POST https://your-cms.com/webhooks/visibly
   Headers:
     Content-Type: application/json
     X-Webhook-Signature: sha256=<hmac_digest>
     X-Webhook-Event: article.approved
     User-Agent: VisiblyAI-Webhook/1.0
   Body:
     {
       "event": "article.approved",
       "article_id": 42,
       "title": "SEO Guide 2026",
       "slug": "seo-guide-2026",
       "project_id": 5,
       "scheduled_date": "2026-03-01T09:00:00",
       "pull_url": "https://app.visibly-ai.com/api/v1/articles/42",
       "timestamp": "2026-02-20T10:00:00Z"
     }
   |
   v
Schritt 3: Dein Handler verifiziert die HMAC-SHA256-Signatur
   Vergleiche X-Webhook-Signature mit HMAC(secret, raw_body)
   Bei Abweichung mit 401 ablehnen
   |
   v
Schritt 4: Dein Handler ruft die Pull API für den vollständigen Inhalt auf
   GET /api/v1/articles/42?include_markdown=true
   Authorization: Bearer cp_your_project_key
   |
   v
Schritt 5: Dein CMS speichert/veröffentlicht den Artikel
   scheduled_date für die Planung, content_html für das Rendering
   |
   v
Schritt 6: Dein Handler bestätigt die Veröffentlichung
   POST /api/v1/articles/42/confirm
   Authorization: Bearer cp_your_project_key
   Body: {"published_url": "https://your-cms.com/blog/seo-guide-2026"}
   |
   v
Fertig. Der Artikel-Status wechselt in Visibly auf "published".

Authentifizierung

Alle API-Anfragen erfordern einen Bearer-Token im Authorization-Header:

Authorization: Bearer cp_your_project_key

Zwei Key-Arten am selben Gate

Präfix Scope Erstellen
lc_ Kontoweit. Zugriff auf alle eigenen Artikel, projektübergreifend. Ein client-seitiger project_id-Filter wirkt hier normal. Einstellungen > API-Key & MCP
cp_ Projekt-gebunden. Sieht ausschliesslich dieses eine Projekt, der Scope wird in jeder Abfrage hart erzwungen. Ein mitgeschickter project_id-Filter wird vom Scope überschrieben. Je Projekt genau ein aktiver Key, Erzeugen rotiert (alter Key wird inaktiv). Im Projekt unter Content Autopilot > CMS-Zugänge > "Contentpilot-API-Key (Pull)"
Fehlender, unbekannter, falsch formatierter oder deaktivierter API-Key wird einheitlich mit HTTP 401 abgelehnt, es gibt keinen Rückschluss darauf, ob ein Key jemals existiert hat. Jeder Key ist an einen Nutzer bzw. an ein Projekt gebunden, du kannst ausschliesslich auf deine eigenen Artikel zugreifen.

Artikel-Lebenszyklus

Jeder Artikel durchläuft folgende Status-Phasen:

Status Beschreibung Webhook
queued Artikel steht in der Warteschlange zur Generierung -
generating Artikel wird gerade von der KI generiert -
draft Entwurf liegt vor, kann bearbeitet werden -
approved Zur Veröffentlichung freigegeben article.approved
published Wurde extern veröffentlicht (via confirm-Endpunkt) article.published
rejected Wurde abgelehnt -
archived Wurde manuell archiviert, kein aktiver Status mehr, kein Ersatz für "abgelehnt" -
failed Generierung fehlgeschlagen article.failed
article.failed ist als Event-Typ reserviert, wird von visibly aktuell aber noch nicht ausgelöst, weder aus dem Generierungs-Workflow noch als wählbare Checkbox in den CMS-Zugangs-Einstellungen (dort stehen nur article.approved und article.published zur Auswahl). Für Fehler-Monitoring filtere die Pull API stattdessen nach status=failed.

Der typische Flow für CMS-Integrationen: Du filterst die Pull API nach status=approved oder empfängst den article.approved-Webhook, holst den Artikel ab und bestätigst dann mit dem confirm-Endpunkt.

Pull API Endpunkte

Basis-URL: https://app.visibly-ai.com

GET/api/v1/articles

Listet Artikel mit optionalen Filtern auf. Ergebnisse sind nach Erstellungsdatum absteigend sortiert.

Parameter Typ Standard Beschreibung
status string (alle) Filter nach Status: queued, generating, draft, approved, rejected, archived, published, failed
project_id int (alle) Filter nach Projekt-ID. Bei einem projekt-gebundenen cp_-Key wird dieser Parameter ignoriert, der Scope des Keys gewinnt immer.
since string (kein) Nur Artikel ab diesem Datum (Format: YYYY-MM-DD)
limit int 20 Max. Ergebnisse pro Seite. Werte unter 1 werden auf 1, Werte über 100 werden auf 100 begrenzt (kein Fehler).
offset int 0 Pagination Offset
# Alle freigegebenen Artikel des Projekts 5 auflisten
curl -H "Authorization: Bearer cp_your_project_key" \
  "https://app.visibly-ai.com/api/v1/articles?status=approved&project_id=5&limit=10"
// Antwort (200 OK)
{
  "articles": [
    {
      "id": 42,
      "title": "SEO Guide 2026",
      "slug": "seo-guide-2026",
      "status": "approved",
      "word_count": 1850,
      "seo_score": 85,
      "meta_description": "Learn how to optimize your website for search engines in 2026.",
      "project_id": 5,
      "plan_id": 12,
      "recommended_page_type": "guide",
      "url_prefix": "/ratgeber/seo/",
      "published_url": "",
      "scheduled_date": "2026-03-01T09:00:00",
      "created_at": "2026-02-20T10:00:00",
      "updated_at": "2026-02-20T12:00:00"
    }
  ],
  "total": 42,
  "limit": 10,
  "offset": 0
}

GET/api/v1/articles/{id}

Ruft einen einzelnen Artikel mit vollständigem Inhalt ab. Gibt HTML-Inhalt immer zurück. Markdown-Inhalt nur auf Anfrage.

Parameter Typ Standard Beschreibung
id (Pfad) int erforderlich Artikel-ID
include_markdown bool false Auch Markdown-Quelltext im Feld content_markdown zurückgeben
# Artikel 42 mit vollständigem Markdown-Inhalt abrufen
curl -H "Authorization: Bearer cp_your_project_key" \
  "https://app.visibly-ai.com/api/v1/articles/42?include_markdown=true"
// Antwort (200 OK)
{
  "article": {
    "id": 42,
    "title": "SEO Guide 2026",
    "slug": "seo-guide-2026",
    "status": "approved",
    "word_count": 1850,
    "seo_score": 85,
    "meta_description": "Learn how to optimize your website for search engines in 2026.",
    "content_html": "<h1>SEO Guide 2026</h1>\n<p>Search engine optimization is...</p>",
    "content_markdown": "# SEO Guide 2026\n\nSearch engine optimization is...",
    "keywords": ["seo", "search engine optimization", "keyword research"],
    "project_id": 5,
    "plan_id": 12,
    "recommended_page_type": "guide",
    "url_prefix": "/ratgeber/seo/",
    "published_url": "",
    "scheduled_date": "2026-03-01T09:00:00",
    "cms_published_at": null,
    "created_at": "2026-02-20T10:00:00",
    "updated_at": "2026-02-20T12:00:00"
  }
}
Das Feld content_markdown wird nur zurückgegeben, wenn include_markdown=true gesetzt ist. Ohne diesen Parameter fehlt es in der Antwort, um die Antwortgröße zu reduzieren. plan_id, recommended_page_type und url_prefix sind Routing-Signale für dein CMS, etwa für eigene Pfad-Präfixe. Vollständige Beschreibung siehe Feld-Referenz weiter unten.

POST/api/v1/articles/{id}/confirm

Bestätigt, dass der Artikel extern veröffentlicht wurde. Dieser Endpunkt ist idempotent: Ein erneuter Aufruf auf einen bereits veröffentlichten Artikel gibt Erfolg zurück.

Feld Typ Pflicht Beschreibung
id (Pfad) int Ja Artikel-ID
published_url (Body) string Nein Die öffentliche URL des veröffentlichten Artikels. Muss ein erreichbares http(s)-URL mit öffentlichem Host sein, localhost und interne Hosts werden abgelehnt.
# Veröffentlichung mit der Live-URL bestätigen
curl -X POST -H "Authorization: Bearer cp_your_project_key" \
  -H "Content-Type: application/json" \
  -d '{"published_url": "https://myblog.com/seo-guide-2026"}' \
  "https://app.visibly-ai.com/api/v1/articles/42/confirm"
// Antwort (200 OK) - erste Bestätigung
{
  "success": true,
  "article_id": 42,
  "message": null
}

// Antwort (200 OK) - bereits veröffentlicht (idempotent)
{
  "success": true,
  "article_id": 42,
  "message": "Article already confirmed as published"
}

Feld-Referenz

Vollständige Auflistung aller Felder in API-Antworten.

Artikel-Felder (Liste und Einzelabruf)

Feld Typ Nullable Beschreibung
id int Nein Eindeutige Artikel-ID
title string Nein Artikeltitel
slug string Nein URL-freundlicher Slug (z.B. "seo-guide-2026")
status string Nein Aktueller Status (siehe Artikel-Lebenszyklus)
word_count int Nein Wortanzahl des Artikels (0 wenn nicht generiert)
seo_score int Nein SEO-Optimierungsscore von 0-100 (0 wenn nicht gemessen)
meta_description string Nein SEO Meta-Description (max. 160 Zeichen)
project_id int Ja ID des zugehörigen Projekts
plan_id int Ja ID des zugehörigen Content-Plans (Cluster), falls der Artikel aus einer Contentstrategie stammt. null wenn ohne Plan erzeugt.
recommended_page_type string Nein Empfohlener Seitentyp fürs Ziel-CMS, z.B. guide, blog, product, collection, landing_page, local_service. Leerer String wenn nicht gesetzt.
url_prefix string Ja Pfad-Präfix des Clusters, z.B. "/ratgeber/seo/". Bauplan fürs CMS, das die endgültige URL selbst baut. null = kein Cluster-Pfad gesetzt.
published_url string Nein Öffentliche URL nach Veröffentlichung (leer wenn nicht veröffentlicht)
scheduled_date string Ja Geplantes Veröffentlichungsdatum im ISO 8601 Format (z.B. "2026-03-01T09:00:00"). null = kein Datum gesetzt.
created_at string Nein Erstellungsdatum im ISO 8601 Format
updated_at string Nein Letztes Update im ISO 8601 Format

Zusätzliche Felder (nur Einzelabruf GET /articles/{id})

Feld Typ Nullable Beschreibung
content_html string Nein Vollständiger Artikelinhalt als HTML. Immer enthalten.
content_markdown string Nein Artikelinhalt als Markdown-Quelltext. Nur enthalten wenn include_markdown=true.
keywords array Nein Liste der Ziel-Keywords als String-Array, z.B. ["seo", "keyword research"]
cms_published_at string Ja Zeitpunkt der CMS-Veröffentlichung im ISO 8601 Format. null wenn noch nicht veröffentlicht.

Pagination-Felder (nur Liste GET /articles)

Feld Typ Beschreibung
total int Gesamtanzahl der Artikel (ohne limit/offset)
limit int Angewandtes Limit
offset int Angewandter Offset

Webhook-Events

Webhooks werden bei Statusänderungen ausgelöst. Du konfigurierst sie pro CMS-Zugang im Projekt unter dem Werkzeug Content Autopilot > CMS-Zugänge. Dort trägst du deine Webhook-URL und ein selbst gewähltes, geteiltes HMAC-Secret ein (kein Auto-generierter Wert, du hinterlegst dasselbe Secret auf beiden Seiten) und wählst die Events aus, die du empfangen willst.

Event Ausgelöst wenn
article.approved Artikel wurde genehmigt. Dies ist der primäre Event für CMS-Integrationen: Artikel ist bereit zur Veröffentlichung. In den CMS-Zugangs-Einstellungen als Checkbox wählbar.
article.published Artikel wurde über den confirm-Endpunkt als veröffentlicht bestätigt. In den CMS-Zugangs-Einstellungen als Checkbox wählbar.
article.failed Reserviert für Artikelgenerierung fehlgeschlagen. Wird aktuell noch nicht ausgelöst und ist auch nicht als Checkbox wählbar, siehe Hinweis im Abschnitt Artikel-Lebenszyklus.

Webhook-Payload

Jeder Webhook sendet folgendes JSON als POST-Body:

Feld Typ Beschreibung
event string Event-Typ (z.B. "article.approved")
article_id int ID des betroffenen Artikels
title string Artikeltitel
slug string URL-Slug des Artikels
project_id int ID des Projekts
scheduled_date string|null Geplantes Datum im ISO 8601 Format, oder null wenn nicht gesetzt
pull_url string Vollständige URL zum Abrufen des Artikels über die Pull API
timestamp string Zeitpunkt der Webhook-Auslösung im ISO 8601 UTC Format
{
  "event": "article.approved",
  "article_id": 42,
  "title": "SEO Guide 2026",
  "slug": "seo-guide-2026",
  "project_id": 5,
  "scheduled_date": "2026-03-01T09:00:00",
  "pull_url": "https://app.visibly-ai.com/api/v1/articles/42",
  "timestamp": "2026-02-20T10:00:00Z"
}

Webhook-HTTP-Headers

Header Beschreibung
Content-Type application/json
X-Webhook-Signature HMAC-SHA256 Signatur: sha256=<hex_digest>
X-Webhook-Event Event-Typ (z.B. article.approved)
User-Agent VisiblyAI-Webhook/1.0

Webhook-Zustellung

Details zur Webhook-Zustellung:

Eigenschaft Wert
HTTP-Methode POST
Timeout 10 Sekunden. Dein Endpunkt muss innerhalb von 10 Sekunden mit einem 2xx-Status antworten.
Wiederholungen 3 Versuche mit steigendem Abstand: 1s, 5s, 25s
Erfolg HTTP 200-299 gilt als erfolgreiche Zustellung
Redirects HTTP 3xx werden blockiert (SSRF-Schutz). Der Webhook gilt als fehlgeschlagen.
Erwartete Antwort Dein Endpunkt sollte JSON mit {"success": true} zurückgeben, aber jeder 2xx-Status wird akzeptiert.
Wichtig: Verarbeite den Webhook schnell oder asynchron. Wenn dein Endpunkt länger als 10 Sekunden braucht, wird der Webhook als fehlgeschlagen gewertet und erneut zugestellt. Empfehlung: Webhook-Daten sofort speichern und im Hintergrund verarbeiten.

HMAC-SHA256 Verifizierung

Jeder Webhook wird mit einer HMAC-SHA256-Signatur im Header X-Webhook-Signature gesendet. Verifiziere die Signatur, um sicherzustellen, dass der Webhook von Visibly stammt und nicht manipuliert wurde.

Algorithmus:

  1. Lies den rohen Request-Body als Bytes, nicht als String
  2. Berechne HMAC-SHA256(webhook_secret, raw_body)
  3. Vergleiche das Ergebnis als sha256=<hex_digest> mit dem Header-Wert
  4. Verwende einen timing-safe Vergleich, z.B. hmac.compare_digest, um Timing-Attacks zu verhindern

Python

import hmac, hashlib

def verify_signature(payload_bytes, secret, signature):
    """Verify HMAC-SHA256 webhook signature.

    Args:
        payload_bytes: Raw request body as bytes
        secret: Your webhook secret (string)
        signature: Value of X-Webhook-Signature header

    Returns:
        True if signature is valid
    """
    if not signature or not signature.startswith('sha256='):
        return False
    expected = f"sha256={hmac.new(secret.encode('utf-8'), payload_bytes, hashlib.sha256).hexdigest()}"
    return hmac.compare_digest(expected, signature)

# In deiner Flask-Route:
payload = request.get_data()  # rohe Bytes, NICHT request.json
sig = request.headers.get('X-Webhook-Signature', '')
if not verify_signature(payload, WEBHOOK_SECRET, sig):
    return jsonify({'error': 'Invalid signature'}), 401

Node.js

const crypto = require('crypto');

function verifySignature(payload, secret, signature) {
  if (!signature || !signature.startsWith('sha256=')) return false;
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret).update(payload).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(signature)
  );
}

// Express mit raw body parser:
// app.use('/webhooks', express.raw({ type: 'application/json' }))
app.post('/webhooks/visibly', (req, res) => {
  const sig = req.headers['x-webhook-signature'] || '';
  if (!verifySignature(req.body, process.env.WEBHOOK_SECRET, sig)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const data = JSON.parse(req.body);
  const { article_id, scheduled_date, pull_url } = data;

  // Vollständigen Artikel von pull_url abrufen...
  res.json({ success: true });
});

PHP

function verifySignature($payload, $secret, $signature) {
    if (!$signature || strpos($signature, 'sha256=') !== 0) return false;
    $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

$payload = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
if (!verifySignature($payload, $webhookSecret, $sig)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

$data = json_decode($payload, true);
$articleId = $data['article_id'];
$scheduledDate = $data['scheduled_date'] ?? null;

// Vollständigen Artikel über die Pull API abrufen...

Rate Limits

Richtwerte: Diese Grenzen beschreiben die vorgesehene Auslastung; eine harte Durchsetzung kann auf Infrastruktur-Ebene erfolgen und sich ändern.

Endpunkt Limit
GET/api/v1/articles 120 Anfragen / Minute
GET/api/v1/articles/{id} 60 Anfragen / Minute
POST/api/v1/articles/{id}/confirm 30 Anfragen / Minute

Bei Überschreitung erhältst du HTTP 429 mit einem Retry-After Header. Warte die angegebene Anzahl Sekunden, bevor du die Anfrage wiederholst.

Fehler-Antworten

Alle Fehler werden als JSON mit einem detail-Feld zurückgegeben:

{
  "detail": "Human-readable error description"
}
Code detail Ursache
400 Invalid status. Valid values: ... status-Parameter nicht in der erlaubten Liste
400 Invalid date format. Use YYYY-MM-DD since entspricht nicht dem Format YYYY-MM-DD
400 Invalid published_url published_url ist kein erreichbares http(s)-URL mit öffentlichem Host
401 Invalid API key Fehlender, unbekannter, falsch formatierter oder deaktivierter API-Key
404 Article not found Artikel nicht gefunden, gehört einem anderen Benutzer oder liegt außerhalb des Projekt-Scopes eines cp_-Keys
429 Rate limit exceeded Rate Limit überschritten
500 Internal server error Interner Serverfehler
Sicherheitshinweis: Sowohl "nicht gefunden" als auch "gehört einem anderen Benutzer" bzw. "außerhalb des Projekt-Scopes" geben 404 zurück. Dadurch wird verhindert, dass die Existenz von Artikeln anderer Benutzer erkennbar ist.

Vollständiges Integrationsbeispiel

Dieser Code zeigt eine produktionsreife Integration. Kopiere ihn als Ausgangspunkt für dein CMS.

Flask (Python)

import hmac, hashlib, json, logging, requests
from flask import Flask, request, jsonify

app = Flask(__name__)
logger = logging.getLogger(__name__)

# Konfiguration - durch deine echten Zugangsdaten ersetzen
WEBHOOK_SECRET = 'dein-webhook-secret-aus-den-cms-zugangs-einstellungen'
API_KEY = 'cp_dein_projekt_key'
BASE_URL = 'https://app.visibly-ai.com'


def verify_signature(payload_bytes, secret, signature):
    """Verify HMAC-SHA256 webhook signature."""
    if not signature or not signature.startswith('sha256='):
        return False
    expected = f"sha256={hmac.new(secret.encode('utf-8'), payload_bytes, hashlib.sha256).hexdigest()}"
    return hmac.compare_digest(expected, signature)


def fetch_article(article_id):
    """Fetch full article from Pull API."""
    url = f'{BASE_URL}/api/v1/articles/{article_id}?include_markdown=true'
    resp = requests.get(url, headers={'Authorization': f'Bearer {API_KEY}'}, timeout=30)
    resp.raise_for_status()
    return resp.json()['article']


def confirm_published(article_id, published_url):
    """Confirm article was published."""
    url = f'{BASE_URL}/api/v1/articles/{article_id}/confirm'
    resp = requests.post(url,
        headers={'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'},
        json={'published_url': published_url},
        timeout=30)
    return resp.status_code == 200


def save_to_cms(article, scheduled_date):
    """Save article to your CMS. Replace with your actual CMS logic."""
    post = {
        'title': article['title'],
        'slug': article['slug'],
        'body_html': article['content_html'],
        'body_markdown': article.get('content_markdown', ''),
        'meta_description': article['meta_description'],
        'keywords': article['keywords'],
        'publish_at': scheduled_date,  # null = sofort veröffentlichen
    }
    # db.session.add(Post(**post))
    # db.session.commit()
    logger.info(f"Saved article: {article['title']}")
    return f"https://myblog.com/blog/{article['slug']}"


@app.route('/webhooks/visibly', methods=['POST'])
def handle_webhook():
    # Schritt 1: HMAC-Signatur verifizieren
    payload = request.get_data()
    sig = request.headers.get('X-Webhook-Signature', '')
    if not verify_signature(payload, WEBHOOK_SECRET, sig):
        return jsonify({'error': 'Invalid signature'}), 401

    # Schritt 2: Webhook-Payload parsen
    data = json.loads(payload)
    event = data['event']
    article_id = data['article_id']
    scheduled_date = data.get('scheduled_date')  # ISO-String oder null

    # Schritt 3: Nur article.approved verarbeiten
    if event != 'article.approved':
        return jsonify({'success': True, 'message': 'Event ignored'})

    # Schritt 4: Vollständigen Artikel über die Pull API abrufen
    article = fetch_article(article_id)

    # Schritt 5: Im CMS speichern
    published_url = save_to_cms(article, scheduled_date)

    # Schritt 6: Veröffentlichung bestätigen
    confirm_published(article_id, published_url)

    return jsonify({'success': True, 'article_id': article_id})

Node.js (Express)

const express = require('express');
const crypto = require('crypto');
const axios = require('axios');

const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const API_KEY = process.env.VISIBLY_API_KEY;
const BASE_URL = 'https://app.visibly-ai.com';

// WICHTIG: raw body parser für die HMAC-Verifizierung
app.use('/webhooks', express.raw({ type: 'application/json' }));

function verifySignature(payload, secret, signature) {
  if (!signature || !signature.startsWith('sha256=')) return false;
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret).update(payload).digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  } catch {
    return false;
  }
}

app.post('/webhooks/visibly', async (req, res) => {
  // Schritt 1: Signatur verifizieren
  const sig = req.headers['x-webhook-signature'] || '';
  if (!verifySignature(req.body, WEBHOOK_SECRET, sig)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Schritt 2: Payload parsen
  const data = JSON.parse(req.body);
  const { event, article_id, scheduled_date } = data;

  if (event !== 'article.approved') {
    return res.json({ success: true, message: 'Event ignored' });
  }

  // Schritt 3: Vollständigen Artikel abrufen
  const { data: articleResp } = await axios.get(
    `${BASE_URL}/api/v1/articles/${article_id}?include_markdown=true`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );
  const article = articleResp.article;

  // Schritt 4: Im eigenen CMS speichern
  const publishedUrl = await saveToCms(article, scheduled_date);

  // Schritt 5: Veröffentlichung bestätigen
  await axios.post(
    `${BASE_URL}/api/v1/articles/${article_id}/confirm`,
    { published_url: publishedUrl },
    { headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }
  );

  res.json({ success: true, article_id });
});

app.listen(3000);

Python SDK (ContentPilot Integration)

Wir stellen ein fertiges Python-Modul bereit, das du direkt in deine Flask-Anwendung integrieren kannst. Es enthält einen Pull-API-Client, HMAC-Verifizierung und einen Flask-Blueprint als Webhook-Empfänger.

Wichtig: Der Standardwert von base_url in diesem Paket zeigt auf eine andere Installation. Setze base_url immer explizit auf https://app.visibly-ai.com, wenn du mit Visibly AI arbeitest, sowohl in configure_visibly(...) als auch in VisiblyClient(...).

Verwendung

# pip install ai-content-autopilot
from ai_content_autopilot import (
    contentpilot_webhook_bp,
    configure_visibly,
    VisiblyClient,
    verify_webhook_signature,
)

# --- Option A: Vollständiger Blueprint (Webhook-Empfänger + Auto-Fetch) ---

def my_article_handler(article):
    """Called when a webhook delivers an article.

    Args:
        article: dict with all article fields + webhook metadata:
            - id, title, slug, content_html, content_markdown
            - keywords, meta_description, seo_score, word_count
            - scheduled_date, cms_published_at
            - _webhook_event (e.g. "article.approved")
            - _webhook_timestamp
            - _scheduled_date (from webhook payload)

    Returns:
        True if successfully processed
    """
    # In deiner Datenbank, CMS oder Dateisystem speichern
    db.session.add(Post(
        title=article['title'],
        body=article['content_html'],
        publish_at=article.get('_scheduled_date'),
    ))
    db.session.commit()
    return True

configure_visibly(
    webhook_secret='dein-webhook-secret',
    api_key='cp_dein_projekt_key',
    base_url='https://app.visibly-ai.com',
    on_article_received=my_article_handler,
)
app.register_blueprint(contentpilot_webhook_bp)
# POST /webhooks/visibly erledigt danach automatisch:
# 1. HMAC-Signatur verifizieren
# 2. Vollständigen Artikel über die Pull API abrufen
# 3. my_article_handler(article) aufrufen
# 4. {"success": true} oder einen Fehler zurückgeben


# --- Option B: Eigenständiger Pull-API-Client ---

client = VisiblyClient(
    api_key='cp_dein_projekt_key',
    base_url='https://app.visibly-ai.com',
    timeout=30,
)

# Freigegebene Artikel auflisten
articles = client.list_articles(status='approved', project_id=5, limit=20)
for a in articles:
    print(a['id'], a['title'], a['scheduled_date'])

# Einzelnen Artikel mit vollständigem Inhalt abrufen
article = client.fetch_article(42, include_markdown=True)
print(article['content_html'])
print(article['keywords'])

# Veröffentlichung bestätigen
success = client.confirm_published(42, 'https://myblog.com/seo-guide-2026')


# --- Option C: Nur HMAC-Verifizierung ---

payload_bytes = request.get_data()
signature = request.headers.get('X-Webhook-Signature', '')
is_valid = verify_webhook_signature(payload_bytes, 'dein-secret', signature)

API-Referenz

Funktion / Klasse Beschreibung
configure_visibly(webhook_secret, api_key, base_url, on_article_received) Konfiguriert den Blueprint mit Zugangsdaten und Callback-Funktion
verify_webhook_signature(payload_bytes, secret, signature_header) Verifiziert HMAC-SHA256 Signatur. Gibt True/False zurück.
VisiblyClient(api_key, base_url, timeout) Pull-API-Client für Artikel abrufen, auflisten und Veröffentlichung bestätigen
client.fetch_article(article_id, include_markdown) Gibt Artikel-Dict zurück oder None bei Fehler
client.list_articles(status, project_id, limit, offset) Gibt Liste von Artikel-Dicts zurück
client.confirm_published(article_id, published_url) Bestätigt Veröffentlichung. Gibt True/False zurück.
contentpilot_webhook_bp Flask Blueprint. Registriere mit app.register_blueprint(). Endpunkt: POST /webhooks/visibly
default_flask_blog_handler(article) Standard-Handler: Speichert Artikel als JSON in blog_translations/html/