Catalog PyPI
PACKAGE EXPLORERView in registry

ai-content-autopilot

PyPI

Python SDK for the Visibly Content Autopilot API - Pull API client, HMAC webhook verification, and Flask Blueprint.

apiautopilotcmscontentflaskseowebhook
Versions collected1
Direct dependencies4From release 1.1.0
Last observed21 Oct, 10:16 UTC

README

Version 1.1.0 documentation

ai-content-autopilot

Python SDK for the Visibly Content Autopilot API.

Visibly Content Autopilot is an AI-powered content generation platform that creates SEO-optimized articles for your projects. It handles keyword research, content planning, and article generation — then delivers finished articles to your CMS via webhooks or a Pull API.

This SDK gives you everything you need to integrate Content Autopilot into any Python/Flask application:

  • Pull API Client — fetch, list, and confirm articles programmatically
  • Webhook Receiver — a ready-made Flask Blueprint that verifies HMAC signatures, fetches full article content, and calls your handler
  • HMAC-SHA256 Verification — standalone signature verification for custom webhook implementations

How It Works

1. Content Autopilot generates & approves an article
                    |
2. Webhook fires to your endpoint (POST /webhooks/visibly)
   with HMAC-SHA256 signature for security
                    |
3. Your app verifies the signature, then calls the Pull API
   to fetch the full article (HTML, Markdown, keywords, SEO score)
                    |
4. Your app saves/publishes the article in your CMS
                    |
5. Your app confirms publication back to Visibly
   (article status changes to "published")

Installation

pip install ai-content-autopilot

Requirements: Python 3.8+, Flask 2.3+, Requests 2.25+

Getting Started

1. Get your credentials

  • API Key: Go to Account > API Keys and create a new key (starts with sk_live_)
  • Webhook Secret: Go to your Project > CMS Settings and configure a webhook endpoint. The secret is generated automatically.

Full API documentation: Developer Docs

2. Choose your integration style

The easiest way to integrate. Register the blueprint and provide a handler function — the SDK handles signature verification, article fetching, and error responses automatically.

from flask import Flask
from ai_content_autopilot import configure_visibly, contentpilot_webhook_bp

