Java 11 ships with java.net.http.HttpClient, so calling a SERP API from Java needs one small dependency for JSON and nothing else. Here's a client class that posts a query, checks the API-level status, maps results to a record, and paginates — about eighty lines, no SDK.
The request you're making
POST https://api.serpbase.dev/google/search with an X-API-Key header and a JSON body: q is required; hl (language, defaults to en), gl (country, defaults to us), page (1-based, defaults to 1) and device (default / pc / mobile, search endpoint only) are optional. A successful request costs 1 credit.
Add Jackson for JSON handling:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
The client
package dev.serpbase.examples;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class SerpClient {
private static final String API_URL = "https://api.serpbase.dev/google/search";
private static final ObjectMapper MAPPER = new ObjectMapper();
private final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
private final String apiKey;
public SerpClient(String apiKey) {
this.apiKey = apiKey;
}
public record OrganicResult(int rank, String title, String link, String snippet) {}
public List<OrganicResult> searchPage(String query, int page) throws Exception {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("q", query);
payload.put("hl", "en");
payload.put("gl", "us");
payload.put("page", page);
payload.put("device", "default");
HttpRequest request = HttpRequest.newBuilder(URI.create(API_URL))
.header("X-API-Key", apiKey)
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IllegalStateException("HTTP " + response.statusCode() + ": " + response.body());
}
JsonNode root = MAPPER.readTree(response.body());
int status = root.path("status").asInt(-1);
if (status != 0) {
throw new IllegalStateException("status=" + status
+ " error=" + root.path("error").asText()
+ " request_id=" + root.path("request_id").asText());
}
List<OrganicResult> results = new ArrayList<>();
for (JsonNode item : root.path("organic")) {
results.add(new OrganicResult(
item.path("rank").asInt(),
item.path("title").asText(),
item.path("link").asText(),
item.path("snippet").asText("")));
}
return results;
}
}
The endpoint shape and field names above come from the SerpBase search API reference, which documents rank as the 1-based position within the response, with position as an alias, plus optional fields like url, date and sitelinks. I only map the four fields I need and let Jackson ignore the rest.
Two things to notice in the code:
-
There are two error layers. A non-200 HTTP status is a transport problem; anything else arrives in the JSON body as a numeric
status(0 means success). The client throws witherrorandrequest_idincluded, so logs are actionable. -
path()instead ofget(). Optional fields simply won't exist on some results;path()returns a missing node andasText("")gives you an empty string instead of an NPE.
Paginating
package dev.serpbase.examples;
public class Main {
public static void main(String[] args) throws Exception {
SerpClient client = new SerpClient(System.getenv("SERPBASE_API_KEY"));
for (int page = 1; page <= 2; page++) {
var results = client.searchPage("java httpclient example", page);
if (results.isEmpty()) {
break; // no more results
}
for (var r : results) {
System.out.printf("%d. %s%n %s%n", r.rank(), r.title(), r.link());
}
Thread.sleep(1000); // be polite between pages
}
}
}
Keep Main in the same package (dev.serpbase.examples) so it can see SerpClient.
What it costs to run
1 credit per successful request, and the response's credits_charged is the source of truth for billing. New accounts start with 100 free searches, which is plenty to validate the client. If you hit 1029 RATE_LIMITED, you're pushing QPS or concurrency too hard — keep the loop serial and sleep between pages.
FAQ
Why not OkHttp or a full SDK? HttpClient is in the JDK, and the API surface here is one POST. Fewer moving parts in a service that only needs search results.
How do I get the optional fields like date or sitelinks? Add them to the record and read them with path(); treat missing values as absent rather than defaulting to something misleading.
Can I use virtual threads for many queries? You can, but respect the documented rate limits — a Semaphore capping in-flight requests plus a small delay between batches keeps you clear of 1029.
Compile it, set SERPBASE_API_KEY, and run Main — the same class drops into a Spring service or a CLI unchanged. All the parameters used here are in the API reference linked above.
Top comments (0)