Skip to content

Latest commit

 

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fopost

PyPI Python versions CI License: MIT

Official Python SDK for the FoPost API. Schedule and publish to +30 social platforms from your code.

pip install fopost

Requires Python 3.10 or newer. Built on httpx and pydantic v2, fully typed.

0.x release. The public API is still settling and minor versions may contain breaking changes. Pin an exact version if that matters to you.

Quick start

from fopost import Fopost

client = Fopost(api_key="fp_...")  # or set FOPOST_API_KEY

workspace = client.workspaces.list()[0]
accounts = client.accounts.list(workspace_id=workspace.id)

post = client.posts.create(
    workspace_id=workspace.id,
    content="Hello from Python",
    accounts=[a.id for a in accounts],
)

client.posts.publish(post.id)

content takes a string for a single block, or a list for a thread:

client.posts.create(
    workspace_id=workspace.id,
    content=[
        "First post in the thread",
        {
            "text": "Second one, with an image",
            "media": [
                {"type": "image", "name": "chart.png", "url": "https://.../chart.png"},
            ],
        },
    ],
    accounts=[a.id for a in accounts],
)

Scheduling

status is "draft" or "scheduled"; a scheduled post needs schedule_at. To send something out now, create it and call publish.

from datetime import datetime, timezone

client.posts.create(
    workspace_id=workspace.id,
    status="scheduled",
    schedule_at=datetime(2026, 9, 1, 10, 0, tzinfo=timezone.utc),
    content="Scheduled with the SDK",
    accounts=[accounts[0].id],
)

Pagination

posts.list returns one page and iterates over its items directly. posts.iter walks every page for you:

page = client.posts.list(workspace_id=workspace.id, status="published", per_page=50)
print(f"{page.meta.total} published posts")
for post in page:
    print(post.id, post.status)

# Every post, one page fetched at a time
for post in client.posts.iter(workspace_id=workspace.id):
    print(post.id)

# Or page by page, when you want the meta
for page in client.posts.iter_pages(workspace_id=workspace.id):
    print(page.meta.current_page, len(page))

AI features

balance = client.ai.credits()
print(f"{balance.credits_remaining} of {balance.credits_total} credits left")

result = client.ai.generate_caption(
    current_caption="shipping a new feature",
    platforms=["twitter", "linkedin"],
)
print(result.caption)

rewrite and repurpose_url are wired the same way:

rewrites = client.ai.rewrite(
    content="Long article-style draft...",
    platforms=["twitter", "linkedin", "bluesky"],
)
for variant in rewrites.results:
    print(variant.platform, variant.content)

repurposed = client.ai.repurpose_url(
    url="https://example.com/blog/post",
    platforms=["twitter", "linkedin", "bluesky", "threads"],
)

API keys reach credits and generate_caption. rewrite and repurpose_url currently require a signed-in dashboard session and answer 401 to an API key. They are here so the surface is complete once the server opens them up.

Direct uploads

upload_direct presigns an upload slot, PUTs the bytes straight to storage, and registers the result as a library item:

with open("chart.png", "rb") as f:
    item = client.media.upload_direct(
        workspace_id=workspace.id,
        filename="chart.png",
        mime_type="image/png",
        data=f.read(),
    )
print(item.id, item.preview_url)

presign and complete are the two halves, for when you want to send the bytes yourself.

Configuration

Fopost(
    api_key="fp_...",  # or FOPOST_API_KEY
    base_url="https://api.fopost.com/v1",  # override for a dev server
    timeout=30.0,  # seconds, or an httpx.Timeout
    max_retries=3,  # total attempts on a 429
    http_client=my_httpx_client,  # bring your own transport
)
Env var Used for
FOPOST_API_KEY API key, when not passed to the constructor

The client is a context manager, and closes its transport on exit:

with Fopost() as client:
    client.posts.list(workspace_id=workspace.id)

A 429 is retried automatically, waiting for the interval the API asks for in Retry-After (delta-seconds or an HTTP date, capped at 60s). max_retries counts total attempts, so the default of 3 means two retries.

Error handling

Every non-2xx response raises FopostError or one of its subclasses, carrying the API's status, code, and message.

from fopost import Fopost, FopostError, PaymentRequiredError, RateLimitError

try:
    client.posts.publish("9b2f6c1e-...")
except PaymentRequiredError as err:
    print(f"Out of credits — upgrade at {err.upgrade_url}")
