← Blog

Claude API Integration with Spring Boot: Full Guide

2026-09-28 · 4 min read · SubToAPI Team

Integrating Claude into a Spring Boot application means adding an HTTP client layer that talks to a messages API, handles authentication, parses JSON responses, and — if you want a good user experience — streams tokens back to the frontend. There's no official Anthropic Java SDK maintained at the same level as the Python or TypeScript ones, so most Spring Boot teams either call the REST API directly with WebClient or route through a gateway that gives them an OpenAPI-friendly HTTPS endpoint.

This guide covers both: a clean WebClient-based service you can drop into any Spring Boot 3 project, how to handle streaming responses with Reactor, and where a service like SubToAPI simplifies key management if you're building for a team rather than a single developer.

Project setup

You only need the standard reactive web starter — no extra SDK dependency required.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

If your app is otherwise a traditional MVC (servlet) app, that's fine — WebClient works outside of WebFlux too, you just call .block() when you need a synchronous result.

Configuration

Keep the API key and base URL in application.yml, injected from an environment variable:

claude:
  base-url: ${CLAUDE_BASE_URL}
  api-key: ${CLAUDE_API_KEY}
  model: claude-sonnet-4-5
@ConfigurationProperties(prefix = "claude")
public record ClaudeProperties(String baseUrl, String apiKey, String model) {}

Register it with @EnableConfigurationProperties(ClaudeProperties.class) on your main application class or a config class. Never hardcode the key — see our guide on securing Claude API keys if you haven't set up secret management yet.

Building the client service

@Service
public class ClaudeService {

    private final WebClient webClient;
    private final ClaudeProperties props;

    public ClaudeService(ClaudeProperties props) {
        this.props = props;
        this.webClient = WebClient.builder()
                .baseUrl(props.baseUrl())
                .defaultHeader("Authorization", "Bearer " + props.apiKey())
                .defaultHeader("Content-Type", "application/json")
                .build();
    }

    public Mono<ChatResponse> sendMessage(String userMessage) {
        var body = Map.of(
            "model", props.model(),
            "max_tokens", 1024,
            "messages", List.of(Map.of("role", "user", "content", userMessage))
        );

        return webClient.post()
                .uri("/v1/messages")
                .bodyValue(body)
                .retrieve()
                .bodyToMono(ChatResponse.class);
    }
}

The ChatResponse record maps whatever fields your provider returns — typically id, content, usage, and stop_reason. Define it as a simple record with @JsonIgnoreProperties(ignoreUnknown = true) so you don't break every time a new field is added upstream.

@JsonIgnoreProperties(ignoreUnknown = true)
public record ChatResponse(String id, List<ContentBlock> content, Usage usage) {
    public record ContentBlock(String type, String text) {}
    public record Usage(int inputTokens, int outputTokens) {}
}

Expose it through a controller:

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ClaudeService claudeService;

    public ChatController(ClaudeService claudeService) {
        this.claudeService = claudeService;
    }

    @PostMapping
    public Mono<ChatResponse> chat(@RequestBody ChatRequest request) {
        return claudeService.sendMessage(request.message());
    }
}

Streaming responses

For a chat UI, blocking until the full response arrives feels slow. Claude-compatible APIs support server-sent events when you set "stream": true. With WebClient, consume it as a Flux<String> and forward it directly to the browser:

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String message) {
    var body = Map.of(
        "model", props.model(),
        "max_tokens", 1024,
        "stream", true,
        "messages", List.of(Map.of("role", "user", "content", message))
    );

    return webClient.post()
            .uri("/v1/messages")
            .bodyValue(body)
            .retrieve()
            .bodyToFlux(String.class);
}

Spring's TEXT_EVENT_STREAM_VALUE handles SSE framing for you, and any EventSource client on the frontend can consume it without extra plumbing. If you're building a browser client, our streaming docs walk through the client-side event format in detail.

Error handling and retries

Wrap calls with onStatus to convert non-2xx responses into domain exceptions, and add retry logic for transient 429/5xx errors:

return webClient.post()
        .uri("/v1/messages")
        .bodyValue(body)
        .retrieve()
        .onStatus(HttpStatusCode::is4xxClientError, resp ->
            Mono.error(new ClaudeClientException(resp.statusCode().value())))
        .onStatus(HttpStatusCode::is5xxServerError, resp ->
            Mono.error(new ClaudeServerException(resp.statusCode().value())))
        .bodyToMono(ChatResponse.class)
        .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
            .filter(ex -> ex instanceof ClaudeServerException));

This pattern keeps retry logic out of your controllers and makes it easy to log usage metadata (token counts, latency) in one place for cost tracking.

Why teams route through SubToAPI instead of raw Anthropic keys

The code above works against any provider that speaks the same messages format. The part that gets harder as your team grows isn't the WebClient call — it's managing who has access to which key, tracking usage per service, and rotating credentials without redeploying every microservice.

SubToAPI turns your existing Claude access into an HTTPS API with per-application keys (sub_live_...), so each Spring Boot service — chat backend, background worker, internal tool — gets its own key with its own usage metadata, instead of one shared secret everyone has to know about. A quick test from your terminal:

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4-5",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Summarize this ticket."}]
  }'

Point your claude.base-url property at https://api.subtoapi.app/v1 and the ClaudeService shown above needs zero code changes — it's the same request/response shape. Streaming, tool use, and usage metadata all work the same way. Set it up from /signup, check /pricing for Solo, Team, and Scale plans, and see the request/response formats in full in /docs/messages and /docs/streaming.

If your Spring Boot app also calls tools (database lookups, internal APIs), /docs/tools covers the schema for defining and handling tool calls from Java without changing your controller layer.

Questions

Do I need the Anthropic Python or TypeScript SDK to use Claude from Spring Boot? No. The API is plain HTTPS with JSON bodies, so Spring's WebClient (or even RestTemplate) is enough — you don't need a language-specific SDK.

Should I use WebClient or RestTemplate for the integration? Use WebClient. It's non-blocking, supports streaming responses natively via Flux, and is the client Spring recommends going forward; RestTemplate is in maintenance mode.

How do I keep separate API keys for different microservices calling Claude? Issue a distinct key per service rather than sharing one secret. SubToAPI's dashboard lets you generate a sub_live_... key per application and see usage broken down by key — see /docs/quickstart for setup.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →