fopost 0.2.0

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

fopost

crates.io docs.rs license ci

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

[dependencies]
fopost = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Async, built on reqwest. Requires Rust 1.88 or newer.

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

use fopost::{models::CreatePost, Client};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::from_env()?; // reads FOPOST_API_KEY

    // Everything is scoped to a workspace.
    let workspaces = client.workspaces().list().await?;
    let workspace = &workspaces[0];

    let accounts = client.accounts().list(Some(&workspace.id)).await?;
    let ids: Vec<_> = accounts.iter().map(|a| a.id.clone()).collect();

    // Create a post, then publish it immediately.
    let post = client
        .posts()
        .create(&CreatePost::text(&workspace.id, "Hello from Rust").accounts(ids.clone()))
        .await?;
    client.posts().publish(&post.id, &Default::default()).await?;

    // Or schedule it for later.
    client
        .posts()
        .create(
            &CreatePost::text(&workspace.id, "Scheduled with the SDK")
                .accounts(ids)
                .schedule_at("2026-09-01T10:00:00Z"),
        )
        .await?;

    Ok(())
}

Configuration

use std::time::Duration;

let client = fopost::Client::builder()
    .api_key(std::env::var("FOPOST_API_KEY")?)
    .base_url("https://api.fopost.com/v1") // override for another deployment
    .timeout(Duration::from_secs(30))
    .max_retries(3)                        // 1 disables retrying
    .build()?;
# Ok::<(), Box<dyn std::error::Error>>(())
Env var Used for
FOPOST_API_KEY The API key Client::from_env() reads

Keys are created in the dashboard under Settings → API Keys. A key carries only the scopes granted at creation and may be bound to a single workspace, in which case naming any other workspace answers 403.

Resources

Namespace Methods
posts() list, list_all, get, create, update, delete, duplicate, publish, retry, cancel, preflight, deliveries, publish_runs, analytics, bulk, validate_import, commit_import, rollback_import
accounts() list, get, create, delete, toggle_primary, validate, health, health_summary, refresh_token, analytics, communities, sync_communities, search_communities, add_community, remove_community
workspaces() list, get, create, update, delete, analytics
labels() list, get, create, update, delete
webhooks() list, create, update, delete, test
automations() list, get, create, update, delete, toggle, runs, get_run, stats, trigger
analytics() overview, time_series, top_posts, labels, posts_table, posting_streak, demographics, collect
media() list, upload, delete
inbox() list, threads, conversations, unread_count, accounts, platforms, mark_thread_read, refresh, update, reply, hide, unhide, delete, approvals, approve_reply, reject_reply
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

inbox() needs the inbox scope and ads() the ads scope; boost, create, set_status and delete on ads() spend money and also need publish. A boost or ad starts paused unless you pass .paused(false).

That is every endpoint the API documents. Anything not yet wrapped is reachable through client.request(method, path, query, body), which gets the same auth, retries, and error handling.

Error handling

Every call returns Result<T, fopost::Error>. A non-2xx response becomes Error::Api, carrying the status, the API's machine-readable code, and the whole body.

use fopost::Error;

# async fn run(client: fopost::Client) {
match client.posts().publish("9b2f6c1e-…", &Default::default()).await {
    Ok(outcome) => println!("{} deliveries queued", outcome.deliveries().len()),
    Err(Error::Api(err)) if err.is_payment_required() => {
        println!("Out of room on this plan — upgrade at {:?}", err.upgrade_url());
    }
    Err(Error::Api(err)) if err.is_rate_limited() => {
        println!("Rate limited, retry in {:?}s", err.retry_after);
    }
    Err(err) => eprintln!("{err}"),
}
# }

Requests are rate limited per key, per minute. A 429 is retried automatically, waiting for the interval the API asks for in Retry-After, up to max_retries attempts.

Publishing safely

publish in dry-run mode, and preflight, both report what would happen without sending anything:

use fopost::models::{PublishOptions, PublishOutcome};

# async fn run(client: fopost::Client, post_id: &str) -> Result<(), fopost::Error> {
// Hard blockers per account, plus advisory content signals.
let check = client.posts().preflight(post_id).await?;
if !check.ready {
    for account in &check.accounts {
        for issue in &account.issues {
            println!("{}: {issue}", account.platform.as_deref().unwrap_or("?"));
        }
    }
}

// Or run the publish path itself without anything leaving the building.
let outcome = client.posts().publish(post_id, &PublishOptions::new().dry_run()).await?;
assert!(matches!(outcome, PublishOutcome::DryRun(_)));
# Ok(()) }

Media

Media upload and bulk CSV import post multipart bodies, which the multipart feature covers. It is on by default.

use fopost::models::{ContentBlock, CreatePost, MediaItem, MediaType, MediaUpload};

# async fn run(client: fopost::Client, workspace_id: &str) -> Result<(), fopost::Error> {
let bytes = std::fs::read("card.png").unwrap();
let uploaded = client
    .media()
    .upload(workspace_id, [MediaUpload::new("card.png", "image/png", bytes)])
    .await?;

let block = ContentBlock::text("Ship it").with_media([MediaItem::new(
    MediaType::Image,
    &uploaded[0].name,
    &uploaded[0].url,
)
.alt("A product screenshot")]);

client.posts().create(&CreatePost::new(workspace_id, [block])).await?;
# Ok(()) }

Cargo features

Feature Default What it does
rustls-tls yes TLS through rustls, needing no system OpenSSL
native-tls no TLS through the platform's own stack
multipart yes Media upload and bulk CSV import

To use native-tls instead, turn the defaults off and name what you want back:

fopost = { version = "0.2", default-features = false, features = ["native-tls", "multipart"] }

Forward compatibility

Response types ignore fields they do not know, and every enum keeps an Other(String) variant, so a platform or status added server-side parses on an older SDK instead of failing the whole response.

use fopost::models::Platform;

assert_eq!(Platform::InstagramBusiness.as_str(), "instagram-business");
assert_eq!(Platform::from("brand-new"), Platform::Other("brand-new".into()));

Examples

export FOPOST_API_KEY=fp_...
cargo run --example create_post -- "Hello from the Rust SDK"
cargo run --example preflight -- <post-id>

Contributing

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

cargo fmt --check
cargo clippy --all-features --all-targets -- -D warnings
cargo test --all-features

Other SDKs

TypeScript (@fopost/sdk), Python (fopost), PHP, Laravel, and WordPress, plus ready-to-run API collections for Postman and Bruno.

License

MIT. See LICENSE.