Java SDK

The official Java client for the FoPost API, on the JDK's own HTTP client.

com.fopost:fopost-java is the official Java client. It needs Java 17 or newer; HTTP goes through the JDK's own client, so the only dependency is Jackson.

<dependency>
  <groupId>com.fopost</groupId>
  <artifactId>fopost-java</artifactId>
  <version>0.1.0</version>
</dependency>
implementation("com.fopost:fopost-java:0.1.0")

This 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 Maven Central. Source and issues: github.com/fopost/fopost-java. MIT licensed.

Quick start

import com.fopost.sdk.FoPost;
import com.fopost.sdk.model.*;
import com.fopost.sdk.param.*;

FoPost client = FoPost.create("fp_...");   // or set FOPOST_API_KEY

Workspace workspace = client.workspaces().list().get(0);
List<Account> accounts = client.accounts().list(workspace.id());

Post post = client.posts().create(
        CreatePostParams.of(workspace.id())
                .content("Hello from Java")
                .accounts(accounts.stream().map(Account::id).toList()));

client.posts().publish(post.id());

publish returns once delivery is queued, not once it is live. Read posts().deliveries() or subscribe to webhooks for the result.

Threads and scheduling

A post is one or more content blocks. One block is a plain update; several make a thread:

client.posts().create(CreatePostParams.of(workspace.id())
        .accounts(accountId)
        .content("First post in the thread")
        .block(ContentBlockInput.text("Second one, with an image")
                .media(MediaItem.of("image", "chart.png", "https://.../chart.png"))));

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

client.posts().create(CreatePostParams.of(workspace.id())
        .accounts(accountId)
        .content("Scheduled with the SDK")
        .schedule(Instant.parse("2026-09-01T10:00:00Z")));

Before publishing, preflight reports the per-account blockers and advisory signals without sending anything, and publish with dryRun rehearses the whole thing:

PreflightResult check = client.posts().preflight(post.id());
if (!check.isReady()) {
    check.accounts().forEach(a -> System.out.println(a.platform() + ": " + a.issues()));
}

Pagination

list returns one page and iterates over its items. autoPaginate walks every page for you, fetching each one as you read it:

Page<Post> page = client.posts().list(
        PostListParams.create().workspaceId(workspace.id()).status(PostStatus.PUBLISHED).perPage(50));
System.out.println(page.meta().total() + " published posts");

for (Post post : client.posts().autoPaginate(PostListParams.create().workspaceId(workspace.id()))) {
    System.out.println(post.id());
}

long failed = client.posts().stream(PostListParams.create().workspaceId(workspace.id()))
        .filter(p -> PostStatus.FAILED.equals(p.status()))
        .count();

Media

Upload once, then attach the returned file to a content block:

UploadedMedia file = client.media().upload(workspace.id(), Path.of("chart.png")).get(0);

client.posts().create(CreatePostParams.of(workspace.id())
        .accounts(accountId)
        .block(ContentBlockInput.text("Numbers are in").media(file.toMediaItem())));

Webhooks

The signing secret is returned by the create call and never shown again, so store it then.

Webhook hook = client.webhooks().create(
        workspace.id(),
        "https://example.com/hooks/fopost",
        List.of(WebhookEvents.POST_PUBLISHED, WebhookEvents.DELIVERY_FAILED));

System.out.println(hook.secret());
client.webhooks().test(hook.id());

What is on the client

NamespaceMethods
posts()list, autoPaginate, stream, get, create, update, delete, duplicate, publish, retry, cancel, preflight, deliveries, publishRuns, analytics, bulkShift, bulkLabel, bulkDelete, validateImport, commitImport, rollbackImport
workspaces()list, get, create, update, delete, analytics
accounts()list, get, create, delete, healthSummary, health, togglePrimary, validate, refreshToken, analytics, communities()
labels()list, get, create, update, delete
webhooks()list, create, update, delete, test
analytics()overview, timeSeries, topPosts, labels, postsTable, postingStreak, demographics, collect
automations()list, get, create, update, delete, toggle, runs, run, trigger, stats
media()list, upload, delete
ai()credits, generateCaption, rewrite, repurposeUrl

accounts().communities() covers the X communities an account can post into: list, sync, search, add, remove.

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

JsonNode body = client.request("GET", "/v1/analytics/overview", null, Map.of("days", 30));

On ai(), only credits and generateCaption work with an API key today. rewrite and repurposeUrl reach endpoints that require a dashboard session and will answer 401. Use the composer for those until a later release.

Configuration

FoPost client = FoPost.builder()
        .apiKey("fp_...")                          // or FOPOST_API_KEY
        .baseUrl("https://api.fopost.com")         // override for a dev server
        .timeout(Duration.ofSeconds(30))
        .maxRetries(3)                             // total attempts on a 429
        .transport(myTransport)                    // bring your own HTTP stack
        .build();

Clients are immutable and safe to share across threads. A 429 is retried automatically, waiting for the interval the API asks for in Retry-After (capped at 60 seconds). maxRetries counts total attempts, so the default of 3 means two retries.

Errors

Every non-2xx response throws FoPostException or one of its subclasses, carrying the API's status, code, and message. They are unchecked, so nothing forces a try you did not want.

StatusException
400, 422ValidationException
401AuthenticationException
402PaymentRequiredException
403PermissionDeniedException
404NotFoundException
429RateLimitException
anything elseFoPostException
try {
    client.posts().publish(postId);
} catch (PaymentRequiredException e) {
    System.out.println("Upgrade at " + e.upgradeUrl());
} catch (RateLimitException e) {
    System.out.println("Rate limited, retry in " + e.retryAfter());
} catch (FoPostException e) {
    System.out.println("API " + e.status() + " (" + e.code() + "): " + e.getMessage());
}

A 403 where isSubscriptionRequired() is true means the workspace has no active subscription; read endpoints keep working without one.

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.

Was this helpful?

On this page