How to Integrate a GST Verification API in Java
A Java integration tutorial for GSTIN verification using the built-in HttpClient, records, and proper error handling.
To call a GST verification API from Java, use the built-in java.net.http.HttpClient, model the
JSON response as immutable record types, and deserialise with Jackson — no Apache HttpClient or
Retrofit required. This guide covers a synchronous client, an async batch lookup built on
CompletableFuture, and local GSTIN checksum validation so you do not spend a credit on
malformed input.
Why java.net.http.HttpClient
java.net.http.HttpClient, introduced as a stable API in Java 11, supports HTTP/1.1 and HTTP/2,
connection reuse, and both blocking and asynchronous request styles out of the box. For a single GET
endpoint like GET https://gstinapi.com/api/get-taxpayer-info/{gstin}, authenticated with an
X-API-Key header, it removes any reason to add Apache HttpClient or OkHttp purely for this
integration — though either works fine if your project already depends on one.
Validate the GSTIN before calling the API
Every account gets 20 free verification credits, and each successful lookup consumes one. Validating a GSTIN’s structure and check digit locally, before the HTTP call, catches malformed input without touching the network. The algorithm is a Luhn mod-36 variant applied to the first 14 characters, checked against the 15th.
public final class GstinValidator {
private static final String CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private GstinValidator() {}
public static boolean isChecksumValid(String gstin) {
if (gstin == null || gstin.length() != 15) {
return false;
}
String upper = gstin.toUpperCase();
String body = upper.substring(0, 14);
char checkChar = upper.charAt(14);
int sum = 0;
for (int i = 0; i < 14; i++) {
int value = CHARSET.indexOf(body.charAt(i));
if (value < 0) {
return false;
}
int factor = (i % 2 == 0) ? 1 : 2;
int product = value * factor;
sum += (product / 36) + (product % 36);
}
char expected = CHARSET.charAt((36 - (sum % 36)) % 36);
return expected == checkChar;
}
}
Modelling the response as records
The live API returns legal name, trade name, status, registration and cancellation dates, constitution,
taxpayer type, jurisdiction, nature of business activities, e-invoicing status, principal and additional
places of business, and the full GSTR filing history with per-return ARN and timeliness. A
record is a clean way to represent this in Java — immutable, with generated accessors, and no
boilerplate getters to maintain.
public record TaxpayerInfo(
String gstin,
String legalName,
String tradeName,
String status,
String registrationDate,
String cancellationDate,
String constitution,
String taxpayerType
) {}
Deserialising with Jackson
Jackson has supported records natively since 2.12, so a single ObjectMapper can deserialise
straight into TaxpayerInfo without a custom deserialiser, as long as the JSON field names line
up (use @JsonProperty on individual record components if the API's field naming does not
match your Java naming convention).
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
TaxpayerInfo info = mapper.readValue(responseBody, TaxpayerInfo.class);
A synchronous client
Wrap the request, header, and JSON parsing in a small client class so the rest of the application calls one method rather than repeating the HTTP boilerplate at every call site.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public final class GstinClient {
private static final String BASE_URL = "https://gstinapi.com/api";
private final HttpClient httpClient;
private final ObjectMapper mapper = new ObjectMapper();
private final String apiKey;
public GstinClient(String apiKey) {
this.apiKey = apiKey;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
}
public TaxpayerInfo getTaxpayerInfo(String gstin) throws Exception {
if (!GstinValidator.isChecksumValid(gstin)) {
throw new IllegalArgumentException(gstin + " is not a structurally valid GSTIN");
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/get-taxpayer-info/" + gstin))
.header("X-API-Key", apiKey)
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new RuntimeException("GST verification API returned HTTP " + response.statusCode());
}
return mapper.readValue(response.body(), TaxpayerInfo.class);
}
}
Reliance Industries’ public GSTIN, 27AAACR5055K1Z7, returns 354 filing records and 244
additional places of business in its real response — a good payload to test your parsing and any UI you
build against before rolling this out to smaller taxpayers.
Configuring the client for production use
The example above builds a fresh HttpClient in the constructor, which is fine for a
demonstration but worth revisiting in a real service. HttpClient instances are designed to be
reused across many requests — they hold their own connection pool — so in a Spring Boot or Micronaut
application it typically makes more sense to construct one HttpClient as a singleton bean and
inject it into GstinClient, rather than let every instantiation of the client build its own.
It is also worth setting an explicit executor on the client if your application already manages its own thread pools, so async GST verification calls do not compete for threads with unrelated work elsewhere in the same JVM.
An async batch lookup with CompletableFuture
When you need to verify a list of GSTINs — a vendor master import, for example — issuing requests one after
another wastes time waiting on network round trips. HttpClient.sendAsync returns a
CompletableFuture<HttpResponse<String>>, so you can fire every request, then wait on
all of them together.
import java.net.http.HttpResponse.BodyHandlers;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public List<TaxpayerInfo> getTaxpayerInfoBatch(List<String> gstins) {
List<CompletableFuture<TaxpayerInfo>> futures = gstins.stream()
.filter(GstinValidator::isChecksumValid)
.map(gstin -> {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/get-taxpayer-info/" + gstin))
.header("X-API-Key", apiKey)
.GET()
.build();
return httpClient.sendAsync(request, BodyHandlers.ofString())
.thenApply(response -> {
try {
return mapper.readValue(response.body(), TaxpayerInfo.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
})
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream().map(CompletableFuture::join).collect(Collectors.toList()))
.join();
}
Handling errors
Distinguish HTTP status codes rather than catching one broad exception: a 401 means the
X-API-Key header is missing or wrong, a 404 typically means the GSTIN was not found in the
underlying registry, and a 429 means you have hit a rate limit and should back off before retrying, not
retry immediately. See the API error codes guide for the full
list, and the API documentation for the complete response schema, including the GSTR-1,
GSTR-3B, GSTR-2B and IMS filing details returned per period.
If your services span more than one runtime, the request and response shape here matches our Python, PHP, and .NET/C# guides — only the client code differs. See pricing for credit packs beyond the free 20, and features for the full field list this API returns.
Caching, secrets, and timeouts
Load the API key from an environment variable or a secrets manager, not a constant in source. Set an
explicit connectTimeout on the HttpClient builder and a per-request
timeout(), as shown above, rather than relying on defaults — a hung upstream connection should
fail fast, not block a thread indefinitely, particularly in the async batch path where many requests share
the same underlying client.
Registration status, filing history, and e-invoicing eligibility do not change minute to minute, so a short
cache in front of getTaxpayerInfo — Caffeine, or whatever your application already uses — is a
reasonable default for any GSTIN you look up more than once, such as a vendor checked on every invoice. It
reduces both latency and credit spend without materially affecting accuracy.
Frequently asked questions
A note on accuracy. GST rules change often. This article reflects our understanding as of 30 August 2026 and is general information, not tax or legal advice. For the authoritative position, check gst.gov.in and cbic-gst.gov.in, or speak to a qualified tax professional about your specific situation.
Check a GSTIN right now, free
Five lookups a day, no account required. Status, filing history, e-invoicing and jurisdiction.
Free GST search toolBuild it into your product
One REST call, JSON back. Get an API key and 20 free verification credits in under a minute.
Start freeMore on developer integration guides
Browse every guide in the GST API blog.