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.
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.
| Concern | Where it lives |
|---|---|
| Connection pooling | IHttpClientFactory |
| Retries and backoff | Polly, scoped to transient failures only |
| Serialisation | System.Text.Json with a source-generated JsonSerializerContext |
| Bulk streaming | IAsyncEnumerable consumed with await foreach |
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
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.