How to Bulk Verify GST Numbers via CSV Upload
Verify thousands of GSTINs at once from a spreadsheet — a step-by-step guide to bulk GST verification pipelines.
Bulk GST verification from a CSV is a five-stage pipeline: dedupe and normalise the GSTIN column, run a local checksum pre-filter to drop obviously bad values before you spend any API credits, process the rest in small concurrent batches with retries, and write a per-row status back to an output file. Done this way, a vendor master with tens of thousands of rows can be verified in minutes without wasting credits on garbage input.
The pipeline, end to end
A bulk GST verification job is not really one operation — it is a small ETL pipeline with GSTIN verification as the middle step. Skipping the cleanup stages is the most common reason bulk uploads produce confusing results (a wave of "invalid" rows that are actually just formatting artifacts, not real invalid GSTINs).
- Load the CSV and identify the GSTIN column (by header name, or by pattern if the header is inconsistent).
- Normalise: trim whitespace, uppercase, strip non-alphanumeric characters.
- Dedupe: collapse repeated GSTINs to one row, keeping a mapping back to every original row that referenced it.
- Checksum pre-filter: locally validate each normalised value; anything that fails skips the API call.
- Verify: call the API for every value that passed the pre-filter, in bounded concurrent batches with retry.
- Write output: one row per original input row, each carrying a status and the verified fields where available.
For the API-side details of the verification call itself — rate limits, response shape, error codes — see the companion piece, the bulk GSTIN verification guide, rather than this post, which focuses on the pipeline you build around that call.
Dedupe and normalise the GSTIN column
A GSTIN is fifteen fixed-format characters, but the same value can arrive from a spreadsheet in several inconsistent forms: lowercase letters, leading or trailing whitespace, an embedded tab character from a copy-paste out of another system, or duplicated rows because the same vendor appears under two purchase order numbers. None of these are unusual in a real vendor master — normalise before you validate anything.
Normalisation, in order:
- Trim leading/trailing whitespace and any embedded tab or newline characters.
- Uppercase the entire string (GSTINs are uppercase alphanumeric).
- Strip characters that are not letters or digits — this catches invisible non-breaking spaces and stray punctuation from copy-paste.
- Deduplicate on the normalised value, but keep every original row it maps back to, since the output file needs to report a status for each original row, not each unique GSTIN.
Worked example: cleaning a messy vendor-master column
Vendor-master exports are the worst-case input for this pipeline, because they are usually exported by someone other than the person doing the verification, from a system that was never built to store GSTINs cleanly. A representative sample column looks like this before cleanup:
| Raw value | Problem | Cleaned value |
|---|---|---|
| " 27aaacr5055k1z7" | Leading space, lowercase | 27AAACR5055K1Z7 |
| "27AAACR5055K1Z7\t" | Trailing tab character | 27AAACR5055K1Z7 |
| 2.70E+14 | Excel mangled the value into scientific notation | Unrecoverable from the export — must be re-pulled from source with the column formatted as text |
| "27AAACR5055K1Z7 (HO)" | Free-text annotation appended by whoever maintained the sheet | 27AAACR5055K1Z7 |
| (blank) | Missing value | Skip — flag as "missing" in output, do not call the API |
A checksum pre-filter so you do not spend credits on garbage
Every GSTIN's 15th character is a check digit computed from the first 14 using a Luhn mod-36 style algorithm. Running this check locally, before calling the API, costs nothing and instantly filters out rows that are structurally impossible — blank cells, truncated values, transposed digits from manual entry — without spending a single verification credit on them.
const CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
function isChecksumValid(gstin) {
if (gstin.length !== 15) return false;
let sum = 0;
for (let i = 0; i < 14; i++) {
const value = CHARSET.indexOf(gstin[i]);
if (value === -1) return false;
const factor = i % 2 === 0 ? 1 : 2;
const product = value * factor;
sum += Math.floor(product / 36) + (product % 36);
}
const checkChar = CHARSET[(36 - (sum % 36)) % 36];
return checkChar === gstin[14];
}
A checksum pass only proves the value is well-formed, not that the GSTIN was ever issued — that still requires the live API call. But on a genuinely messy vendor master, this pre-filter routinely removes a meaningful share of rows before you touch the network, which is where the real credit savings come from on large files.
Chunked concurrent requests with a concurrency cap
Sending ten thousand requests serially is slow; firing all ten thousand at once risks tripping rate limits and makes failures hard to attribute. The middle ground is chunking the checksum-valid rows into batches and processing each batch with a fixed number of requests in flight at a time.
const CONCURRENCY = 8;
async function verifyBatch(gstins) {
const results = [];
for (let i = 0; i < gstins.length; i += CONCURRENCY) {
const chunk = gstins.slice(i, i + CONCURRENCY);
const chunkResults = await Promise.all(chunk.map(verifyOne));
results.push(...chunkResults);
}
return results;
}
The right concurrency number depends on your account's own rate limit and how latency-sensitive the rest of the job is — start conservative (5–10) and only raise it once you have confirmed you are not seeing throttling responses.
Retry and backoff on failures
Any bulk job against a network API will see some fraction of transient failures — a timeout, a rate-limit response, a momentary 5xx. Treat these differently from a genuine "not found" or "invalid" result, which should not be retried.
- 429 (rate limited) or 5xx: retry with exponential backoff, up to a small fixed number of attempts (three is a reasonable default).
- 4xx other than 429: do not retry — this usually means the GSTIN itself is invalid or malformed, and retrying will not change the answer.
- Network timeout: retry once or twice with a short backoff, since this is often transient.
An output CSV schema with a per-row status
The output file is the actual deliverable of a bulk verification job — whoever requested it needs to be able to open it and immediately see which rows need attention, without re-running anything. A per-row status column, not just a summary count, is what makes that possible.
| Column | Purpose |
|---|---|
| original_gstin | Exactly as it appeared in the source row, unchanged. |
| normalised_gstin | After trimming, uppercasing and stripping stray characters. |
| status | One of: verified-active, verified-cancelled, verified-suspended, invalid-format, not-found, missing, error. |
| legal_name | Populated when the API returned a match; blank otherwise. |
| checked_at | Timestamp of the API call, for audit and for knowing how stale the row is. |
| error_detail | Populated only on status = error, for troubleshooting without re-running the whole job. |
Keeping status granular (rather than a flat verified/failed boolean) is what lets a finance
or procurement team triage the output directly — a "missing" row needs someone to go find the GSTIN, an
"invalid-format" row needs a data-entry fix, and a "verified-cancelled" row needs a business decision, and
those are three different follow-up actions that a single boolean would hide.
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 use cases & workflows
Browse every guide in the GST API blog.