Integration guide 9 min read

GST Verification API Integration in .NET and C#

Integrate GSTIN verification into a .NET or C# app with HttpClient, typed DTOs, dependency injection and Polly retries.

Written and reviewed by the GSTIN API team Last reviewed

Integrate the GST verification API in .NET or C# with a typed HttpClient registered through IHttpClientFactory, source-generated System.Text.Json serialisation for the DTOs, and Polly for retries. This guide builds that client end to end, including local GSTIN checksum validation and a streaming bulk-lookup method built on IAsyncEnumerable.

Why a typed HttpClient over a bare HttpClient

A raw new HttpClient() per request is a well-known way to exhaust sockets under load in .NET, and a single long-lived static instance does not pick up DNS changes. IHttpClientFactory solves both problems and, more usefully here, lets you register a named or typed client with its base address, default headers, and resilience policies configured once in Program.cs rather than repeated at every call site.

The API itself is a single endpoint — GET https://gstinapi.com/api/get-taxpayer-info/{gstin}, authenticated with an X-API-Key header — so most of what follows is about making the client resilient and idiomatic, not about the call itself.

Validate the GSTIN before calling the API

Every account starts with 20 free verification credits, and each successful lookup uses one. Validating a GSTIN’s 15-character structure and Luhn mod-36 check digit in C#, before the HTTP call, rejects malformed input without spending a credit.


public static class GstinValidator
{
    private const string Charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    public static bool IsChecksumValid(string gstin)
    {
        if (string.IsNullOrEmpty(gstin) || gstin.Length != 15)
        {
            return false;
        }

        var upper = gstin.ToUpperInvariant();
        var body = upper[..14];
        var checkChar = upper[14];
        var sum = 0;

        for (var i = 0; i < 14; i++)
        {
            var value = Charset.IndexOf(body[i]);
            if (value < 0)
            {
                return false;
            }

            var factor = i % 2 == 0 ? 1 : 2;
            var product = value * factor;
            sum += product / 36 + product % 36;
        }

        var expected = Charset[(36 - sum % 36) % 36];
        return expected == checkChar;
    }
}

DTOs with source-generated JSON

Model the response as an immutable record, and pair it with a JsonSerializerContext so System.Text.Json generates the serialisation code at compile time instead of using reflection. This is faster and plays well with native AOT and trimmed container images.


public record TaxpayerInfo(
    string Gstin,
    string LegalName,
    string? TradeName,
    string Status,
    string RegistrationDate,
    string? CancellationDate,
    string Constitution,
    string TaxpayerType
);

[JsonSerializable(typeof(TaxpayerInfo))]
public partial class GstinJsonContext : JsonSerializerContext
{
}

A typed client registered in DI

Register the client once in Program.cs with its base address, default header, and timeout, then inject GstinClient wherever it is needed instead of resolving HttpClient directly.


public class GstinClient
{
    private readonly HttpClient _http;

    public GstinClient(HttpClient http)
    {
        _http = http;
    }

    public async Task<TaxpayerInfo> GetTaxpayerInfoAsync(string gstin, CancellationToken ct = default)
    {
        if (!GstinValidator.IsChecksumValid(gstin))
        {
            throw new ArgumentException($"'{gstin}' is not a structurally valid GSTIN", nameof(gstin));
        }

        using var response = await _http.GetAsync($"get-taxpayer-info/{gstin}", ct);
        response.EnsureSuccessStatusCode();

        var stream = await response.Content.ReadAsStreamAsync(ct);
        return await JsonSerializer.DeserializeAsync(
            stream, GstinJsonContext.Default.TaxpayerInfo, ct)
            ?? throw new InvalidOperationException("Empty response body");
    }
}

// Program.cs
builder.Services.AddHttpClient<GstinClient>(client =>
{
    client.BaseAddress = new Uri("https://gstinapi.com/api/");
    client.DefaultRequestHeaders.Add("X-API-Key", builder.Configuration["GstinApi:ApiKey"]);
    client.Timeout = TimeSpan.FromSeconds(10);
});

Reliance Industries’ public GSTIN, 27AAACR5055K1Z7, is a useful one to test against manually — its real response returns 354 filing records and 244 additional places of business, so it exercises the deserialiser and any UI you build against a large payload before rolling this out more broadly.

Retries with Polly

Attach a Polly resilience handler to the same typed-client registration so transient failures are retried with backoff, while genuine client errors fail fast. Since .NET 8, Microsoft.Extensions.Http.Resilience wraps Polly with sensible defaults, or you can configure Polly directly for finer control.


