Guide 8 min read

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.

Written and reviewed by the GSTIN API team Last reviewed

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).

  1. Load the CSV and identify the GSTIN column (by header name, or by pattern if the header is inconsistent).
  2. Normalise: trim whitespace, uppercase, strip non-alphanumeric characters.
  3. Dedupe: collapse repeated GSTINs to one row, keeping a mapping back to every original row that referenced it.
  4. Checksum pre-filter: locally validate each normalised value; anything that fails skips the API call.
  5. Verify: call the API for every value that passed the pre-filter, in bounded concurrent batches with retry.
  6. 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 valueProblemCleaned value
" 27aaacr5055k1z7"Leading space, lowercase27AAACR5055K1Z7
"27AAACR5055K1Z7\t"Trailing tab character27AAACR5055K1Z7
2.70E+14Excel mangled the value into scientific notationUnrecoverable 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 sheet27AAACR5055K1Z7
(blank)Missing valueSkip — flag as "missing" in output, do not call the API
Raw vs. cleaned values from a typical messy vendor-master export.

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.

ColumnPurpose
original_gstinExactly as it appeared in the source row, unchanged.
normalised_gstinAfter trimming, uppercasing and stripping stray characters.
statusOne of: verified-active, verified-cancelled, verified-suspended, invalid-format, not-found, missing, error.
legal_namePopulated when the API returned a match; blank otherwise.
checked_atTimestamp of the API call, for audit and for knowing how stale the row is.
error_detailPopulated only on status = error, for troubleshooting without re-running the whole job.
A practical output schema for a bulk GSTIN verification 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

Dedupe and normalise the GSTIN column first, run a local checksum check to filter out obviously malformed values before calling the API, then process the remaining rows in small concurrent batches with retries, writing a per-row status back to an output CSV.
A checksum check is free and instant, and a messy vendor-master export typically has a meaningful share of rows that are blank, truncated, or garbled by Excel formatting. Filtering those out locally means you only spend API credits on GSTINs that are at least well-formed, not on garbage.
There is no universal number — it depends on your account's rate limit and the API's own throughput. A safe starting point for most bulk jobs is a small, fixed concurrency cap (for example, 5–10 requests in flight at once) with backoff on any 429 or 5xx response, rather than firing the whole file at once.
At minimum: the original GSTIN as entered, a normalised GSTIN, a per-row status (verified / invalid-format / not-found / error), the legal name and current status when available, and a timestamp — so the output is both usable immediately and auditable later.
GSTINs are alphanumeric but some vendor-master exports store them as if numeric, or a mixed column gets auto-formatted; Excel can render long numeric-looking strings in scientific notation (e.g. 2.7E+14) when the cell format is General or Number, silently corrupting the value. Re-importing that column as Text, or sourcing the export as CSV with the column pre-formatted as text, avoids this.

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