Technical guide 9 min read

GST API Rate Limits, Retries and Webhooks Explained

How to design resilient integrations against GST verification API rate limits, timeouts, retries, and webhook callbacks.

Written and reviewed by the GSTIN API team Last reviewed

Rate limits exist to keep an API fast and fair for every caller, and the way you handle a 429 — immediate retry versus backoff with jitter — is usually the biggest factor in whether your integration feels reliable under load. This post covers the general patterns (token buckets, backoff, idempotency, circuit breakers, webhooks) and how each one applies specifically to a GST verification workflow.

Token bucket versus fixed window

Rate limiting almost always comes down to one of two shapes. A fixed window counter resets at a clean boundary — say, the top of every minute — and allows up to N requests inside that window. It is simple to implement and reason about, but it has an edge case: a client can send N requests in the last second of one window and another N in the first second of the next, doubling the effective burst rate right at the boundary.

A token bucket instead holds a pool of tokens that refills continuously at a fixed rate (say, one token every 100ms up to a cap). Each request consumes a token; if the bucket is empty, the request is throttled. This smooths traffic because there is no hard reset moment to exploit — it is the more common choice for production APIs, including most GST verification providers, because it produces a steadier, more predictable load pattern on the backend.

PropertyFixed windowToken bucket
Burst behaviourCan double at window boundarySmoothed, no hard edge
Implementation costVery lowLow to moderate
Fairness under loadWeakerStronger
Typical client experienceOccasional sharp 429 spikesGradual throttling
Two common rate-limiting shapes, compared.

Reading a 429 and Retry-After correctly

A 429 Too Many Requests response is not an error in your integration logic — it is the server telling you, correctly, to slow down. The response should carry a Retry-After header, either as a number of seconds or an HTTP date, and the right behaviour is to wait at least that long before the next attempt:

function shouldWaitBeforeRetry(response) {
  if (response.status !== 429) return 0;

  const retryAfter = response.headers.get('Retry-After');
  if (!retryAfter) return null; // fall back to exponential backoff

  // Retry-After can be seconds, or an HTTP date
  const asSeconds = Number(retryAfter);
  if (!Number.isNaN(asSeconds)) return asSeconds * 1000;

  const asDate = new Date(retryAfter).getTime();
  return Math.max(0, asDate - Date.now());
}

Exponential backoff with jitter, worked out

When Retry-After is absent (a timeout, a 5xx, a dropped connection), exponential backoff is the standard fallback: each retry waits roughly twice as long as the last. The arithmetic for attempt n (starting at 0) with a base delay of 500ms is:

delay = min(maxDelay, base * 2^n)
delay_with_jitter = random(0, delay)   // "full jitter"

Concretely, with a 500ms base and a 30-second cap: attempt 0 waits up to 500ms, attempt 1 up to 1,000ms, attempt 2 up to 2,000ms, attempt 3 up to 4,000ms, and so on until the delay hits the 30-second ceiling and stays there. The random component (full jitter, picking uniformly between 0 and the computed delay) matters because without it, every client throttled at the same instant would retry at the same later instant too, recreating exactly the burst that got them throttled in the first place.

async function withBackoff(fn, { maxAttempts = 5, base = 500, cap = 30000 } = {}) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts - 1 || !isRetryable(err)) throw err;

      const delay = Math.min(cap, base * 2 ** attempt);
      const jittered = Math.random() * delay;
      await new Promise(r => setTimeout(r, jittered));
    }
  }
}

function isRetryable(err) {
  return err.status === 429 || err.status === 502 || err.status === 504 || err.isNetworkError;
}

Idempotency: making retries safe

Retrying is only safe if the retried operation cannot cause double effects. For a read-only lookup like GET https://gstinapi.com/api/get-taxpayer-info/{gstin}, this is close to automatic — GET requests are naturally idempotent, and the GSTIN in the path is already a stable key. Calling it twice for the same GSTIN returns the same taxpayer record; it does not create a duplicate anything. The practical concern shifts from "will this double-charge me" to "will this double-count against my usage," and on this API a call that fails basic validation (malformed GSTIN, missing key) is rejected before it reaches the lookup, so it does not consume a credit — only a completed, successful lookup does. See our error codes guide for the full breakdown of what counts as a completed lookup versus a rejected request.

