Back to blog

Calling Floggy's Headless CMS API and CLI from Java

floggy7 min read

Why 'headless CMS with an API and CLI' results skew JavaScript, and where that leaves a Java stack

Search for a headless CMS with an API and a CLI and most results assume you're calling it from Node. The docs show fetch calls, the SDKs ship as npm packages, and the CLI examples pipe into other JS tools. If your backend is Java, that's not wrong information, it's just not written for you.

Floggy doesn't have a Java SDK. It doesn't need one. The API is plain REST over HTTPS, the CLI is a standalone binary, and neither cares what language called it. This post covers the parts a Java service actually needs: building the auth header, making the HTTP calls, parsing the JSON, and running the CLI as a shell step in a Java-centric pipeline.

What you get from Floggy's REST API without a JS SDK in the way

Floggy's API lives at a fixed base URL, https://floggy-api.pehcastro.workers.dev, and every endpoint returns JSON. There's nothing here that requires a JavaScript runtime: no client-side hydration, no build step, no package to install. You send an HTTP request with java.net.http.HttpClient (built into the JDK since 11) or a library like OkHttp, and you get a JSON body back.

The API splits into two halves. Public read endpoints (listing posts, fetching a single post by slug, searching, listing tags) work with no authentication at all, namespaced by your Floggy project username: GET /api/posts/by-username/:project. Admin endpoints (creating, updating, deleting posts, and everything under custom collections) require a Bearer API key. That's the whole surface area a Java client needs to know about before writing code.

Authenticating and calling the API from a Java service

Generate an API key in the Floggy dashboard under Settings -> Developer, and pick the scopes it needs (posts:read, posts:write, posts:delete, collections:read, collections:write, collections:delete, or * for everything). The key is shown once, formatted flg_ followed by 32 characters. Store it in an environment variable, conventionally FLOGGY_API_KEY, and never hardcode it.

Every authenticated request needs an Authorization: Bearer flg_... header. If the key is missing a required scope, the API returns 403 with a body like {"error":"Insufficient scope","required":["posts:write"]}, so a caught 403 usually means a scope problem, not a bad key.

Here's a minimal Java client using HttpClient, no external dependency:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class FloggyClient {
    private static final String BASE_URL = "https://floggy-api.pehcastro.workers.dev";
    private final HttpClient client = HttpClient.newHttpClient();
    private final String apiKey = System.getenv("FLOGGY_API_KEY");

    public HttpResponse<String> get(String path) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE_URL + path))
                .header("Authorization", "Bearer " + apiKey)
                .GET()
                .build();
        return client.send(request, HttpResponse.BodyHandlers.ofString());
    }
}

For deserializing the JSON responses, Jackson or org.json both handle this without issue since the API returns plain, unnested JSON for most calls. Rate limits are 300 requests per minute per API key, and a 429 comes back with {"error": "rate_limited", "retryAfter": 60}, so a retry-with-backoff wrapper around the send call is worth adding if you're doing bulk work.

Creating and publishing a post from Java

Creating a post is a POST /api/posts call with the posts:write scope. The request body needs at minimum a title and a slug (matching ^[a-z0-9-]+$), plus content as Tiptap JSON. Optional fields include excerpt, coverImage, visibility, status, tagNames, publishedAt, coauthors, subtitle, featured, and SEO overrides.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class CreatePost {
    public static void main(String[] args) throws Exception {
        String apiKey = System.getenv("FLOGGY_API_KEY");
        String body = """
            {
              \"title\": \"Deploying from a Java CI pipeline\",
              \"slug\": \"deploying-from-a-java-ci-pipeline\",
              \"content\": { \"type\": \"doc\", \"content\": [] },
              \"status\": \"published\",
              \"tagNames\": [\"java\", \"ci\"]
            }
            """;

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://floggy-api.pehcastro.workers.dev/api/posts"))
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}

A successful create returns 201 with {"id": "<postId>"}. Updating that post later is a PATCH /api/posts/:id with the same optional fields, returning {"success": true}. If you're publishing in bulk, POST /api/posts/bulk takes {"ids": [...], "action": "publish"} and returns the count of posts affected, which is the call to reach for once you're generating more than a handful of posts per run.

Working with custom collections for programmatic content from a non-JS backend

Custom collections are where Floggy stops being just a blog and becomes a general content store, and they're built for exactly the case of a Java job generating structured records on a schedule. A collection has a schema and a set of entries, each with its own data payload, versioning, and labels.

Listing collections is GET /api/collections with collections:read. Creating entries is POST /api/collections/:id/entries with collections:write, body {"data": {...}, "label": "optional-label"}. Here's a Java snippet that pulls a collection's entries and creates a new one:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class CollectionEntries {
    static final String BASE = "https://floggy-api.pehcastro.workers.dev";
    static final HttpClient client = HttpClient.newHttpClient();
    static final String apiKey = System.getenv("FLOGGY_API_KEY");

    static HttpResponse<String> listEntries(String collectionId) throws Exception {
        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(BASE + "/api/collections/" + collectionId + "/entries"))
                .header("Authorization", "Bearer " + apiKey)
                .GET().build();
        return client.send(req, HttpResponse.BodyHandlers.ofString());
    }

    static HttpResponse<String> createEntry(String collectionId, String jsonData) throws Exception {
        String body = "{\"data\": " + jsonData + "}";
        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(BASE + "/api/collections/" + collectionId + "/entries"))
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build();
        return client.send(req, HttpResponse.BodyHandlers.ofString());
    }
}

List responses come back as {"entries": [...], "pageInfo": {"total": ..., "page": ..., "perPage": ..., "hasMore": ...}}, so paginate on hasMore if a scheduled job is walking a large collection. For batch generation, POST /api/collections/:id/entries/bulk accepts up to 50 creates, updates, or deletes in one call, which matters if a Java job is generating dozens of programmatic SEO pages worth of structured data in one run rather than one entry per request.

Running the CLI from a CI pipeline or a scheduled Java job

The floggy CLI is a standalone binary, so it doesn't matter that your build tooling is Maven or Gradle rather than npm. Install it once on the CI runner or the job's host, authenticate with export FLOGGY_API_KEY=flg_... (or floggy login interactively), and call it as a shell step.

In a GitHub Actions workflow building a Java project, the CLI step sits alongside the build step, not inside it:

- name: Build
  run: mvn -B package

- name: Publish collection entries
  env:
    FLOGGY_API_KEY: ${{ secrets.FLOGGY_API_KEY }}
  run: floggy collections push product-pages ./generated --publish

The same pattern works from a Jenkins pipeline's sh step, or from a cron job or a Quartz-scheduled job in a Java service that shells out to floggy after writing generated JSON files to a directory. floggy collections push <collection> <dir> picks up files from that directory and pushes them as entries; add --publish to publish rather than draft. floggy collections diff <collection> <dir> is worth running first in CI to catch unintended changes before they publish. Read commands accept --json for piping into jq, and upload commands print URLs to stdout and status to stderr, so a Java process invoking the CLI via ProcessBuilder can capture the two streams separately.

Where this fits for agencies and dev teams running programmatic SEO on a mixed stack

If your orchestration layer is Java, a headless CMS being JS-first usually means picking between rewriting your pipeline in Node or fighting an SDK that doesn't fit your stack. Floggy avoids that trade because the API and CLI are both language-agnostic: the API is REST and JSON, and the CLI is a binary that runs the same from a cron job, a Jenkins agent, or a GitHub Actions runner regardless of what language triggered it.

For an agency or dev team running Floggy as a base CMS for programmatic content across a mixed stack, that means the content generation logic can live in Java (or wherever the rest of the system already lives) while Floggy handles storage, versioning, drafts, and publishing through the same collections and posts API a JS client would use. The Java side just needs an HTTP client and an API key.

Calling Floggy's Headless CMS API and CLI from Java - Floggy