Django
The FoPost Python client wired into Django, with settings, management commands, and signals.
fopost-django is a thin wrapper over the Python SDK. It adds Django settings, a lazily built shared client, two management commands, system checks, and a signed webhook receiver that fires Django signals. There are no models and no migrations, because this package stores nothing.
Needs Python 3.10 or newer and Django 4.2, 5.0, 5.1, or 5.2.
pip install fopost-djangoThis is a 0.x release. The surface is still settling and a minor version may break something. Pin an exact version if that matters to you.
Published on PyPI. Source and issues: github.com/fopost/fopost-django. MIT licensed.
Settings
Add the app and one settings dict:
import os
INSTALLED_APPS = [
# ...
"fopost_django",
]
FOPOST = {
"API_KEY": os.environ["FOPOST_API_KEY"],
"WEBHOOK_SECRET": os.environ["FOPOST_WEBHOOK_SECRET"],
"DEFAULT_WORKSPACE_ID": os.environ.get("FOPOST_WORKSPACE_ID"),
}| Key | Default | Env fallback | What it does |
|---|---|---|---|
API_KEY | none, required | FOPOST_API_KEY | Your API key |
BASE_URL | the hosted API | FOPOST_BASE_URL | API root |
TIMEOUT | 30.0 | (none) | Seconds to wait for one request |
MAX_RETRIES | 3 | (none) | Attempts for a rate limited request |
DEFAULT_WORKSPACE_ID | None | FOPOST_WORKSPACE_ID | Workspace the management commands use when --workspace is left out |
WEBHOOK_SECRET | None | FOPOST_WEBHOOK_SECRET | Secret the webhook receiver verifies signatures against |
HTTP_CLIENT | None | (none) | Advanced: an httpx.Client to send through, for a proxy or a test transport |
The whole dict is optional as long as FOPOST_API_KEY is in the environment. Django refuses to start without a key (an ImproperlyConfigured at boot beats a 401 in a customer's request), and manage.py check warns about the softer misconfigurations: a webhook URL wired up with no secret, a plaintext base URL, an API key hardcoded into settings.
Quick start
from django.http import JsonResponse
from fopost_django import client
def announce(request):
workspace = client.workspaces.list()[0]
accounts = client.accounts.list(workspace_id=workspace.id)
post = client.posts.create(
workspace_id=workspace.id,
content="Shipping today: scheduled posting straight from Django.",
accounts=[a.id for a in accounts],
)
client.posts.publish(post.id)
return JsonResponse({"post_id": post.id})client is a lazy proxy, so importing it at module scope never touches settings. Prefer an explicit call? get_client() returns the same memoized instance:
from fopost_django import get_client
get_client().posts.list(workspace_id=workspace_id, status="scheduled")The client is built once per process, on first use, behind a lock, and rebuilt automatically if settings.FOPOST changes, which is what override_settings does in your tests.
Management commands
python manage.py fopost_accounts --workspace 9b2f6c1e-...
python manage.py fopost_accounts --jsonLists the social accounts connected to a workspace. Falls back to FOPOST["DEFAULT_WORKSPACE_ID"], and to every workspace the key reaches when neither is set.
# A draft
python manage.py fopost_post -a acc_1 -a acc_2 --text "Hello from Django"
# Scheduled
python manage.py fopost_post -a acc_1 --text "Later" --schedule-at 2026-09-01T10:00:00Z
# Out the door now
python manage.py fopost_post -a acc_1 --text "Now" --publish| Flag | What it does |
|---|---|
-w, --workspace | Workspace id. Defaults to FOPOST["DEFAULT_WORKSPACE_ID"] |
-a, --account | A connected account. Repeat for more than one. At least one is required |
-t, --text | The post body. Required |
--title | Title, for platforms that use one |
--label | A label id to attach. Repeat for more than one |
--schedule-at | ISO 8601 datetime. A naive value is read in the project's current timezone |
--publish | Queue the post for delivery straight after creating it |
--schedule-at and --publish are mutually exclusive. API failures come back as ordinary CommandError output, not a traceback.
Receiving webhooks
Add the URLs:
from django.urls import include, path
urlpatterns = [
path("fopost/", include("fopost_django.urls")),
]That serves the receiver at /fopost/webhook/, reversible as reverse("fopost:webhook"). Register that URL as a webhook in the dashboard, copy the secret it shows you into FOPOST["WEBHOOK_SECRET"], and connect a receiver:
from django.dispatch import receiver
from fopost_django.signals import post_published, post_failed
@receiver(post_published)
def on_published(sender, event, data, payload, request, delivery_id, **kwargs):
Article.objects.filter(fopost_post_id=data["postId"]).update(announced=True)
@receiver(post_failed)
def on_failed(sender, data, **kwargs):
logger.error("FoPost post %s failed", data.get("postId"))Connect them from your app config's ready(), the usual way.
| Signal | FoPost event |
|---|---|
post_published | post.published |
post_failed | post.failed |
post_partially_failed | post.partially_failed |
delivery_published | delivery.published |
delivery_failed | delivery.failed |
delivery_delayed | delivery.delayed |
account_health_changed | account.health_changed |
webhook_received | every verified delivery, whatever the event |
Every receiver gets the same keyword arguments: event, data, payload (the whole envelope, with its timestamp), request, and delivery_id, the X-FoPost-Delivery header, which stays the same across retries and so makes a good idempotency key.
FoPost signs the raw request body with HMAC-SHA256, keyed on the webhook secret, and sends the hex digest as X-FoPost-Signature: sha256=<digest>. The view recomputes it over request.body and compares with hmac.compare_digest. A missing, malformed, or wrong signature is a 403 before any signal fires; a body that is not a JSON object is a 400. The view is csrf_exempt and accepts POST only.
Failures are meant to propagate. If a receiver raises, the response is a 5xx and FoPost retries the delivery with backoff. Keep receivers quick and idempotent, or hand the work to a task queue.
Testing your own code
Point the SDK at a stub transport instead of the network:
import httpx
from django.test import override_settings
from fopost_django import reset_client
def handler(request):
return httpx.Response(200, json={"data": {"id": "post_1", "status": "draft"}})
with override_settings(
FOPOST={
"API_KEY": "fp_test",
"HTTP_CLIENT": httpx.Client(transport=httpx.MockTransport(handler)),
}
):
reset_client()
...override_settings already invalidates the cached client; reset_client() is there for the cases where you swap the transport by hand.
The rest of the API
Posts, accounts, workspaces, labels, AI, pagination, error classes, and retry behaviour all live in the parent SDK, and everything the Python SDK page documents works through fopost_django.client unchanged.
from fopost import FopostError, RateLimitError
try:
client.posts.publish(post_id)
except RateLimitError as exc:
retry_in = exc.retry_after
except FopostError as exc:
print(exc.status, exc.code, exc.message)Next
Related documentation
- SDKs & Integrations
Official FoPost clients for TypeScript, Python, PHP, Ruby, Go, Rust, Java, .NET, Swift, Kotlin, Dart, and Elixir, framework integrations from Laravel to Next.js, and tooling for the CLI, CI, Terraform, and the automation platforms.
- SDKs Overview
Every official FoPost client, what it covers, and how to pick one.
- TypeScript SDK
The official TypeScript and Node.js client for the FoPost API.
- Python SDK
The official Python client for the FoPost API.
- PHP SDK
The official PHP client for the FoPost API, with no framework and no HTTP library.
- Ruby SDK
The official Ruby client for the FoPost API, with no runtime dependencies.
- Go SDK
The official Go client for the FoPost API.
- Rust SDK
The official Rust client for the FoPost API, async and built on reqwest.