For write-heavy APIs in general — not this one specifically — idempotency keys are the standard pattern: the client generates a unique key per logical operation and sends it on every retry, so the server can recognise "I've already done this" and return the original result instead of repeating the effect.

Circuit breakers: stop hammering a struggling API

A circuit breaker sits in front of an outbound call and tracks recent failures. Once the failure rate crosses a threshold — say, more than 50% of calls failing over a rolling window — it "opens": new calls fail fast locally, without even attempting the network request, for a cooldown period. After the cooldown it lets a small number of test calls through ("half-open") and closes again if they succeed.

This matters for two reasons. First, it protects your own application: if a dependency is slow rather than simply erroring, every caller blocking on a long timeout can exhaust your connection pool or worker threads. Second, it is considerate to the dependency — retrying aggressively into a service that is already struggling makes recovery slower for everyone calling it, not just you.

Designing bulk verification as a queue, not a loop

The most common mistake in bulk GSTIN verification is a synchronous for loop that fires requests as fast as the language allows. This guarantees you will hit rate limits, and it ties up whatever process is running the loop (a web request handler, a cron job) for as long as the batch takes.

The more robust design: push each GSTIN onto a queue, and have a small pool of workers pull from it at a controlled rate that respects the API's published limit — for example, a token-bucket limiter shared across workers, sized comfortably under the ceiling. This gives you natural backpressure, easy retry handling per item (failed items go back on the queue with backoff), and a job that survives a process restart partway through. See our bulk GSTIN verification guide for the end-to-end version of this pattern applied specifically to GSTIN lookups.

When a webhook beats polling

Polling — repeatedly asking "is it done yet" — is simple to build but wasteful when the answer is "no" most of the time, and it adds latency equal to your polling interval in the best case. A webhook (or any callback pattern) inverts this: the caller registers where to be notified, and the server pushes a single event exactly when there is something to report. This is the right choice whenever the underlying work is asynchronous and variable in duration — a bulk verification job over a large CSV, for instance, where individual lookups are fast but the batch as a whole might take minutes.

For a single, synchronous GSTIN lookup like the one this API exposes, polling does not apply at all — the response comes back in the same request. Webhooks become relevant the moment you build bulk or scheduled verification on top of it and want to be told when a batch finishes rather than checking a status endpoint in a loop.

Frequently asked questions

A fixed window counts requests in a clock-aligned slice of time (e.g. per calendar minute) and resets sharply at the boundary, which allows a burst right at the reset. A token bucket refills capacity continuously at a steady rate, so it smooths bursts and gives a more even, predictable ceiling on request rate.
Stop retrying immediately. Read the Retry-After header if present and wait at least that long, or fall back to exponential backoff with jitter if the header is absent. Retrying instantly on a 429 makes the situation worse for you and everyone else sharing the same limit.
Without jitter, every client that got rate-limited at the same moment retries at exactly the same later moment, recreating the spike. Adding a random component spreads those retries out in time so they do not all land together again.
An idempotency key is a unique value you attach to a request so that retrying it — after a timeout or a dropped connection — is guaranteed not to be double-processed. For a read-only GET lookup like GSTIN verification, a natural idempotency key already exists: the GSTIN itself, since looking it up twice returns the same result and, on this API, only a successfully validated lookup consumes a credit.
Use a webhook or callback pattern when verification is asynchronous or high-latency by nature — for example, a bulk job checking thousands of GSTINs. Polling a status endpoint every few seconds for a job that takes minutes wastes calls and adds latency; a callback that fires once when the job completes is more efficient for both sides.
A circuit breaker tracks recent failure rates for calls to a given dependency and, once failures cross a threshold, stops sending new requests for a cooldown period instead of letting every caller time out individually. It protects your own application's threads/connections and gives the downstream API room to recover.

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