GST API Rate Limits, Retries and Webhooks Explained
How to design resilient integrations against GST verification API rate limits, timeouts, retries, and webhook callbacks.
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.
| Property | Fixed window | Token bucket |
|---|---|---|
| Burst behaviour | Can double at window boundary | Smoothed, no hard edge |
| Implementation cost | Very low | Low to moderate |
| Fairness under load | Weaker | Stronger |
| Typical client experience | Occasional sharp 429 spikes | Gradual throttling |
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 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 developer integration guides
Browse every guide in the GST API blog.