Integration guide 9 min read

How to Integrate a GST Verification API in Python

A hands-on Python tutorial for validating GSTINs in real time using requests, with retries, typing and a reusable client.

Written and reviewed by the GSTIN API team Last reviewed

To integrate a GST verification API in Python, build a small typed client around requests.Session, attach a urllib3 retry adapter for transient failures, and model the JSON response with dataclasses so the rest of your codebase never touches raw dictionaries. This post walks through that client end to end, including offline GSTIN validation and a pytest suite that mocks the HTTP layer with the responses library.

What a Python integration actually needs

A GST verification API integration in Python is a small piece of software, but it has the same requirements as any other outbound HTTP client: correct headers, sane timeouts, retry logic that does not make failures worse, and a response shape your application code can rely on. The endpoint itself is simple — a single GET request per GSTIN — so most of the engineering effort goes into making the client predictable to call and easy to test, not into the HTTP call itself.

The API you are calling is GET https://gstinapi.com/api/get-taxpayer-info/{gstin}, authenticated with an X-API-Key header. Every account gets 20 free verification credits on signup, and each successful lookup consumes one credit — which is exactly why client-side validation, covered next, matters more in Python than it might in a language with weaker typing discipline.

Validate the GSTIN before you spend a credit

A GSTIN is a 15-character string with a fixed structure, and the 15th character is a check digit computed from the first 14 using a Luhn mod-36 style algorithm. Validating both the shape and the check digit locally, before the API call, catches typos and obviously malformed input without touching the network — which means it does not cost a verification credit. Note that a valid checksum only proves the string is well-formed; it does not prove the GSTIN was ever issued, which is exactly what the live API call in the next section resolves.


GSTIN_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

def gstin_checksum_is_valid(gstin: str) -> bool:
    """Validate a GSTIN's structure and Luhn mod-36 check digit locally."""
    if len(gstin) != 15:
        return False
    if any(ch not in GSTIN_CHARSET for ch in gstin.upper()):
        return False

    body, check_char = gstin[:14].upper(), gstin[14].upper()
    total = 0
    for i, ch in enumerate(body):
        value = GSTIN_CHARSET.index(ch)
        factor = 1 if i % 2 == 0 else 2
        product = value * factor
        total += product // 36 + product % 36

    expected = GSTIN_CHARSET[(36 - (total % 36)) % 36]
    return expected == check_char

A typed client with a session and retries

Wrap the API in a small class rather than calling requests.get inline wherever you need a lookup. A requests.Session reuses the underlying TCP connection across calls, and mounting a urllib3 Retry adapter on it gives you automatic, bounded retries for the failure modes that are actually worth retrying — connection resets, timeouts, and 429/5xx responses — while leaving 4xx errors, like a bad API key or a rejected GSTIN, to fail immediately.


from __future__ import annotations

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


class GstinApiError(Exception):
    """Raised for non-2xx responses from the GST verification API."""

    def __init__(self, status_code: int, message: str):
        super().__init__(message)
        self.status_code = status_code


class GstinClient:
    BASE_URL = "https://gstinapi.com/api"

    def __init__(self, api_key: str, timeout: float = 10.0):
        self.api_key = api_key
        self.timeout = timeout
        self.session = requests.Session()

        retry = Retry(
            total=3,
            backoff_factor=0.5,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=["GET"],
        )
        adapter = HTTPAdapter(max_retries=retry)
        self.session.mount("https://", adapter)
        self.session.headers.update({"X-API-Key": self.api_key})

    def get_taxpayer_info(self, gstin: str):
        if not gstin_checksum_is_valid(gstin):
            raise ValueError(f"'{gstin}' is not a structurally valid GSTIN")

        response = self.session.get(
            f"{self.BASE_URL}/get-taxpayer-info/{gstin}",
            timeout=self.timeout,
        )
        if not response.ok:
            raise GstinApiError(response.status_code, response.text)

        return TaxpayerInfo.from_dict(response.json())

Modelling the response with dataclasses

The live API returns a rich payload — 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 dataclass with a small from_dict constructor keeps that structure explicit instead of passing a raw dictionary around your codebase, where a typo in a key name fails silently.


from dataclasses import dataclass
from typing import Optional


@dataclass(frozen=True)
class TaxpayerInfo:
    gstin: str
    legal_name: str
    trade_name: Optional[str]
    status: str
    registration_date: str
    cancellation_date: Optional[str]
    constitution: str
    taxpayer_type: str

    @classmethod
    def from_dict(cls, data: dict) -> "TaxpayerInfo":
        return cls(
            gstin=data["gstin"],
            legal_name=data["legal_name"],
            trade_name=data.get("trade_name"),
            status=data["status"],
            registration_date=data["registration_date"],
            cancellation_date=data.get("cancellation_date"),
            constitution=data["constitution"],
            taxpayer_type=data["taxpayer_type"],
        )

