SEOJuice Python SDK
Official Python SDK for the SEOJuice Intelligence API.
- Typed wrappers for all
/api/v2/endpoints - Sync and async clients (powered by httpx)
- Built-in pagination helpers
- Django and ASGI middleware for automatic SEO tag injection
- Full type annotations with PEP 561 support
Installation
pip install seojuice
With Django middleware support:
pip install seojuice[django]
Quick Start
from seojuice import SEOJuice
client = SEOJuice("your-api-key")
# List all websites
websites = client.list_websites()
for site in websites["results"]:
print(site["domain"])
# Get detailed website info
detail = client.get_website("example.com")
print(detail["seo_score"])
client.close()
Domain-Scoped Client
Use .website(domain) to scope every call to one domain without repeating it. The returned WebsiteResource is fully typed, so autocomplete and mypy work across its methods:
from seojuice import SEOJuice
with SEOJuice("your-api-key") as client:
site = client.website("example.com")
# Get website details
detail = site.detail()
# List pages
pages = site.pages(page_size=20)
for page in pages:
print(page["url"], page["seo_score"])
# Get intelligence summary with trends
intel = site.intelligence(period="30d", include_trends=True)
# Check AISO score
aiso = site.aiso(include_history=True)
print(f"AISO: {aiso['aiso_score']}")
# List content gaps
gaps = site.content_gaps(intent="informational")
for gap in gaps:
print(gap["page_name"], gap["seo_potential"])
Pagination
Manual Pagination
result = client.list_pages("example.com", page=1, page_size=10)
print(f"Total: {result.total_count}")
print(f"Has next: {result.has_next}")
for page in result:
print(page["url"])
Auto-Pagination
Iterate through all pages automatically:
from seojuice import SEOJuice, auto_paginate
client = SEOJuice("your-api-key")
for page in auto_paginate(
lambda **kw: client.list_pages("example.com", **kw),
page_size=100,
):
print(page["url"])
Async Usage
import asyncio
from seojuice import AsyncSEOJuice
async def main():
async with AsyncSEOJuice("your-api-key") as client:
site = client.website("example.com")
detail = await site.detail()
pages = await site.pages(page_size=50)
for page in pages:
print(page["url"])
asyncio.run(main())
Async Auto-Pagination
import asyncio
from seojuice import AsyncSEOJuice
from seojuice._pagination import async_auto_paginate
async def main():
async with AsyncSEOJuice("your-api-key") as client:
site = client.website("example.com")
async for page in async_auto_paginate(site.pages, page_size=100):
print(page["url"])
asyncio.run(main())
Error Handling
from seojuice import SEOJuice, AuthError, NotFoundError, RateLimitError, APIError
client = SEOJuice("your-api-key")
try:
detail = client.get_website("example.com")
except AuthError:
print("Invalid API key")
except NotFoundError:
print("Website not found")
except RateLimitError as e:
print(f"Rate limited: {e.message}")
except APIError as e:
print(f"API error [{e.status_code}]: {e.message}")
Exception hierarchy:
SEOJuiceError-- base exceptionAPIError-- any HTTP 4xx/5xxAuthError-- 401ForbiddenError-- 403NotFoundError-- 404RateLimitError-- 429ServerError-- 5xx
APIConnectionError-- connection refused, DNS failure, resetAPITimeoutError-- connect or read timeout
Everything the client can raise for a network round-trip is a SEOJuiceError, so a single except SEOJuiceError catches it all. A non-JSON error body (an HTML 502/504, an empty 503/429) surfaces as the typed ServerError/RateLimitError for its status code, not a raw JSONDecodeError. Connect/read timeouts and transport failures raise APITimeoutError/APIConnectionError.
The client does not retry. Wrap the call and retry on APITimeoutError, APIConnectionError, or RateLimitError if you want that.
Changes Management
Review, approve, reject, revert, and automate SEO changes over their lifecycle:
with SEOJuice("your-api-key") as client:
site = client.website("example.com")
# Get change statistics
stats = site.change_stats()
print(f"Total changes: {stats['total']}")
print(f"By status: {stats['by_status']}")
# List pending changes
pending = site.changes(status="pending", page_size=20)
for change in pending:
print(f"{change['change_type']}: {change['page_url']}")
print(f" {change['previous_value']} → {change['proposed_value']}")
# Approve a change
site.approve_change(change_id=42)
# Reject with reason
site.reject_change(change_id=43, reason="Not aligned with brand voice")
# Revert an applied change
site.revert_change(change_id=44, reason="Caused ranking drop")
# Bulk actions
result = site.bulk_change_action(
action="approve",
ids=[10, 11, 12],
)
print(f"Succeeded: {result['total_succeeded']}")
# Configure automation settings
site.update_change_settings(
auto_approve_internal_links=True,
auto_approve_meta_descriptions=False,
)
Action Items
List, create, and update SEO action items by priority and category:
with SEOJuice("your-api-key") as client:
site = client.website("example.com")
# Get action item summary
summary = site.action_item_summary()
print(f"Total: {summary['total']}, Open: {summary['open']}")
# List action items
items = site.action_items(priority="high", status="open")
for item in items:
print(f"[{item['priority']}] {item['title']}")
# Get grouped by category
groups = site.action_item_groups()
for group in groups:
print(f"{group['category']}: {group['count']} items")
# Create a new action item
site.create_action_item(
title="Fix missing alt tags on product images",
priority="high",
category="accessibility",
)
# Update status
site.update_action_item(item_id=99, action="complete")
Webhook Verification
Verify incoming webhook signatures using HMAC-SHA256:
from seojuice import verify_webhook_signature
# In your webhook handler
is_valid = verify_webhook_signature(
secret="your-webhook-secret",
body=request.body,
signature=request.headers.get("X-SEOJuice-Signature"),
)
if not is_valid:
return Response(status=401)
verify_webhook_signature fails closed: it returns False (never raises) when the secret, body, or signature is missing, None, or the wrong type. A request with no X-SEOJuice-Signature header rejects with 401 instead of raising a 500 inside your handler — so passing request.headers.get(...) straight through, as above, is safe. signature must be the raw hex digest from the header, with no sha256= prefix.
See examples/webhook_receiver.py for a complete Flask-based receiver.
Scores & Additional Endpoints
with SEOJuice("your-api-key") as client:
site = client.website("example.com")
# Domain health score
health = site.domain_health()
print(f"Health score: {health['overall_score']}")
# SERP landscape analysis
serp = site.serp_landscape()
# Industry benchmarks
benchmarks = site.benchmarks()
# Page-scoped endpoints (require page_id)
quality = site.content_quality(page_id=123)
geo = site.geo_readiness(page_id=123)
content = site.page_content(page_id=123)
# Submit URLs for processing
site.submit_urls(urls=[{"url": "https://example.com/new-page"}])
status = site.url_status(url="https://example.com/new-page")
PDF Report Download
with SEOJuice("your-api-key") as client:
site = client.website("example.com")
# Create a report
created = site.create_report("this_month")
print(f"Report queued: {created['report_id']}")
# Download PDF (once report is ready)
pdf_bytes = site.report_pdf(created["report_id"])
with open("report.pdf", "wb") as f:
f.write(pdf_bytes)
Django Middleware
Automatically inject SEO meta tags, Open Graph tags, and structured data into your HTML responses.
Setup
Add to your settings.py:
MIDDLEWARE = [
# ... other middleware ...
"seojuice.injection.django.SEOJuiceDjangoMiddleware",
]
# Optional settings
SEOJUICE_INJECTION_ENABLED = True # Default: True
SEOJUICE_INJECTION_TIMEOUT = 5.0 # Default: 5.0 seconds
The middleware fetches suggestions from smart.seojuice.io and injects:
<meta name="description">tag- Open Graph tags (
og:title,og:description,og:url,og:image) <title>tag injection when missing- JSON-LD structured data
Responses are cached in-memory with a 5-minute TTL. Non-HTML responses are passed through unchanged.
FastAPI / ASGI Middleware
For any ASGI framework (FastAPI, Starlette, etc.):
from fastapi import FastAPI
from seojuice.injection.asgi import SEOJuiceASGIMiddleware
app = FastAPI()
app.add_middleware(
SEOJuiceASGIMiddleware,
base_url="https://example.com",
timeout=3.0,
enabled=True,
)
Configuration
Both clients accept these constructor arguments:
| Parameter | Type | Default | Description |
|---|---|---|---|
api_key |
str |
(required) | Your SEOJuice API key |
base_url |
str |
https://seojuice.com/api/v2 |
API base URL |
timeout |
float |
30.0 |
Request timeout in seconds |
http_client |
httpx.Client |
None |
Custom httpx client instance |
Constructing with an empty, whitespace, or None key raises ValueError("api_key is required") at call time — the client never sends Bearer None.
Flask Integration
Use before_request / after_request hooks for automatic SEO tag injection:
from flask import Flask, g, request
from seojuice.injection._fetcher import apply_suggestions, fetch_suggestions_sync
app = Flask(__name__)
@app.before_request
def fetch_seo():
g.seo_suggestions = fetch_suggestions_sync(request.url, timeout=3.0)
@app.after_request
def inject_seo(response):
suggestions = getattr(g, "seo_suggestions", None)
if suggestions and "text/html" in (response.content_type or ""):
html = response.get_data(as_text=True)
response.set_data(apply_suggestions(html, suggestions))
return response
Install with Flask extras:
pip install seojuice[flask]
Examples
| Example | Description |
|---|---|
intelligence_api.py |
Full Intelligence API workflow — overview, gaps, decay, PageSpeed |
changes_management.py |
Change lifecycle: stats, triage, bulk approve, review/reject, automation settings |
webhook_receiver.py |
Flask webhook receiver with HMAC-SHA256 signature verification |
action_items.py |
Action items: summary, listing, groups, create, update |
django_views.py |
Django views and class-based mixin for SEO context |
fastapi_app.py |
FastAPI app with async client lifecycle and ASGI middleware |
flask_app.py |
Flask app with request hooks and TTL cache |
async_workflow.py |
Concurrent fetching, async pagination, batch domain processing |
celery_tasks.py |
Background analysis, periodic decay checks, PDF report generation |
redis_cache.py |
Redis caching layer with pattern-based invalidation |
cms_integration.py |
Wagtail, Django CMS, and headless WordPress integrations |
Requirements
- Python 3.9+
- httpx >= 0.24.0
- typing_extensions >= 4.0
- django >= 3.2 (optional, for Django middleware)
License
MIT
Metadata
Release files for seojuice 1.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| seojuice-1.4.0.tar.gz | 67.8 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| seojuice-1.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 100.8 kB
Release files / seojuice-1.4.0.tar.gz
| Download URL | seojuice-1.4.0.tar.gz |
|---|---|
| Size | 67.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
98c5da206eaa41117e6bdce6a8b0a42070be3671bf4a6df9822952678b56f6ef
|
|
BLAKE2b-256 checksum How to use checksums |
0c2b74c2ccf16629e072767ee6756fc152ccca4f713f10c6a58dd7908554b1ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.11
|
Release files / seojuice-1.4.0-py3-none-any.whl
| Download URL | seojuice-1.4.0-py3-none-any.whl |
|---|---|
| Size | 32.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
738c2d12c64189bd1f7e2146df8dfc39c73a515be01431bbcc55775f61cb2138
|
|
BLAKE2b-256 checksum How to use checksums |
0fd4b739a0f135e60a6dd318ffbbd1a007f8069f4e65d5e0911c51e04f26344e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.11
|
Security for seojuice 1.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.