except RateLimitError as err:
    print(f"Rate limited, retry in {err.retry_after}s")
except FopostError as err:
    print(f"API {err.status} ({err.code}): {err.message}")
Status Exception
401 AuthenticationError
402 PaymentRequiredError
403 PermissionDeniedError
404 NotFoundError
429 RateLimitError
other FopostError

Resources

Namespace Methods
posts list, iter, iter_pages, get, create, update, delete, publish, cancel, retry, preflight, deliveries
accounts list, get, health, update, move, create_telegram_connect_code, get_telegram_connect_status, get_telegram_bot_commands, set_telegram_bot_commands, delete_telegram_bot_commands, list_slack_channels, list_slack_members, get_slack_identity, update_slack_identity, get_ice_breakers, set_ice_breakers, delete_ice_breakers, get_persistent_menu, set_persistent_menu, delete_persistent_menu, get_greeting, set_greeting, delete_greeting, get_webhook_subscription, resubscribe_webhook, list_discord_channels, switch_discord_channel, get_discord_identity, update_discord_identity, list_discord_pins, delete_discord_message, pin_discord_message, unpin_discord_message, crosspost_discord_message, create_discord_thread, send_discord_dm, list_discord_events, get_discord_event, create_discord_event, update_discord_event, delete_discord_event, list_discord_members, get_discord_member, list_discord_roles, create_discord_role, update_discord_role, delete_discord_role, add_discord_member_role, remove_discord_member_role, platform_metrics
account_groups list, create, get, update, delete, set_members
workspaces list, get
labels list
media presign, complete, upload_direct
ai credits, generate_caption, rewrite, repurpose_url
inbox list, threads, conversations, unread_count, accounts, platforms, mark_thread_read, refresh, update, edit_comment, reply, hide, unhide, delete, like, unlike, pin, unpin, react, start_conversation, set_typing, handover, list_approvals, approve_reply, reject_reply
inbox list, threads, conversations, unread_count, accounts, platforms, mark_thread_read, refresh, update, edit_comment, reply, hide, unhide, delete, like, unlike, pin, unpin, react, start_conversation, set_typing, list_approvals, approve_reply, reject_reply
contacts list, get, create, update, delete, conversations, import_csv, list_fields, create_field, update_field, delete_field, conversation_analytics
broadcasts list, get, create, update, delete, send, cancel, recipients
sequences list, get, create, update, delete, enroll, unenroll, enrollments
ads list, external, boostable, connections, sources, authorize_meta, delete_connection, boost, create, refresh, set_status, delete, audiences, create_audience, search_targeting, lead_forms, create_lead_form, leads, account_tree, create_campaign, get_campaign, update_campaign, delete_campaign, duplicate_campaign, create_ad_set, get_ad_set, update_ad_set, delete_ad_set, duplicate_ad_set, create_network_ad, get_network_ad, update_network_ad, delete_network_ad, duplicate_network_ad, bulk_set_status, creatives, create_creative, get_creative, delete_creative, get_audience, update_audience, delete_audience, add_audience_users, estimate_reach, insights, ad_insights, get_lead_form, archive_lead_form, leads_feed, lead_pages, subscribe_lead_page, unsubscribe_lead_page, goals, catalogs, create_catalog, get_catalog, update_catalog, delete_catalog, catalog_products, write_catalog_products, product_feeds, create_product_feed, delete_product_feed, feed_uploads, start_feed_upload, product_sets, create_product_set, update_product_set, delete_product_set, reach_frequency, create_reach_frequency, get_reach_frequency, reserve_reach_frequency, cancel_reach_frequency, library, partnership_creators, request_partnership, revoke_partnership, account_activity, labels, create_label, update_label, delete_label, apply_label, studies, create_study, get_study, delete_study, ios_campaign_limits, high_demand_periods, create_high_demand_period, delete_high_demand_period, value_rule_sets, create_value_rule_set, delete_value_rule_set
knowledge list, create, update, delete, sync, search
ads list, external, boostable, connections, sources, authorize_meta, delete_connection, boost, create, refresh, set_status, delete, audiences, create_audience, search_targeting, lead_forms, create_lead_form, leads, account_tree, create_campaign, get_campaign, update_campaign, delete_campaign, duplicate_campaign, create_ad_set, get_ad_set, update_ad_set, delete_ad_set, duplicate_ad_set, create_network_ad, get_network_ad, update_network_ad, delete_network_ad, duplicate_network_ad, bulk_set_status, creatives, create_creative, get_creative, delete_creative, get_audience, update_audience, delete_audience, add_audience_users, estimate_reach, insights, ad_insights, get_lead_form, archive_lead_form, leads_feed, lead_pages, subscribe_lead_page, unsubscribe_lead_page
google_business get_location, update_location, get_attributes, update_attributes, get_menus, replace_menus, get_services, replace_services, list_media, add_media, delete_media, list_place_actions, create_place_action, update_place_action, delete_place_action, get_verification_options, start_verification, complete_verification, get_performance, get_search_keywords, assign
validate post, length, media
activity list