builder.Services.AddHttpClient<GstinClient>(client =>
{
    client.BaseAddress = new Uri("https://gstinapi.com/api/");
    client.DefaultRequestHeaders.Add("X-API-Key", builder.Configuration["GstinApi:ApiKey"]);
    client.Timeout = TimeSpan.FromSeconds(10);
})
.AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(
    retryCount: 3,
    sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(500 * Math.Pow(2, attempt)),
    onRetry: (outcome, delay, attempt, _) =>
    {
        // Log the retry attempt and status code here.
    }));

AddTransientHttpErrorPolicy already scopes retries to HttpRequestException, request timeouts, and 5xx responses by default — extend the predicate if you also want to retry 429 specifically, but never retry a 401 or 400, since those mean the request itself needs to change.

Testing without hitting the live API

Because GstinClient depends on HttpClient rather than constructing one internally, it is straightforward to unit test with a fake HttpMessageHandler that returns a canned response body instead of making a real network call. This avoids spending verification credits in CI and keeps the test suite deterministic — assert on how GstinClient parses a given payload, not on what the live API happens to return today.

For integration-level confidence beyond unit tests, keep a small allowance of real calls against a known GSTIN — Reliance Industries’ 27AAACR5055K1Z7 is a reasonable choice since its status is stable — and run those sparingly, such as in a nightly pipeline rather than on every commit.

Bulk lookups with IAsyncEnumerable

For verifying a batch of GSTINs — a vendor master import, for example — return results as an IAsyncEnumerable<TaxpayerInfo> so the caller can process each result as it arrives instead of waiting for the entire batch to finish.


public async IAsyncEnumerable<TaxpayerInfo> GetTaxpayerInfoBulkAsync(
    IEnumerable<string> gstins,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    foreach (var gstin in gstins.Where(GstinValidator.IsChecksumValid))
    {
        TaxpayerInfo? info = null;

        try
        {
            info = await GetTaxpayerInfoAsync(gstin, ct);
        }
        catch (HttpRequestException)
        {
            // Log and skip; do not let one failed GSTIN stop the whole batch.
        }

        if (info is not null)
        {
            yield return info;
        }
    }
}

Consume it with await foreach in the caller, and pace or chunk the underlying calls rather than firing all of them at once — see the bulk GSTIN verification guide for patterns that apply across languages.

Handling errors

Map HTTP status codes to explicit outcomes rather than relying on EnsureSuccessStatusCode throwing a generic exception everywhere: a 401 means the X-API-Key header is missing or wrong, a 404 usually means the GSTIN is not found in the underlying registry, and a 429 means you have hit a rate limit. See the API error codes guide for the full list and the API documentation for the complete response schema, including GSTR-1, GSTR-3B, GSTR-2B and IMS filing history per return period.

ConcernWhere it lives
Connection poolingIHttpClientFactory
Retries and backoffPolly, scoped to transient failures only
SerialisationSystem.Text.Json with a source-generated JsonSerializerContext
Bulk streamingIAsyncEnumerable consumed with await foreach
Resilience responsibilities by layer.

The request and response shape here is identical across languages — see our Python, PHP, and Java guides if other services in your stack need the same integration. Check pricing for credit packs beyond the free 20, and features for the full response schema.

Caching, secrets, and configuration

Store the API key in user secrets locally and in your platform’s configuration or secret store in production — never in appsettings.json committed to source control. The typed-client registration above already reads it from IConfiguration, which keeps it out of application code entirely.

Because registration status, filing history, and e-invoicing eligibility do not change from one minute to the next, wrapping GstinClient with IMemoryCache or a distributed cache for a few hours per GSTIN is a sensible default for any workload that re-checks the same vendors repeatedly. It cuts both latency and verification-credit spend without a meaningful loss of freshness for this kind of data.

Frequently asked questions

Creating a new HttpClient per request can exhaust sockets under load, and a single static instance can serve stale DNS. IHttpClientFactory manages the underlying HttpMessageHandler pool for you and lets you register a typed client with its own base address, headers, and Polly policies through dependency injection.
System.Text.Json can generate serialisation code at compile time via a JsonSerializerContext, instead of using reflection at runtime. It is faster, trims better for native AOT and container images, and catches DTO mismatches at build time rather than at runtime.
Use Polly’s WaitAndRetryAsync with exponential backoff, scoped to transient failures — HTTP request exceptions and 429/5xx responses. Do not retry 4xx errors like an invalid API key or a malformed GSTIN, since retrying will not change the outcome and just wastes a verification credit.
IAsyncEnumerable lets you stream results back to the caller as each GSTIN finishes verifying, instead of waiting for an entire batch to complete before returning anything. That matters when you are verifying hundreds of vendor GSTINs and want to show progress or persist results incrementally.
No. The GSTIN check digit only proves the string is structurally well-formed — it does not prove the number was ever issued. Only the live API call, which checks the GSTIN against actual government records, confirms registration and 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