Go to Account > API Keys and create a new API key. The key starts with sk_live_. API key generated. Copy it now - it will only be shown once.
2
Configure Webhook
Go to a project > CMS Settings. Enter your webhook URL and copy the automatically generated secret. Select the events you want to receive.
3
Publish articles
Receive webhooks, verify the HMAC signature, fetch the article via the Pull API and confirm publication.
Integration Flow
The integration follows a push + pull pattern. Webhooks notify your system of new articles, the Pull API delivers the full content.
Step 1: Article approved in Visibly Content Autopilot
|
v
Step 2: Webhook fires to your endpoint
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://www.antonioblago.com/content-autopilot/api/v1/articles/42",
"timestamp": "2026-02-20T10:00:00Z"
}
|
v
Step 3: Your handler verifies HMAC-SHA256 signature
Compare X-Webhook-Signature against HMAC(secret, raw_body)
Reject with 401 if invalid
|
v
Step 4: Your handler calls Pull API to fetch full content
GET /content-autopilot/api/v1/articles/42?include_markdown=true
Authorization: Bearer sk_live_abc123
|
v
Step 5: Your CMS saves/publishes the article
Use scheduled_date for scheduling, content_html for rendering
|
v
Step 6: Your handler confirms publication
POST /content-autopilot/api/v1/articles/42/confirm
Authorization: Bearer sk_live_abc123
Body: {"published_url": "https://your-cms.com/blog/seo-guide-2026"}
|
v
Done. Article status changes to "published" in Visibly.
Authentication
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer sk_live_abc123def456
You manage your API keys under /account/api-keys.
Inactive or deleted API keys are immediately rejected with HTTP 401. Each API key is tied to a user. You can only access your own articles.
Article Lifecycle
Every article passes through the following status phases:
Status
Description
Webhook
queued
Article queued for generation.
-
generating
Article is being regenerated...
-
draft
Draft is available, can be edited
-
approved
Approved for publication
article.approved
published
Was published externally (via confirm endpoint)
article.published
rejected
Was rejected
-
failed
Status change failed.
article.failed
The typical flow for CMS integrations: you filter the Pull API by status=approved or you receive the article.approved webhook, fetch the article and then confirm with the confirm endpoint.
Pull API Endpoints
Base URL: https://www.antonioblago.com/content-autopilot
GET /api/v1/articles
Lists articles with optional filters. Results are sorted by creation date descending.
Parameter
Typ
Standard
Description
status
string
All
Filter by status: queued, generating, draft, approved, published, rejected, failed
project_id
int
All
Filter by project ID (positive integer)
since
string
(none)
Only articles from this date onwards (format: YYYY-MM-DD)
limit
int
20
Max. results per page (1-100, values above 100 are capped at 100)
offset
int
0
Pagination Offset
# List all approved articles for project 5
curl -H "Authorization: Bearer sk_live_abc123" \
"https://www.antonioblago.com/content-autopilot/api/v1/articles?status=approved&project_id=5&limit=10"
The field content_markdown is only returned when include_markdown=true is set. Without this parameter it is absent from the response to reduce response size.
POST /api/v1/articles/{id}/confirm
Confirms that the article was published externally. This endpoint is idempotent: calling it again on an already published article returns success.
Field
Typ
Required
Description
id (Path)
int
Yes
Article ID
published_url (Body)
string
No
The public URL of the published article. Must start with https://.
# Confirm article published with the live URL
curl -X POST -H "Authorization: Bearer sk_live_abc123" \
-H "Content-Type: application/json" \
-d '{"published_url": "https://myblog.com/seo-guide-2026"}' \
"https://www.antonioblago.com/content-autopilot/api/v1/articles/42/confirm"
Public URL after publication (empty if not published)
scheduled_date
string
Yes
Scheduled publication date in ISO 8601 format (e.g. "2026-03-01T09:00:00"). null = no date set.
created_at
string
No
Creation date in ISO 8601 format
updated_at
string
No
Last update in ISO 8601 format
Additional Fields (single fetch only GET /articles/{id})
Field
Typ
Nullable
Description
content_html
string
No
Full article content as HTML. Always included.
content_markdown
string
No
Article content as Markdown source. Only included when include_markdown=true.
keywords
array
No
List of target keywords as a string array, e.g. ["seo", "keyword research"]
cms_published_at
string
Yes
Time of CMS publication in ISO 8601 format. null if not yet published.
Pagination Fields (list only GET /articles)
Field
Typ
Description
total
int
Total number of articles (without limit/offset)
limit
int
Applied limit
offset
int
Applied offset
Webhook Events
Webhooks are triggered on status changes. You configure them per CMS connection under Project > CMS Settings. There you select the events and receive the webhook secret.
Event
Triggered when
article.approved
Article was approved. This is the primary event for CMS integrations: article is ready for publication.
article.published
Article was confirmed as published via the confirm endpoint.
article.failed
Article generation has failed. Useful for monitoring/alerting.
Webhook Payload
Every webhook sends the following JSON as the POST body:
Field
Typ
Description
event
string
Event type (e.g. "article.approved")
article_id
int
ID of the affected article
title
string
Articles in progress
slug
string
URL slug of the article
project_id
int
Error loading the project.
scheduled_date
string|null
Scheduled date in ISO 8601 format, or null if not set
pull_url
string
Full URL for fetching the article via the Pull API
10 seconds. Your endpoint must respond within 10 seconds with a 2xx status.
Retries
3 attempts with increasing delay: 1s, 5s, 25s
successful!
HTTP 200-299 counts as successful delivery
Redirects
HTTP 3xx are blocked (SSRF protection). The webhook is considered failed.
Expected Response
Your endpoint should return JSON with {"success": true} but any 2xx status is accepted.
Important: Process the webhook quickly or asynchronously. If your endpoint takes longer than 10 seconds, the webhook is considered failed and will be redelivered. Recommendation: save webhook data immediately and process in the background.
HMAC-SHA256 Verification
Every webhook is sent with an HMAC-SHA256 signature in the header X-Webhook-Signature sent. Verify the signature to ensure the webhook originates from Visibly and has not been tampered with.
Algorithm:
Read the raw request body as bytes (not as a string!)
Compute HMAC-SHA256(webhook_secret, raw_body)
Compare the result as sha256=<hex_digest> with the header value
Use a timing-safe comparison (e.g. hmac.compare_digest) to prevent timing attacks
Python
import hmac, hashlib
defverify_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 your Flask route:
payload = request.get_data() # raw bytes, NOT 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');
functionverifySignature(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 with 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;
// Fetch full article from pull_url...
res.json({ success: true });
});
We provide a ready-made Python module that you can integrate directly into your Flask application. It includes a Pull API client, HMAC verification and a Flask Blueprint as a webhook receiver.
Usage
# pip install ai-content-autopilotfrom ai_content_autopilot import (
contentpilot_webhook_bp,
configure_visibly,
VisiblyClient,
verify_webhook_signature,
)
# --- Option A: Full Blueprint (webhook receiver + auto-fetch) ---defmy_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
"""# Save to your database, CMS, filesystem, etc.
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='your-webhook-secret',
api_key='sk_live_your_api_key',
on_article_received=my_article_handler,
)
app.register_blueprint(contentpilot_webhook_bp)
# Now POST /webhooks/visibly will automatically:
# 1. Verify HMAC signature
# 2. Fetch full article via Pull API
# 3. Call my_article_handler(article)
# 4. Return {"success": true} or error# --- Option B: Standalone Pull API Client ---
client = VisiblyClient(
api_key='sk_live_your_api_key',
base_url='https://www.antonioblago.com/content-autopilot',
timeout=30,
)
# List approved articles
articles = client.list_articles(status='approved', project_id=5, limit=20)
for a in articles:
print(a['id'], a['title'], a['scheduled_date'])
# Fetch single article with full content
article = client.fetch_article(42, include_markdown=True)
print(article['content_html'])
print(article['keywords'])
# Confirm publication
success = client.confirm_published(42, 'https://myblog.com/seo-guide-2026')
# --- Option C: Just HMAC verification ---
payload_bytes = request.get_data()
signature = request.headers.get('X-Webhook-Signature', '')
is_valid = verify_webhook_signature(payload_bytes, 'your-secret', signature)