<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[IndiaCos]]></title><description><![CDATA[Know any Indian business before you deal with it: companies, LLPs and GST registrations with directors, filings and charges, from public registries. https://ind]]></description><link>https://indiacos.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ad4432632882ddf38cbfb91/de787439-8760-46a5-8fe0-32d627bb4e6d.png</url><title>IndiaCos</title><link>https://indiacos.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 06:13:57 GMT</lastBuildDate><atom:link href="https://indiacos.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[GSTIN check digit: validate GST numbers before you call any API]]></title><description><![CDATA[If you build anything that takes GST numbers from users (vendor onboarding, invoice capture, a checkout for B2B buyers), people will paste in GSTINs with…]]></description><link>https://indiacos.hashnode.dev/gstin-check-digit-validate-gst-numbers-before-you-call-any-api</link><guid isPermaLink="true">https://indiacos.hashnode.dev/gstin-check-digit-validate-gst-numbers-before-you-call-any-api</guid><category><![CDATA[Python]]></category><category><![CDATA[GST]]></category><category><![CDATA[india]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[IndiaCos]]></dc:creator><pubDate>Fri, 23 Oct 2026 04:28:49 GMT</pubDate><content:encoded><![CDATA[<p>If you build anything that takes GST numbers from users (vendor onboarding, invoice capture, a checkout for B2B buyers), people will paste in GSTINs with typos. An O instead of a 0, one character left out, two digits swapped. You can catch most of these locally, before you call any lookup service, because the last character of every GSTIN is a check character worked out from the first 14.</p>
<p>This post covers the format, works through the check-digit algorithm by hand on a real public GSTIN, and gives you a 20-line Python validator with the tests I ran against it.</p>
<h2>The 15 characters</h2>
<p>A GSTIN is always 15 characters. Using Tata Consultancy Services' Maharashtra registration, <code>27AAACR4849R4ZI</code>, as the example:</p>
<table>
<thead>
<tr>
<th>Position</th>
<th>Example</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td>1–2</td>
<td><code>27</code></td>
<td>State code (27 is Maharashtra)</td>
</tr>
<tr>
<td>3–12</td>
<td><code>AAACR4849R</code></td>
<td>PAN of the business. The 4th letter is the holder type: C company, P individual, F firm or LLP, H HUF, T trust, A AOP, G government</td>
</tr>
<tr>
<td>13</td>
<td><code>4</code></td>
<td>Entity number: counts the registrations this PAN holds in this state (1, 2, … 9, then letters)</td>
</tr>
<tr>
<td>14</td>
<td><code>Z</code></td>
<td><code>Z</code> by default</td>
</tr>
<tr>
<td>15</td>
<td><code>I</code></td>
<td>Check character, calculated from the first 14</td>
</tr>
</tbody></table>
<p>So you can read a lot from a GSTIN before you look anything up: which state it was issued in, the PAN behind it, whether the holder is a company or an individual, and that this is the PAN's fourth registration in Maharashtra.</p>
<h2>How the check character is calculated</h2>
<p>The method is a base-36 version of the Luhn algorithm that card numbers use:</p>
<ol>
<li>Map each of the first 14 characters to a number: <code>0–9</code> stay as they are, <code>A</code> is 10, <code>B</code> is 11 … <code>Z</code> is 35.</li>
<li>Multiply the characters alternately by 1 and 2, starting with 1 on the first character.</li>
<li>Split each product into its quotient and remainder when divided by 36, and add the two together.</li>
<li>Add up all 14 results.</li>
<li>The check value is whatever you need to add to bring the total up to a multiple of 36: <code>(36 − total mod 36) mod 36</code>.</li>
<li>Convert that value back to a character with the same 0–Z table.</li>
</ol>
<p>Here is the working for <code>27AAACR4849R4ZI</code>:</p>
<table>
<thead>
<tr>
<th>Pos</th>
<th>Char</th>
<th>Value</th>
<th>×</th>
<th>Product</th>
<th>Quotient + remainder</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>2</td>
<td>2</td>
<td>1</td>
<td>2</td>
<td>0 + 2 = 2</td>
</tr>
<tr>
<td>2</td>
<td>7</td>
<td>7</td>
<td>2</td>
<td>14</td>
<td>0 + 14 = 14</td>
</tr>
<tr>
<td>3</td>
<td>A</td>
<td>10</td>
<td>1</td>
<td>10</td>
<td>0 + 10 = 10</td>
</tr>
<tr>
<td>4</td>
<td>A</td>
<td>10</td>
<td>2</td>
<td>20</td>
<td>0 + 20 = 20</td>
</tr>
<tr>
<td>5</td>
<td>A</td>
<td>10</td>
<td>1</td>
<td>10</td>
<td>0 + 10 = 10</td>
</tr>
<tr>
<td>6</td>
<td>C</td>
<td>12</td>
<td>2</td>
<td>24</td>
<td>0 + 24 = 24</td>
</tr>
<tr>
<td>7</td>
<td>R</td>
<td>27</td>
<td>1</td>
<td>27</td>
<td>0 + 27 = 27</td>
</tr>
<tr>
<td>8</td>
<td>4</td>
<td>4</td>
<td>2</td>
<td>8</td>
<td>0 + 8 = 8</td>
</tr>
<tr>
<td>9</td>
<td>8</td>
<td>8</td>
<td>1</td>
<td>8</td>
<td>0 + 8 = 8</td>
</tr>
<tr>
<td>10</td>
<td>4</td>
<td>4</td>
<td>2</td>
<td>8</td>
<td>0 + 8 = 8</td>
</tr>
<tr>
<td>11</td>
<td>9</td>
<td>9</td>
<td>1</td>
<td>9</td>
<td>0 + 9 = 9</td>
</tr>
<tr>
<td>12</td>
<td>R</td>
<td>27</td>
<td>2</td>
<td>54</td>
<td>1 + 18 = 19</td>
</tr>
<tr>
<td>13</td>
<td>4</td>
<td>4</td>
<td>1</td>
<td>4</td>
<td>0 + 4 = 4</td>
</tr>
<tr>
<td>14</td>
<td>Z</td>
<td>35</td>
<td>2</td>
<td>70</td>
<td>1 + 34 = 35</td>
</tr>
</tbody></table>
<p>The total is 198. 198 mod 36 is 18, and 36 − 18 = 18, which is <code>I</code>. That matches the last character of the GSTIN.</p>
<p>Step 3 is the part people get wrong. If you just add up the raw products (or take <code>product % 36</code> without adding the quotient back), you'll get the right answer for some GSTINs and the wrong one for others. Here it matters at positions 12 and 14.</p>
<h2>The validator</h2>
<pre><code class="language-python">import re

CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
PATTERN = re.compile(r"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$")

def gstin_check_char(first14: str) -&gt; str:
    total = 0
    for i, ch in enumerate(first14):
        product = CHARS.index(ch) * (2 if i % 2 else 1)  # double every 2nd char
        total += product // 36 + product % 36            # quotient + remainder
    return CHARS[(36 - total % 36) % 36]

def is_valid_gstin(gstin: str) -&gt; bool:
    g = gstin.strip().upper()
    if not PATTERN.match(g):
        return False
    return gstin_check_char(g[:14]) == g[14]

if __name__ == "__main__":
    for g in ["24AAACR4849R4ZO", "27AAACR4849R4ZI", "27AAACR4849R4ZX"]:
        print(g, is_valid_gstin(g))
</code></pre>
<p>Output:</p>
<pre><code>24AAACR4849R4ZO True
27AAACR4849R4ZI True
27AAACR4849R4ZX False
</code></pre>
<p>The regular expression checks the shape: two digits, a PAN-shaped block (five letters, four digits, one letter), an entity number from 1–9 or A–Z, a <code>Z</code>, then any check character. Only then does it compare the check character.</p>
<p>A note on the <code>Z</code>: the pattern here is strict about position 14 because that is the default. If your system also handles unusual registration types, loosen that part of the regex and keep the check-digit comparison.</p>
<h2>How I tested it</h2>
<p>I didn't want to publish a validator that only works on its own examples, so I tested it against GSTINs shown on public IndiaCos pages: three Tata Consultancy Services registrations (Gujarat <code>24AAACR4849R4ZO</code>, Maharashtra <code>27AAACR4849R4ZI</code>, Karnataka <code>29AAACR4849R4ZE</code>) plus two other examples from the site. For each one, the tests check that:</p>
<ul>
<li>the real GSTIN passes, in upper and lower case;</li>
<li>all 35 wrong check characters fail;</li>
<li>every single-character change to the first 14 characters that still fits the format fails;</li>
<li>malformed inputs fail (too short, too long, entity number <code>0</code>, a letter other than <code>Z</code> at position 14, a digit inside the PAN's letter block).</li>
</ul>
<pre><code class="language-python">from gstin import is_valid_gstin
valid = ["24AAACR4849R4ZO", "27AAACR4849R4ZI", "29AAACR4849R4ZE"]
for g in valid:
    assert is_valid_gstin(g) and is_valid_gstin(g.lower())
    for c in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ":
        if c != g[14]:
            assert not is_valid_gstin(g[:14] + c)
assert not is_valid_gstin("24AAACR4849R0ZO")   # entity number can't be 0
</code></pre>
<p>Every single-character typo gets caught, because doubling-then-folding by 36 maps each value to a different result. Adjacent swaps nearly always get caught too. When I brute-forced every swap of two neighbouring characters, the only ones the check missed were swaps of <code>0</code> and <code>Z</code>, the same kind of blind spot Luhn has with <code>09</code> and <code>90</code>.</p>
<h2>What a valid check digit does <em>not</em> tell you</h2>
<p>A GSTIN that passes this check is correctly formed. That's all it tells you. It doesn't tell you whether the number was ever issued, whether it's still active, or whether it belongs to the company on the invoice. For that you need to look the number up.</p>
<p>You can do that one at a time in a browser on the IndiaCos <a href="https://indiacos.com/gstin-format">GSTIN format guide</a> and the search it links to, which also decodes the state and PAN and opens the registration report. For volume, the <a href="https://indiacos.com/api">IndiaCos API</a> returns the same data as JSON. New accounts get ₹50 of free credit, then it costs ₹0.16 per successful call. Errors and not-found lookups are free, so you can run the local check first and skip the paid call for numbers that are malformed anyway. For legal purposes, the GST authority's own records are what count, so for anything high-stakes, confirm the status there too.</p>
<p>The local check costs nothing and runs in microseconds, so run it every time a GSTIN is entered.</p>
<p><em>Disclosure: Sunny Mittal works on marketing at IndiaCos. Corrections welcome.</em></p>
]]></content:encoded></item><item><title><![CDATA[GSTIN check digit explained, with a 20-line validator]]></title><description><![CDATA[How the 15th character of an Indian GST number is calculated, and a tested Python function that catches typos before you make an API call]]></description><link>https://indiacos.hashnode.dev/gstin-check-digit-explained-with-a-20-line-validator</link><guid isPermaLink="true">https://indiacos.hashnode.dev/gstin-check-digit-explained-with-a-20-line-validator</guid><category><![CDATA[Validation]]></category><category><![CDATA[fintech]]></category><category><![CDATA[Python]]></category><category><![CDATA[india]]></category><category><![CDATA[GST]]></category><dc:creator><![CDATA[IndiaCos]]></dc:creator><pubDate>Wed, 21 Oct 2026 03:52:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad4432632882ddf38cbfb91/055d9973-fa3b-42cc-b164-0bf8e09d8325.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you build anything that takes GST numbers from users (vendor onboarding, invoice capture, a checkout for B2B buyers), people will paste in GSTINs with typos. An O instead of a 0, one character left out, two digits swapped. You can catch most of these locally, before you call any lookup service, because the last character of every GSTIN is a check character worked out from the first 14.</p>
<p>This post covers the format, works through the check-digit algorithm by hand on a real public GSTIN, and gives you a 20-line Python validator with the tests I ran against it.</p>
<h2>The 15 characters</h2>
<p>A GSTIN is always 15 characters. Using Tata Consultancy Services' Maharashtra registration, <code>27AAACR4849R4ZI</code>, as the example:</p>
<table>
<thead>
<tr>
<th>Position</th>
<th>Example</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td>1–2</td>
<td><code>27</code></td>
<td>State code (27 is Maharashtra)</td>
</tr>
<tr>
<td>3–12</td>
<td><code>AAACR4849R</code></td>
<td>PAN of the business. The 4th letter is the holder type: C company, P individual, F firm or LLP, H HUF, T trust, A AOP, G government</td>
</tr>
<tr>
<td>13</td>
<td><code>4</code></td>
<td>Entity number: counts the registrations this PAN holds in this state (1, 2, … 9, then letters)</td>
</tr>
<tr>
<td>14</td>
<td><code>Z</code></td>
<td><code>Z</code> by default</td>
</tr>
<tr>
<td>15</td>
<td><code>I</code></td>
<td>Check character, calculated from the first 14</td>
</tr>
</tbody></table>
<p>So you can read a lot from a GSTIN before you look anything up: which state it was issued in, the PAN behind it, whether the holder is a company or an individual, and that this is the PAN's fourth registration in Maharashtra.</p>
<h2>How the check character is calculated</h2>
<p>The method is a base-36 version of the Luhn algorithm that card numbers use:</p>
<ol>
<li><p>Map each of the first 14 characters to a number: <code>0–9</code> stay as they are, <code>A</code> is 10, <code>B</code> is 11 … <code>Z</code> is 35.</p>
</li>
<li><p>Multiply the characters alternately by 1 and 2, starting with 1 on the first character.</p>
</li>
<li><p>Split each product into its quotient and remainder when divided by 36, and add the two together.</p>
</li>
<li><p>Add up all 14 results.</p>
</li>
<li><p>The check value is whatever you need to add to bring the total up to a multiple of 36: <code>(36 − total mod 36) mod 36</code>.</p>
</li>
<li><p>Convert that value back to a character with the same 0–Z table.</p>
</li>
</ol>
<p>Here is the working for <code>27AAACR4849R4ZI</code>:</p>
<table>
<thead>
<tr>
<th>Pos</th>
<th>Char</th>
<th>Value</th>
<th>×</th>
<th>Product</th>
<th>Quotient + remainder</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>2</td>
<td>2</td>
<td>1</td>
<td>2</td>
<td>0 + 2 = 2</td>
</tr>
<tr>
<td>2</td>
<td>7</td>
<td>7</td>
<td>2</td>
<td>14</td>
<td>0 + 14 = 14</td>
</tr>
<tr>
<td>3</td>
<td>A</td>
<td>10</td>
<td>1</td>
<td>10</td>
<td>0 + 10 = 10</td>
</tr>
<tr>
<td>4</td>
<td>A</td>
<td>10</td>
<td>2</td>
<td>20</td>
<td>0 + 20 = 20</td>
</tr>
<tr>
<td>5</td>
<td>A</td>
<td>10</td>
<td>1</td>
<td>10</td>
<td>0 + 10 = 10</td>
</tr>
<tr>
<td>6</td>
<td>C</td>
<td>12</td>
<td>2</td>
<td>24</td>
<td>0 + 24 = 24</td>
</tr>
<tr>
<td>7</td>
<td>R</td>
<td>27</td>
<td>1</td>
<td>27</td>
<td>0 + 27 = 27</td>
</tr>
<tr>
<td>8</td>
<td>4</td>
<td>4</td>
<td>2</td>
<td>8</td>
<td>0 + 8 = 8</td>
</tr>
<tr>
<td>9</td>
<td>8</td>
<td>8</td>
<td>1</td>
<td>8</td>
<td>0 + 8 = 8</td>
</tr>
<tr>
<td>10</td>
<td>4</td>
<td>4</td>
<td>2</td>
<td>8</td>
<td>0 + 8 = 8</td>
</tr>
<tr>
<td>11</td>
<td>9</td>
<td>9</td>
<td>1</td>
<td>9</td>
<td>0 + 9 = 9</td>
</tr>
<tr>
<td>12</td>
<td>R</td>
<td>27</td>
<td>2</td>
<td>54</td>
<td>1 + 18 = 19</td>
</tr>
<tr>
<td>13</td>
<td>4</td>
<td>4</td>
<td>1</td>
<td>4</td>
<td>0 + 4 = 4</td>
</tr>
<tr>
<td>14</td>
<td>Z</td>
<td>35</td>
<td>2</td>
<td>70</td>
<td>1 + 34 = 35</td>
</tr>
</tbody></table>
<p>The total is 198. 198 mod 36 is 18, and 36 − 18 = 18, which is <code>I</code>. That matches the last character of the GSTIN.</p>
<p>Step 3 is the part people get wrong. If you just add up the raw products (or take <code>product % 36</code> without adding the quotient back), you'll get the right answer for some GSTINs and the wrong one for others. Here it matters at positions 12 and 14.</p>
<h2>The validator</h2>
<pre><code class="language-python">import re

CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
PATTERN = re.compile(r"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$")

def gstin_check_char(first14: str) -&gt; str:
    total = 0
    for i, ch in enumerate(first14):
        product = CHARS.index(ch) * (2 if i % 2 else 1)  # double every 2nd char
        total += product // 36 + product % 36            # quotient + remainder
    return CHARS[(36 - total % 36) % 36]

def is_valid_gstin(gstin: str) -&gt; bool:
    g = gstin.strip().upper()
    if not PATTERN.match(g):
        return False
    return gstin_check_char(g[:14]) == g[14]

if __name__ == "__main__":
    for g in ["24AAACR4849R4ZO", "27AAACR4849R4ZI", "27AAACR4849R4ZX"]:
        print(g, is_valid_gstin(g))
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">24AAACR4849R4ZO True
27AAACR4849R4ZI True
27AAACR4849R4ZX False
</code></pre>
<p>The regular expression checks the shape: two digits, a PAN-shaped block (five letters, four digits, one letter), an entity number from 1–9 or A–Z, a <code>Z</code>, then any check character. Only then does it compare the check character.</p>
<p>A note on the <code>Z</code>: the pattern here is strict about position 14 because that is the default. If your system also handles unusual registration types, loosen that part of the regex and keep the check-digit comparison.</p>
<h2>How I tested it</h2>
<p>I didn't want to publish a validator that only works on its own examples, so I tested it against GSTINs shown on public IndiaCos pages: three Tata Consultancy Services registrations (Gujarat <code>24AAACR4849R4ZO</code>, Maharashtra <code>27AAACR4849R4ZI</code>, Karnataka <code>29AAACR4849R4ZE</code>) plus two other examples from the site. For each one, the tests check that:</p>
<ul>
<li><p>the real GSTIN passes, in upper and lower case;</p>
</li>
<li><p>all 35 wrong check characters fail;</p>
</li>
<li><p>every single-character change to the first 14 characters that still fits the format fails;</p>
</li>
<li><p>malformed inputs fail (too short, too long, entity number <code>0</code>, a letter other than <code>Z</code> at position 14, a digit inside the PAN's letter block).</p>
</li>
</ul>
<pre><code class="language-python">from gstin import is_valid_gstin
valid = ["24AAACR4849R4ZO", "27AAACR4849R4ZI", "29AAACR4849R4ZE"]
for g in valid:
    assert is_valid_gstin(g) and is_valid_gstin(g.lower())
    for c in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ":
        if c != g[14]:
            assert not is_valid_gstin(g[:14] + c)
assert not is_valid_gstin("24AAACR4849R0ZO")   # entity number can't be 0
</code></pre>
<p>Every single-character typo gets caught, because doubling-then-folding by 36 maps each value to a different result. Adjacent swaps nearly always get caught too. When I brute-forced every swap of two neighbouring characters, the only ones the check missed were swaps of <code>0</code> and <code>Z</code>, the same kind of blind spot Luhn has with <code>09</code> and <code>90</code>.</p>
<h2>What a valid check digit does <em>not</em> tell you</h2>
<p>A GSTIN that passes this check is correctly formed. That's all it tells you. It doesn't tell you whether the number was ever issued, whether it's still active, or whether it belongs to the company on the invoice. For that you need to look the number up.</p>
<p>You can do that one at a time in a browser on the IndiaCos <a href="https://indiacos.com/gstin-format">GSTIN format guide</a> and the search it links to, which also decodes the state and PAN and opens the registration report. For volume, the <a href="https://indiacos.com/api">IndiaCos API</a> returns the same data as JSON. New accounts get ₹50 of free credit, then it costs ₹0.16 per successful call. Errors and not-found lookups are free, so you can run the local check first and skip the paid call for numbers that are malformed anyway. For legal purposes, the GST authority's own records are what count, so for anything high-stakes, confirm the status there too.</p>
<p>The local check costs nothing and runs in microseconds, so run it every time a GSTIN is entered.</p>
<p><em>Disclosure: Sunny Mittal works on marketing at IndiaCos. Corrections welcome.</em></p>
]]></content:encoded></item></channel></rss>