Kotlin SDK

The official Kotlin client for the FoPost API, coroutines all the way down, on the JVM and on Android.

com.fopost:fopost-kotlin is the official Kotlin client. It targets JVM 11 and Android API 26 and up, transport is OkHttp, JSON is kotlinx-serialization, and the whole public API is suspend functions.

// build.gradle.kts
implementation("com.fopost:fopost-kotlin:0.1.0")
<dependency>
  <groupId>com.fopost</groupId>
  <artifactId>fopost-kotlin</artifactId>
  <version>0.1.0</version>
</dependency>

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-kotlin. MIT licensed.

Quick start

import com.fopost.FoPost
import com.fopost.param.CreatePostParams
import com.fopost.param.contentOf

FoPost("fp_...").use { client ->          // or set FOPOST_API_KEY
    val workspace = client.workspaces.list().first()
    val accounts = client.accounts.list(workspace.id)

    val post = client.posts.create(
        CreatePostParams(
            workspaceId = workspace.id!!,
            accounts = accounts.mapNotNull { it.id },
            content = contentOf("Hello from Kotlin"),
        ),
    )

    client.posts.publish(post.id!!)
}

FoPost owns a connection pool, so close() it when you are done; use { } does that for you. A client you pass your own OkHttpClient to leaves that client alone, because it is still yours to shut down. Instances are safe to share across coroutines, so build one and keep it.

Content

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

client.posts.create(
    CreatePostParams(
        workspaceId = workspaceId,
        accounts = listOf(accountId),
        content = listOf(
            ContentBlockInput("First post in the thread"),
            ContentBlockInput(
                "Second one, with an image",
                media = listOf(MediaItem(type = "image", name = "chart.png", url = "https://.../chart.png")),
            ),
        ),
    ),
)

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

val file = client.media.upload(workspaceId, File("chart.png")).first()

client.posts.create(
    CreatePostParams(
        workspaceId = workspaceId,
        accounts = listOf(accountId),
        content = listOf(ContentBlockInput("Numbers are in", media = listOf(file.toMediaItem()))),
    ),
)

Coroutines

Every call is a suspend function, so nothing blocks the thread it was started on. Call them from any coroutine scope:

val posts = coroutineScope {
    val drafts = async { client.posts.list(PostListParams(status = PostStatus.DRAFT)) }
    val scheduled = async { client.posts.list(PostListParams(status = PostStatus.SCHEDULED)) }
    drafts.await().data + scheduled.await().data
}

Cancelling the coroutine cancels the in-flight HTTP call, and a cancelled request is never retried. From blocking code, wrap the call in runBlocking.

Android

Works on API 26 and up. The SDK deliberately avoids java.net.http, which Android does not ship, and its timestamps use java.time, which arrived in API 26.

// AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
// build.gradle.kts
android {
    defaultConfig { minSdk = 26 }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
}

Keep the API key off the device. An app that ships a key ships it to everyone who installs it, so call FoPost from your own backend and let the app talk to that.

Scheduling

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(
        workspaceId = workspaceId,
        accounts = listOf(accountId),
        content = contentOf("Scheduled with the SDK"),
    ).scheduledAt(Instant.parse("2026-09-01T10:00:00Z")),
)

publish returns once delivery is queued, not once the post is live. Watch the deliveries or a webhook for that. Before publishing, preflight reports the per-account blockers and advisory signals without sending anything, and PublishParams(dryRun = true) rehearses the whole thing:

val check = client.posts.preflight(post.id!!)
if (check.ready != true) {
    check.accounts.forEach { println("${it.platform}: ${it.issues}") }
}

Pagination

list returns one page and iterates over its items. listAll is a Flow that walks every page, fetching each one as it is collected:

val page = client.posts.list(PostListParams(workspaceId = workspaceId, perPage = 50))
println("${page.meta?.total} posts")

client.posts.listAll(PostListParams(workspaceId = workspaceId))
    .filter { it.status == PostStatus.FAILED }
    .collect { println(it.id) }

Resources

NamespaceMethods
postslist, listAll, get, create, update, delete, duplicate, publish, retry, cancel, preflight, deliveries, publishRuns, analytics, bulkShift, bulkLabel, bulkDelete, validateImport, commitImport, rollbackImport
workspaceslist, get, create, update, delete, analytics
accountslist, get, create, delete, healthSummary, health, togglePrimary, validate, refreshToken, analytics
communitieslist, sync, search, add, remove
labelslist, get, create, update, delete
webhookslist, create, update, delete, test
analyticsoverview, timeSeries, topPosts, labels, postsTable, postingStreak, demographics, collect
automationslist, get, create, update, delete, toggle, runs, run, trigger, stats
medialist, upload, delete

For an endpoint the SDK does not wrap yet, request sends an authenticated call and hands back the raw body; requestAs decodes the data payload into a type of yours:

val raw = client.request("GET", "/analytics/overview", query = mapOf("days" to 30))
val platforms: List<String> = client.requestAs("GET", "/platforms")

Webhooks

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

val hook = client.webhooks.create(
    workspaceId,
    "https://example.com/hooks/fopost",
    listOf(WebhookEvents.POST_PUBLISHED, WebhookEvents.DELIVERY_FAILED),
)

println(hook.secret)
client.webhooks.test(hook.id!!)

Configuration

val client = FoPost(
    apiKey = "fp_...",                       // or FOPOST_API_KEY
    baseUrl = "https://api.fopost.com/v1",   // or FOPOST_BASE_URL
    maxRetries = 3,                          // total attempts, so 3 means two retries
    timeout = 30.seconds,
    httpClient = myOkHttpClient,             // optional: your own transport
    userAgent = "acme/2.0",                  // optional: prefixed to the SDK's own
)

Three attempts by default. A 429, a 5xx, and a connection failure are retried; a 4xx other than 429 is not, because retrying it would fail the same way. Backoff is exponential from 500 ms and capped at 60 seconds, and a 429 waits for the interval the API asks for in Retry-After instead. A cancelled request is never retried.

Errors

Every non-2xx response throws a subclass of FoPostException, carrying the API's status, code, message, and raw body.

try {
    client.posts.publish(postId)
} catch (e: PaymentRequiredException) {
    println("Out of credits, upgrade at ${e.upgradeUrl}")
} catch (e: RateLimitException) {
    println("Rate limited, retry in ${e.retryAfter}")
} catch (e: FoPostException) {
    println("API ${e.status} (${e.code}): ${e.message}")
}
StatusException
400, 422ValidationException
401AuthenticationException
402PaymentRequiredException
403PermissionDeniedException
404NotFoundException
429RateLimitException
5xxServerException
otherApiException
no replyTransportException

rateLimit on any of them carries the rate-limit headers that came with the response, and field(name) reads an extra field the error body carried.

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