🐍 Python🇮🇳 IndiaTax Compliance

Indian Business Compliance — GSTIN Validation

Validate India GST Identification Numbers (GSTIN) in Python using asyncio.gather for concurrent batch processing. Extract state codes, entity types, and cross-check embedded PAN numbers.

Also available in Node.js

1. GSTIN structure

A GSTIN is a 15-character alphanumeric identifier with a well-defined structure:

PositionCharactersContent
1–22 digitsState code (01–38, per Indian state/UT)
3–1210 charsPAN number of the taxpayer
131 digitEntity number (1–9 for multiple registrations)
141 charDefault "Z"
151 charCheck character (alphanumeric)
ℹ️Example: 27AAPFU0939F1ZV — state 27 (Maharashtra), PAN AAPFU0939F, entity 1.

2. Validate a GSTIN

Use await iv.gstin(value) (SDK) or GET /v0/gstin?value=….

{
  "valid": true,
  "gstin": "27AAPFU0939F1ZV",
  "stateCode": "27",
  "stateName": "Maharashtra",
  "pan": "AAPFU0939F",
  "entityNumber": "1",
  "entityType": "Private Limited Company",
  "checkChar": "V"
}
  1. Check valid — format + checksum
  2. Use state_name for invoice state field
  3. Store pan for income tax cross-referencing
  4. Log entity_type for compliance records

3. Batch validation with asyncio.gather

For vendor onboarding or invoice bulk processing, validate multiple GSTINs concurrently. Use return_exceptions=True so a single failure doesn't abort the whole batch.

import asyncio
from isvalid_sdk import IsValidConfig, create_client

config = IsValidConfig(api_key="YOUR_API_KEY")
iv = create_client(config)

async def validate_gstin(gstin: str):
    result = await iv.gstin(gstin)
    if not result.valid:
        raise ValueError(f"Invalid GSTIN: {gstin}")
    return result

async def validate_batch(gstins: list[str]):
    results = await asyncio.gather(
        *[iv.gstin(g) for g in gstins],
        return_exceptions=True
    )
    return dict(zip(gstins, results))

async def main():
    # Single validation
    r = await validate_gstin("27AAPFU0939F1ZV")
    print(f"State: {r.state_name}, Entity: {r.entity_type}")
    print(f"PAN: {r.pan}")

    # Batch validation
    gstins = [
        "27AAPFU0939F1ZV",  # Maharashtra
        "09AAACH7409R1ZZ",  # Uttar Pradesh
        "24AAACP8880D1Z5",  # Gujarat
    ]
    batch = await validate_batch(gstins)
    for gstin, result in batch.items():
        if isinstance(result, Exception):
            print(f"{gstin}: ERROR — {result}")
        else:
            print(f"{gstin}: valid={result.valid}, state={result.state_name}")

asyncio.run(main())

4. Cross-checking the embedded PAN

A business may supply its PAN separately during onboarding. Use the embedded PAN from GSTIN to verify consistency:

async def verify_gstin_pan_match(gstin: str, declared_pan: str) -> bool:
    result = await iv.gstin(gstin)
    if not result.valid:
        return False
    return result.pan == declared_pan.upper()

# Usage
match = await verify_gstin_pan_match("27AAPFU0939F1ZV", "AAPFU0939F")
if not match:
    raise ValueError("GSTIN does not match the declared PAN")
⚠️If a customer provides multiple GSTINs for different states, all of them should share the same embedded PAN. A mismatch across GSTINs may indicate data entry errors or fraudulent documents.

5. Edge cases

State code mismatch on invoices

⚠️For inter-state B2B transactions, supplier and buyer state codes must differ (IGST applies). For intra-state, they must match (CGST + SGST apply). Extract state codes from both GSTINs.
supplier, buyer = await asyncio.gather(
    iv.gstin(supplier_gstin),
    iv.gstin(buyer_gstin),
)
if supplier.state_code == buyer.state_code:
    tax_type = "CGST + SGST"   # intra-state
else:
    tax_type = "IGST"          # inter-state

Format valid but registration cancelled

ℹ️The API validates GSTIN format and structure. It does not query the live GST portal for active registration status. For high-value transactions, additionally verify on the official GST portal.

Multiple registrations — same PAN

# Entity number (pos 13) distinguishes multiple state registrations
# for the same business
# "27AAPFU0939F1ZV" and "29AAPFU0939F2ZR" = same PAN, different states
gstins = ["27AAPFU0939F1ZV", "29AAPFU0939F2ZR"]
results = await asyncio.gather(*[iv.gstin(g) for g in gstins])
pans = {r.pan for r in results if r.valid}
if len(pans) == 1:
    print("All GSTINs belong to the same business")

6. Summary checklist

Validate GSTIN format + checksum via API
Extract state code for IGST/CGST determination
Cross-check embedded PAN with declared PAN
Use asyncio.gather for batch onboarding
Handle return_exceptions=True in batch calls
Verify all GSTINs share same PAN for one business
Note: API validates structure, not active registration
Return 422 with field-level error messages

See also

Ready to integrate?

Free tier — 1,000 requests/month. No credit card required.

Get your API key →