n4nAI

Calling the OpenAI API from Java with HttpClient

Hands-on tutorial: call the OpenAI API from Java with the standard HttpClient. Build a minimal chat client with JSON parsing, retries, and streaming.

n4n Team2 min read455 words

Audio narration

Coming soon — every post will get a voice note here.

You don’t need a heavyweight SDK to talk to LLMs. This tutorial shows how to call the java httpclient openai api directly using the standard library’s HttpClient, giving you full control over timeouts, retries, and request shaping. We’ll build a minimal chat completion client from scratch and print real responses.

Prerequisites

  • Java 17 or later (the java.net.http module is stable and ships with the JDK)
  • An OpenAI API key exported as OPENAI_API_KEY
  • Jackson Databind 2.17+ on the classpath for JSON mapping
  • Comfort with Java records, lambdas, and try-with-resources

No Spring, no OpenAI Java SDK. Just the JDK and one JSON library.

Project Setup

If you use Maven, add this to pom.xml:

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.17.1</version>
</dependency>

Gradle users:

implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'

Compile a single file with:

javac -cp jackson-databind-2.17.1.jar:. OpenAIClient.java
java -cp jackson-databind-2.17.1.jar:. OpenAIClient

Building the Request

The java httpclient openai api interaction is a plain HTTP POST to /v1/chat/completions. We construct the JSON body inline for clarity, then wrap it in a HttpRequest.

import java.net.http.*;
import java.net.URI;
import java.nio.charset.StandardCharsets;

public class OpenAIClient {
    static final String ENDPOINT = "https://api.openai.com/v1/chat/completions";
    static final String API_KEY = System.getenv("OPENAI_API_KEY");

    public static void main(String[] args) throws Exception {
        String body = """
        {
          "model": "gpt-4o-mini",
          "messages": [{"role": "user", "content": "Say hello in one word."}],
          "temperature": 0.2
        }
        """;

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(ENDPOINT))
            .header("Authorization", "Bearer " + API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
            .build();

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

Run it. Expected output (truncated):

200
{"id":"chatcmpl-abc123","object":"chat.completion","created":1710000000,"model":"gpt-4o-mini","choices":[{"index":0,"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop"}]}

That is the minimal java httpclient openai api integration. It works, but printing raw JSON is not production-ready.

Dynamic Request Bodies

Hard-coded JSON strings break fast. Use Jackson to serialize a Map so you can inject user input safely:

var payload = Map.of(
    "model", "gpt-4o-mini",
    "messages", java.util.List.of(
        Map.of("role", "user", "content", "Say hello in one word.")
    )
);
String body = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(payload);

This avoids manual escaping and lets you build message lists programmatically.

Parsing the Response

Define records that match the response subset you care about:

record Message(String role, String content) {}
record Choice(Message message, String finish_reason) {}
record ChatCompletion(String id, String model, java.util.List<Choice> choices) {}

Map and extract:

import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();
ChatCompletion completion = mapper.readValue(response.body(), ChatCompletion.class);
String answer = completion.choices().get(0).message().content();
System.out.println("Model " + completion.model() + " says: " + answer);

Output:

Model gpt-4o-mini says: Hello

Timeouts and Retries

HttpClient defaults to no connect timeout. Always set one:

HttpClient client = HttpClient.newBuilder()
    .connectTimeout(java.time.Duration.ofSeconds(10))
    .build();

For transient failures, implement a simple retry loop. OpenAI returns 429 (rate limit) and 5xx (upstream errors) that are worth retrying.

int maxRetries = 3;
HttpResponse<String> resp = null;
for (int i = 0; i < maxRetries; i++) {
    resp = client.send(request, HttpResponse.BodyHandlers.ofString());
    int code = resp.statusCode();
    if (code == 200) break;
    if (code == 401 || code == 400) throw new RuntimeException("Fatal: " + resp.body());
    long backoff = (long) Math.pow(2, i) * 1000;
    Thread.sleep(backoff);
}

In real systems, read the Retry-After header and add jitter. The java httpclient openai api call should fail fast on auth errors, not loop.

Streaming Responses

Non-streaming blocks until the model finishes. For chat UIs, stream tokens with Server-Sent Events. Set Accept: text/event-stream and stream: true in the body.

HttpRequest streamReq = HttpRequest.newBuilder()
    .uri(URI.create(ENDPOINT))
    .header("Authorization", "Bearer " + API_KEY)
    .header("Content-Type", "application/json")
    .header("Accept", "text/event-stream")
    .POST(HttpRequest.BodyPublishers.ofString("""
    {"model":"gpt-4o-mini","messages":[{"role":"user","content":"Count to 5."}],"stream":true}
    """))
    .build();

HttpResponse<java.io.InputStream> streamResp = client.send(streamReq, HttpResponse.BodyHandlers.ofInputStream());
ObjectMapper mapper = new ObjectMapper();
try (var reader = new java.io.BufferedReader(new java.io.InputStreamReader(streamResp.body()))) {
    reader.lines().forEach(line -> {
        if (!line.startsWith("data:")) return;
        String data = line.substring(5).trim();
        if (data.equals("[DONE]")) return;
        try {
            var node = mapper.readTree(data);
            var delta = node.path("choices").get(0).path("delta").path("content").asText();
            if (!delta.isEmpty()) System.out.print(delta);
        } catch (Exception e) { /* skip malformed frame */ }
    });
}

You will see tokens appear incrementally instead of one blocked response.

Using an OpenAI-Compatible Gateway

The same java httpclient openai api code works against any endpoint that speaks the OpenAI protocol. Swap ENDPOINT to https://api.n4n.ai/v1/chat/completions and the request shape stays identical. n4n.ai fronts 240+ models and automatically falls back when a provider is rate-limited, while metering per token and honoring your cache-control headers. That gives you resilience without writing your own fallback branching.

Production Notes

  • Store keys in env vars or a secret manager. Never embed them in source.
  • Prefer sendAsync for high concurrency; it returns CompletableFuture<HttpResponse<T>> and avoids thread-per-request blocking.
  • Map status codes explicitly: 401 means bad key, 429 means throttle, 5xx means upstream issue.
  • Always close the InputStream from streaming responses (the try-with-resources above handles it).
  • For structured output, add "response_format": {"type": "json_object"} to the body and validate the returned string.

The standard library client keeps your dependency tree small and your latency predictable. You now have a working Java LLM client without a vendor SDK.

Tagsjavahttpclientopenai-apitutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All java llm api integration posts →