For an endpoint the SDK does not wrap yet, client.request sends an authenticated call and hands back the decoded body:

client.request("GET", "/analytics/summary", params={"workspace_id": workspace.id})

Chat adapter

fopost.chat_adapter wraps the inbox conversation and reply endpoints in a send/receive interface, so a chatbot framework can treat FoPost as one channel across every network that carries direct messages.

from fopost import Fopost
from fopost.chat_adapter import ChatAdapter

chat = ChatAdapter(
    Fopost(api_key=os.environ["FOPOST_API_KEY"]),
    workspace_id=workspace_id,
    webhook_secret=os.environ["FOPOST_WEBHOOK_SECRET"],
)

Inbound is the inbox.message_received webhook. Subscribe an endpoint to it in FoPost, then hand the raw body and the request headers to parse_webhook. It verifies the signature, refuses a replay, and returns the event; the event carries ids only, so receive_one reads the text back:

@app.post("/webhooks/fopost")
async def inbound(request: Request) -> Response:
    event = chat.parse_webhook(await request.body(), request.headers)
    message = chat.receive_one(event)
    if message is None:
        return Response(status_code=204)

    chat.typing(message.conversation_id, message.account_id)
    chat.send(reply_to=message.id, text=your_bot(message.text))
    chat.mark_read(message)
    return Response(status_code=204)

No webhook? receive() polls the same thing:

for message in chat.receive():
    chat.send(reply_to=message.id, text=your_bot(message.text))
    chat.mark_read(message)

Outbound takes one of three shapes. Reply to a message, reply into a thread, or open one by handle:

chat.send(reply_to=message.id, text="On it.")
chat.send(conversation_id="conv_...", text="Still here.")
chat.send(account_id="acc_...", handle="samrivera", text="Following up.")
Method What it does
parse_webhook(body, headers) Verifies a delivery and returns the ChatEvent
verify_webhook(body, headers) Signature check on its own; raises on a forged or stale delivery
receive(...) Inbound DMs, unread by default
receive_one(event_or_id) The full message behind an event id, or None
send(...) Reply, reply into a thread, or open one
typing(conversation_id, account_id, on=True) Typing indicator
mark_read(message) Marks the message read

Sending needs the publish scope on top of inbox. Every failure is a ChatAdapterError with a code (invalid_signature, stale_delivery, unexpected_event, unsupported_target, and the rest) or the usual FopostError from the API.

Example

examples/create_post.py creates a post against a running API:

export FOPOST_API_KEY=fp_...
export FOPOST_BASE_URL=http://localhost:8080/v1
python examples/create_post.py "Hello from the Python SDK" --publish

Contributing

Issues and pull requests are welcome at fopost/fopost-python.

uv sync --group dev
uv run pytest
uv run ruff check .
uv run mypy

License

MIT

Questions or a problem: fopost.com/contact.

Google Ads

Campaigns, ad groups, ads, audiences and insights are on client.ads and dispatch by connection. What only Google has is under client.ads.google:

keywords = client.ads.google.keywords(
    connection_id="c4d5e6f7-…",
    customer_id="1234567890",
)

client.ads.google.create_keyword(
    workspace_id="7d2b8c11-…",
    connection_id="c4d5e6f7-…",
    customer_id="1234567890",
    ad_group_id="1234567890~adGroup~77",
    text="running shoes",
    match_type="EXACT",
)

Also keyword_ideas, keyword_metrics, search_terms, bid_strategies, ad_schedule and set_ad_schedule, the negative keyword lists, assets and asset_groups, local_services_leads, the conversion methods, and query for a raw read-only GAQL SELECT. Changes need the publish scope as well as ads; customer_id has to name an account the connection's grant reaches.

About

Official Python SDK for the Fopost API. Schedule and publish to +30 social platforms from your code.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages