Integration guide 9 min read

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.

Written and reviewed by the GSTIN API team Last reviewed

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

No. Since Java 11, java.net.http.HttpClient handles synchronous and asynchronous HTTP calls, including HTTPS, without pulling in Apache HttpClient or OkHttp. It is a reasonable default unless your project already standardises on something else.
Java records give you an immutable, constructor-validated data carrier with equals, hashCode and toString generated for free, which is exactly what a response DTO needs. Combined with Jackson’s record support, they deserialise JSON with almost no boilerplate.
Fire each lookup as a CompletableFuture backed by HttpClient’s async sendAsync method, collect the futures, and join them with CompletableFuture.allOf. This lets independent HTTP calls overlap instead of blocking one after another, without introducing a thread pool of your own beyond what HttpClient already manages.
Yes. The GSTIN’s 15th character is a check digit computed from the first 14 with a Luhn mod-36 algorithm. Validating it in Java before the HTTP call rejects malformed input for free and avoids spending a verification credit on a typo.
No. The check digit only confirms the string is well-formed. It does not prove the GSTIN was ever registered — only the live API call, which checks it against actual government records, can confirm that and return the current status.

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 tool

Build it into your product

One REST call, JSON back. Get an API key and 20 free verification credits in under a minute.

Start free