Skip to main content

FoPost for Django

PyPI Python versions CI License: MIT

Official Django integration for the FoPost API. Schedule and publish to +30 social platforms from your Django project.

This is a thin wrapper around the fopost 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. Every platform connection, token refresh, and delivery happens on the hosted API, so there is nothing to run yourself — and no models and no migrations, because this package stores nothing.

Requirements

Install

pip install fopost-django

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 https://api.fopost.com/v1 FOPOST_BASE_URL API root
TIMEOUT 30.0 Seconds to wait for one request
MAX_RETRIES 3 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 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

fopost_accounts

python manage.py fopost_accounts --workspace 9b2f6c1e-...
python manage.py fopost_accounts --json

Lists 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.

fopost_post

# 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 at fopost.com/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.

How it is verified. 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. See fopost on PyPI and its README; everything it 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)

Links

License

MIT. Copyright (c) 2026 Porter Bridge, LLC.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fopost_django-0.1.1.tar.gz (18.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fopost_django-0.1.1-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

Details for the file fopost_django-0.1.1.tar.gz.

File metadata

  • Download URL: fopost_django-0.1.1.tar.gz
  • Upload date:
  • Size: 18.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for fopost_django-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e8eecbb5a55c5e80a7d1d09e7ddbd9d0606d6d78d39f3bc819c2e7db3e780338
MD5 8a0ef5ea7df9528f5f0c1a070a9f5750
BLAKE2b-256 5439fb63b6f527d930630549d8d952d5811b036a7f43d2323efd8ae704512e21

See more details on using hashes here.

Provenance

The following attestation bundles were made for fopost_django-0.1.1.tar.gz:

Publisher: release.yml on fopost/fopost-django

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fopost_django-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: fopost_django-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 18.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for fopost_django-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 75de66d71f0221ff364df3b6841d1554b1aa15d6e70f60ff9d57ebb9b4d89f8b
MD5 9aa82db78fce310b44f2447a307ddefb
BLAKE2b-256 d142b9cc4b82511ca5d768cba29a7653ba1085eb1ab249f276a242f5c4b6f18b

See more details on using hashes here.

Provenance

The following attestation bundles were made for fopost_django-0.1.1-py3-none-any.whl:

Publisher: release.yml on fopost/fopost-django

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page