Trim or extend this dataclass to match the fields your product actually consumes — a lending workflow might only need status and constitution, while an accounts-payable tool will want the full filing history to flag a vendor who is behind on GSTR-3B. See the full response schema in the API documentation.

Calling the client

With the client and dataclass in place, a lookup is a couple of lines anywhere in your application. Reliance Industries’ public GSTIN, 27AAACR5055K1Z7, is a useful one to test against manually — its real response includes 354 filing records and 244 additional places of business, which is a good stress test for any UI you build on top of this data.


client = GstinClient(api_key="YOUR_API_KEY")
info = client.get_taxpayer_info("27AAACR5055K1Z7")

print(info.legal_name, info.status)

Testing with pytest and responses

Do not hit the live API in unit tests — it costs a credit per call and makes your test suite dependent on network access. The responses library patches requests at the transport layer, so you can register an exact URL and payload and assert your client parses it correctly.


import responses

from gstin_client import GstinClient


@responses.activate
def test_get_taxpayer_info_parses_response():
    responses.add(
        responses.GET,
        "https://gstinapi.com/api/get-taxpayer-info/27AAACR5055K1Z7",
        json={
            "gstin": "27AAACR5055K1Z7",
            "legal_name": "RELIANCE INDUSTRIES LIMITED",
            "trade_name": "RELIANCE INDUSTRIES LIMITED",
            "status": "Active",
            "registration_date": "2017-07-01",
            "cancellation_date": None,
            "constitution": "Public Limited Company",
            "taxpayer_type": "Regular",
        },
        status=200,
    )

    client = GstinClient(api_key="test-key")
    info = client.get_taxpayer_info("27AAACR5055K1Z7")

    assert info.status == "Active"
    assert info.legal_name == "RELIANCE INDUSTRIES LIMITED"


@responses.activate
def test_rejects_malformed_gstin_without_calling_api():
    client = GstinClient(api_key="test-key")

    try:
        client.get_taxpayer_info("NOT-A-GSTIN")
        assert False, "expected ValueError"
    except ValueError:
        pass

    assert len(responses.calls) == 0

Handling errors and rate limits

The GstinApiError in the client above carries the HTTP status code, so callers can branch on it: a 401 means the API key is missing or wrong, a 404 typically means the GSTIN is not found in the underlying registry, and a 429 means you are rate limited — which the retry adapter already backs off on automatically. Log the status code and a truncated response body on failure, not the full payload, since GST filing data can be sensitive.

For high-volume use, batch and pace your calls rather than firing hundreds of concurrent requests — see the bulk GSTIN verification guide and rate limits and webhooks post for patterns that apply regardless of language. If you are building this inside a Django or Flask app rather than a script, the same client class drops in as a service object with no changes.

Once your Python client is solid, the same request/response shape applies whether you are calling it from a Laravel app in PHP, a Java service, or a .NET backend — see our guides for PHP, Java, and .NET/C# if your stack spans more than one language. For the full field list this API returns, check the features page, and see pricing for credit packs beyond the free 20.

Logging, secrets, and timeouts

Keep the API key out of source control — load it from an environment variable or a secrets manager, not a hardcoded string in the client. Set an explicit timeout on every call rather than relying on the default, since requests will otherwise wait indefinitely on a hanging connection. When you log a failed lookup, log the HTTP status code and the GSTIN, but avoid logging the full response body — filing history and address data returned by the API can be sensitive, and most teams treat it the same way they treat other PII in application logs.

One more thing worth building in early: a thin caching layer in front of the client for GSTINs you look up repeatedly, such as a vendor whose status you check on every invoice. Registration status, filing history, and e-invoicing eligibility do not change minute to minute, so caching a lookup for a few hours is usually safe and cuts your credit spend meaningfully on a busy integration.

Frequently asked questions

requests is the pragmatic default for most backends and scripts. Pair it with a urllib3 Retry adapter mounted on a requests.Session so transient network failures and 429/5xx responses are retried automatically without you writing retry loops by hand.
Yes. GSTIN structure and its check digit can be validated entirely offline in a few lines of Python. Rejecting a malformed GSTIN before the HTTP call saves a verification credit and gives the caller a faster, clearer error.
Use the responses library to register a fake response for the exact URL your client calls, then assert on how your client parsed it. This keeps tests fast and deterministic without hitting the network or spending real credits.
No. A valid checksum only proves the GSTIN is well-formed — anyone can construct a fake number that passes the check digit. Only a live lookup against the API confirms the GSTIN was actually issued and its current registration status.
Use urllib3’s Retry with a small total count, an exponential backoff_factor, and a status_forcelist limited to 429 and 5xx codes. Never retry on 4xx client errors like an invalid API key or a malformed GSTIN — those will not change on a retry.

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