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.
Inhaltsverzeichnis
Schnellstart
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.
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.
Artikel abrufen
Empfange Webhooks, verifiziere die HMAC-Signatur, rufe den Artikel über die Pull API ab und bestätige die Veröffentlichung.
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)" |
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"
}
}
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. |
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:
- Lies den rohen Request-Body als Bytes, nicht als String
- Berechne
HMAC-SHA256(webhook_secret, raw_body) - Vergleiche das Ergebnis als
sha256=<hex_digest>mit dem Header-Wert - 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 |
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.
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/ |