Java subtitle generator API: burn captions with HttpClient
Generate subtitles from Java with the JDK HttpClient: POST the video URL to a captions API, poll the job, then read the captioned video_url.

To generate subtitles from Java, you don't need a Java subtitle library: call a captions API over HTTPS with the JDK's java.net.http.HttpClient. With Sume, POST the video's public URL to https://api.sume.com/v1/video-captions, poll the job's status_url until terminal is true, then read GET /v1/video-captions/{id} for the captioned video_url. Sume transcribes the speech and burns the words into the picture.
Sume facts come from the Video captions and Jobs and results docs and the Sume API reference. Java behavior comes from the Java SE 21 pages for HttpClient and HttpRequest.Builder, and Jackson's databind README. All were read on 2026-09-29. Anything called current behavior is read from Sume's code. The same flow in Python is How to add subtitles to a video in Python.
What do I need before I start?
- Java 11 or newer:
HttpClienthas been in the JDK since 11. Sume's SDK is a TypeScript client, and everything it does is also reachable over plain HTTP, so Java calls the API directly. - A JSON library. The code uses Jackson's
jackson-databind, whose README says to create oneObjectMapperand reuse it. - Your API key in the
SUME_API_KEYenvironment variable of a server. Sume's docs say not to place keys in frontend JavaScript or mobile apps. - The video at a public HTTPS URL. Localhost, private-network, non-HTTPS, and signed or private URLs are rejected.
- In current code, a source of 60 seconds or less that has an audio stream.
How do I send the video from Java?
Build one HttpClient and reuse it: the JDK docs say a built client is immutable and can send multiple requests. Set a timeout on each request, because not setting one is the same as an infinite wait. The Idempotency-Key makes a retry of the same body return the original job instead of billing a second one.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.Map;
public class SumeCaptions {
static final String API = "https://api.sume.com/v1";
static final HttpClient HTTP = HttpClient.newHttpClient();
static final ObjectMapper JSON = new ObjectMapper(); // create once, reuse
static JsonNode call(HttpRequest.Builder req) throws Exception {
HttpResponse<String> res = HTTP.send(req.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("SUME_API_KEY")).build(),
HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 300) throw new IllegalStateException(res.statusCode() + " " + res.body());
return JSON.readTree(res.body()).get("data");
}
static JsonNode submit(String videoUrl, String key) throws Exception {
String body = JSON.writeValueAsString(Map.of("video_url", videoUrl, "language", "en"));
return call(HttpRequest.newBuilder(URI.create(API + "/video-captions"))
.header("Content-Type", "application/json").header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(body)));
}How do I wait for the captioned video?
Poll status_url, sleeping for next_poll_after_seconds between reads, until terminal is true. A client-side timeout does not cancel the job: it keeps running and still bills, so keep the id to resume from. Then read the caption resource and check its status.
| Step | Request | Fields the code reads |
|---|---|---|
| Submit | POST /v1/video-captions with video_url and an Idempotency-Key header | status_url, video_caption_id |
| Poll | GET the status_url | terminal, next_poll_after_seconds |
| Read | GET /v1/video-captions/{id} | status, video_url, error |
public static void main(String[] args) throws Exception {
JsonNode job = submit("https://example.com/clip.mp4", "clip-captions-001");
URI statusUrl = URI.create(job.get("status_url").asText());
JsonNode status = job;
do {
Thread.sleep(1000L * Math.max(1, status.path("next_poll_after_seconds").asInt(5)));
status = call(HttpRequest.newBuilder(statusUrl));
} while (!status.get("terminal").asBoolean());
JsonNode caption = call(HttpRequest.newBuilder(
URI.create(API + "/video-captions/" + job.get("video_caption_id").asText())));
if (!"completed".equals(caption.get("status").asText()))
throw new IllegalStateException(caption.get("error").toString());
System.out.println(caption.get("video_url").asText());
}
}Can I control the wording and the look?
languageis only a speech-to-text hint (en,ko, …); omit it for automatic detection.script_textkeeps the speech-to-text timings and aligns the burned wording to your script. Alignment can fail withscript_alignment_mismatchorscript_alignment_failed.cues(text,start,endin seconds) burn your own lines and skip speech-to-text. Sume takes no .srt upload, so turn SRT blocks into cues first.stylepicks the look, such asslam,punch, ortiktok-green; customize burned-in captions covers the options.
Can I get an SRT file instead of burned-in subtitles?
Not from the caption job: it returns a captioned video, and raw transcripts are not part of its public contract. For a sidecar file, transcribe with STT 1.0 (POST /v1/stt-1.0/transcribe) and segmentation.mode: "sentence", which groups the timed words into sentences, then write each segment as a numbered SRT block yourself. Speech to text API in Java has the Java call, and generate an SRT file from a video covers the format.
What are the limits, and what does it cost?
- Each accepted caption job reserves and captures $0.20 for videos up to 60 seconds, plus a 5.5% agent fee by default. Speech to text for an SRT is $0.01 per audio minute.
- In current code the caption job refuses a source over 60 seconds or one with no audio stream, even when you send cues. Split a longer video first.
- A clip with no audible speech fails as
caption_no_speech. - Script alignment is described for English or Korean voiceover; don't count on other languages for
script_text.
Sources
Related posts
More in Developers
- 503 provider_capacity_exceeded: same idempotency key or new?
A Sume 503 provider_capacity_exceeded means the provider queue is full. Docs say retry later with the same key; code replays the refusal, so use a new key.
- Which field do I branch on in a Sume API error: code or next_action?
Branch on the HTTP status, then on error.code. Use retryable, retry_after_seconds and next_action to decide on a resend. Never match on message.
- Which model did my AI video use? sume/auto does not say
Sume never discloses which family ran a sume/auto video: the response echoes sume/auto. What the docs say, why not to infer it, and how to pin a model instead.
- API sends both Authorization Bearer and x-api-key: 401
Sume rejects a request that carries both Authorization: Bearer and x-api-key with 401 and 'Send only one API key credential.' Neither header wins. Fix it.
Written by Sume