app = Flask(__name__)

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

    article dict contains:
      id, title, slug, content_html, content_markdown,
      keywords, meta_description, seo_score, word_count,
      _webhook_event, _webhook_timestamp, _scheduled_date
    """
    # Save to your database, CMS, filesystem, etc.
    db.session.add(Post(
        title=article['title'],
        body=article['content_html'],
        slug=article['slug'],
        publish_at=article.get('_scheduled_date'),
    ))
    db.session.commit()
    return True  # Return False to reject (422 response)

configure_visibly(
    webhook_secret='your-webhook-secret',
    api_key='sk_live_your_api_key',
    on_article_received=my_handler,
)

app.register_blueprint(contentpilot_webhook_bp)
# POST /webhooks/visibly is now active and handles:
#   1. HMAC-SHA256 signature verification
#   2. Full article fetch via Pull API
#   3. Calls my_handler(article)
#   4. Returns {"success": true} or appropriate error

Option B: Standalone Pull API Client

Use the client directly to poll for articles or integrate into non-Flask applications.

from ai_content_autopilot import VisiblyClient

client = VisiblyClient(api_key='sk_live_your_api_key')

# List approved articles ready for publishing
articles = client.list_articles(status='approved', project_id=5, limit=20)
for a in articles:
    print(a['id'], a['title'], a.get('scheduled_date'))

# Fetch a single article with full content
article = client.fetch_article(42, include_markdown=True)
print(article['content_html'])
print(article['keywords'])       # e.g. ["seo", "keyword research"]
print(article['seo_score'])      # 0-100

# Confirm publication (updates status to "published" in Visibly)
client.confirm_published(42, 'https://myblog.com/seo-guide-2026')

Option C: HMAC verification only

For custom webhook implementations in any framework.

from ai_content_autopilot import verify_webhook_signature

# In your webhook endpoint:
payload_bytes = request.get_data()       # raw bytes, NOT request.json
signature = request.headers.get('X-Webhook-Signature', '')

if not verify_webhook_signature(payload_bytes, 'your-secret', signature):
    return {'error': 'Invalid signature'}, 401

Article Lifecycle

StatusDescriptionWebhook Event
queuedWaiting to be generated-
generatingAI is writing the article-
draftDraft ready for review-
approvedReady for publishingarticle.approved
publishedConfirmed published via APIarticle.published
rejectedRejected by user-
failedGeneration failedarticle.failed

An already published article that is edited in Visibly and pushed back fires article.updated. The payload carries published_url and revision so you can find the existing post by URL and skip a state you already have.

API Reference

Function / ClassDescription
configure_visibly(webhook_secret, api_key, base_url, on_article_received, background)Configure the Blueprint. background=True (default) acknowledges with 202 and works in a thread
verify_webhook_signature(payload_bytes, secret, signature_header)Verify HMAC-SHA256 signature. Returns True/False
VisiblyClient(api_key, base_url, timeout)Pull API client for fetching, listing, and confirming articles
client.fetch_article(article_id, include_markdown)Returns article dict or None on error
client.list_articles(status, project_id, limit, offset)Returns list of article dicts
client.confirm_published(article_id, published_url)Confirms publication. Returns True/False
contentpilot_webhook_bpFlask Blueprint. Register with app.register_blueprint(). Endpoint: POST /webhooks/visibly
default_flask_blog_handler(article)Default handler: saves article as JSON to ./content_output/

Webhook Payload

When a webhook fires, the POST /webhooks/visibly endpoint receives a JSON body with these fields:

{
  "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"
}
FieldTypeDescription
eventstringarticle.approved, article.updated, article.published, or article.failed
article_idintDatabase ID of the article
titlestringArticle title
slugstringURL-safe slug
project_idintOwning project ID
scheduled_datestring/nullISO 8601 scheduled publish date, or null
pull_urlstringFull URL to fetch article content via Pull API
timestampstringISO 8601 UTC timestamp of the event
published_urlstringLive URL of the existing post (article.updated only)
revisionintCounter that increases on every write in Visibly (article.updated only)

The Blueprint fetches the full article via the Pull API and injects webhook metadata (_webhook_event, _webhook_timestamp, _scheduled_date, _published_url, _revision) into the article dict before calling your handler.

Article Response Fields

When fetching an article via VisiblyClient.fetch_article(), the returned dict contains:

FieldTypeDescription
idintUnique article ID
titlestringArticle title
slugstringURL-friendly slug
statusstringCurrent status (see Article Lifecycle)
content_htmlstringFull article as HTML
content_markdownstringArticle as Markdown (only if include_markdown=True)
keywordslistTarget keywords, e.g. ["seo", "keyword research"]
meta_descriptionstringSEO meta description (max 160 chars)
seo_scoreintSEO optimization score (0-100)
word_countintArticle word count
project_idintOwning project ID
plan_idint/nullContent cluster the article belongs to
url_prefixstring/nullPath prefix of the cluster, e.g. /glossary/
content_languagestring/nullLanguage of the cluster, e.g. de, en
target_countrystring/nullTarget country of the cluster, e.g. DE
recommended_page_typestringPage template hint, e.g. guide, blog
revisionintIncreases on every write in Visibly
content_formatstringhtml or markdown
published_urlstringPublic URL after publication (empty if unpublished)
scheduled_datestring/nullPlanned publish date (ISO 8601)
created_atstringCreation timestamp
updated_atstringLast update timestamp

Delivery Contract

A webhook is a signal, not a job with a return value. Visibly waits 10 seconds for your response, and this is what each outcome means to it:

Your responseHow Visibly reads it
202Accepted, you are working on it. Success.
200 with a body naming blog_post_id, post_id or idProcessed, done.
200 with a JSON body naming none of thoseAcknowledged, but nothing happened. Logged as a failure with your message.
4xxRejected. Not retried, a second attempt would fail the same way.
5xx, 429Transient. Retried after 1s, 5s, 25s.
No response within 10s (read timeout)Delivered, outcome unknown. Not retried.

That last row is the one that matters. Up to v1.0.2 this SDK fetched the article and ran your handler inside the request. If your handler was slow, and translating an article into several languages is slow, Visibly gave up waiting and delivered again. Measured in production on 2026-09-14: three delivery attempts for one article turned into three LLM translation runs on the receiving CMS.

Both sides are fixed now. Visibly no longer retries on a read timeout, because the request had already reached you. And this SDK acknowledges with 202 before doing any work:

configure_visibly(
    webhook_secret='...',
    api_key='...',
    on_article_received=my_handler,
    background=True,   # the default: acknowledge first, work afterwards
)

The background path also keeps a process-local register of articles currently being processed, so a repeated delivery is skipped instead of running your handler twice. With multiple web workers that register does not span processes. If double processing would be expensive for you, add a claim column in your own database.

Set background=False only if your handler reliably finishes inside those 10 seconds. You then get the synchronous response codes (200 / 422 / 500 / 502).

Multilingual Sites and hreflang

Visibly writes one article in one language. There is no translation endpoint, and no article carries several language variants. Going multilingual is a structural decision, not a post-processing step, and you have two ways to make it.

Option A: your CMS translates

You receive the source article and produce the other languages yourself. This is what TMPilot.ai does: one German article arrives, the CMS translates it and stores all languages under one post.

  • You own the translation cost, the glossary, and the consistency between languages.
  • Visibly sees one article. A later article.updated overwrites your source language, and your CMS re-derives the rest.
  • Slow by nature, which is exactly why your handler must not run inside the webhook request. See Delivery Contract above.

Option B: one cluster per language

A content cluster in Visibly is a plan with its own path prefix, language and target country. Create one per language, and Visibly writes each article natively in that language instead of translating it:

Clustercontent_languagetarget_countryurl_prefix
Glossary DEdeDE/glossar/
Glossary ENenUS/en/glossary/
Glossary FRfrFR/fr/glossaire/

Every article then arrives with those three fields, and you build the URL:

def my_handler(article):
    prefix = article.get('url_prefix') or '/'
    url = f"https://example.com{prefix}{article['slug']}"
    lang = article.get('content_language')  # 'de', 'en', 'fr' - or None
    ...
  • Native phrasing per language, not a translation of German sentence structure. Keywords are researched per market.
  • Costs one article per language against your monthly quota.

How the hreflang structure comes about

Visibly does not build URLs and does not emit hreflang tags. It delivers slug, url_prefix and content_language as the blueprint; the finished URL is yours, and you report it back with confirm_published(). The tags are therefore yours to render:

<link rel="alternate" hreflang="de" href="https://example.com/glossar/nizza-klasse/">
<link rel="alternate" hreflang="en" href="https://example.com/en/glossary/nice-class/">
<link rel="alternate" hreflang="fr" href="https://example.com/fr/glossaire/classe-de-nice/">
<link rel="alternate" hreflang="x-default" href="https://example.com/en/glossary/nice-class/">

Three rules that generators get wrong more often than not:

  1. Every variant links to every variant, including itself. A page that omits its own hreflang is treated as an incomplete set and ignored.
  2. x-default points at the version for visitors none of the others fit, usually the English one. It is not "the site's default language".
  3. Only link pages that exist and are indexable. An hreflang pointing at a noindex page or a 404 invalidates the whole cluster.

What Visibly does not tell you yet: which articles are translations of one another. Two clusters deliver two independent articles, and there is no group identifier linking them. Keep that mapping on your side, using the cluster pair plus your own topic key, or derive it from plan_id and the order in which topics were queued.

Webhook Security

Every webhook request includes an X-Webhook-Signature header with an HMAC-SHA256 signature:

X-Webhook-Signature: sha256=<hex_digest>

The SDK verifies this automatically when using the Blueprint. The verification uses hmac.compare_digest for timing-safe comparison to prevent timing attacks.

Error Handling

The VisiblyClient methods handle errors gracefully:

  • fetch_article() returns None on any error (404, network failure, timeout)
  • list_articles() returns [] on any error
  • confirm_published() returns False on any error

All errors are logged via Python's logging module at WARNING or ERROR level.

When using the Blueprint with background=True (the default), HTTP responses are:

CodeMeaning
202Accepted — fetching and processing in the background
202 already_processingThis article is already being processed, duplicate skipped
400Invalid JSON in webhook payload
401HMAC signature verification failed
500Webhook secret or API key not configured

A failure inside your handler does not become a 5xx: the delivery succeeded, the work did not. A 5xx would invite a retry that reproduces the same error. Failures are logged, so watch your logs rather than the HTTP status.

With background=False the response reflects the outcome of the work:

CodeMeaning
200Success — article processed
422Handler returned False (article rejected)
500Webhook not configured, or handler raised an exception
502Failed to fetch article from Pull API

Rate Limits

The Visibly API enforces the following rate limits:

EndpointLimit
GET /api/v1/articles120 requests/min
GET /api/v1/articles/{id}60 requests/min
POST /api/v1/articles/{id}/confirm30 requests/min

When rate-limited, the API returns HTTP 429 with a Retry-After header.

Development

git clone https://github.com/AntonioBlago/ai-content-autopilot.git
cd ai-content-autopilot
pip install -e ".[dev]"
pytest tests/ -v

License

MIT - see LICENSE for details.

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
1.1.014 Sept 20264

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
antonioblago/ai-content-autopilot

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 21 Oct, 10:16 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:3aea704a155946725566254e2b0ed11fc466a6d1a68f35c07c16d6ab9bded6c5