Handle India API errors, rate limits and retries
Every error has the same JSON shape; rate limits are weighted per endpoint and told in RateLimit headers. A reusable client in Python and JavaScript with timeouts, retries and backoff.
Every endpoint answers errors the same way, so one small wrapper handles them all. This guide shows what can go wrong, what each status means, and a client that retries only what is worth retrying.
One error shape
A successful answer is { "data": ..., "meta": ... }. An error is always:
{"error": {"code": "not_found", "message": "No post office found for pincode 999999."}}
code is for your program, message for people (it says what to fix). That one is the answer to GET /v1/pincode/999999, a pincode that does not exist.
| Status | Codes | What to do |
|---|---|---|
| 400 | bad_request |
Your input is wrong; show the message, do not retry |
| 401 | key_required, invalid_key |
Send a key (Authorization: Bearer <key> or X-API-Key); do not retry |
| 404 | not_found |
The thing does not exist (an unknown pincode or IFSC); do not retry |
| 405 | method_not_allowed |
Wrong method; fix the code |
| 413 | too_large |
The body is over the endpoint's limit; send less |
| 429 | rate_limited, quota_exceeded, token_quota_exceeded |
Wait for Retry-After seconds, then retry |
| 503 | busy, dataset_unavailable, not_available |
Retry after Retry-After; a model or dataset is loading or the server is busy |
| 500 | internal_error |
Retry once later; report it if it stays |
Weighted rate limits
Without a key each address has 600 units a minute; a free key has 3,000. A lookup costs 1 unit, heavier work more (a file upload 15, a model 30). Every charged answer says where you stand:
RateLimit-Limit: 600
RateLimit-Remaining: 559
RateLimit-Reset: 50
RateLimit-Reset is the seconds until the minute's allowance refills. Model endpoints also send X-Quota-Remaining (requests left today) and the LLM endpoints X-Token-Remaining.
Cache what does not change
A pincode or an IFSC rarely changes, and answers carry Cache-Control and an ETag. Keep answers for a day in your own cache, and send If-None-Match with the ETag to get a free 304 Not Modified when nothing changed. A logo answered from cache does not count against the limit at all.
A client in Python
import random
import time
import requests
BASE = "https://api.dharun.dev/v1"
RETRY = {429, 500, 502, 503, 504}
class IndiaAPIError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status, self.code = status, code
def get(path, params=None, key=None, tries=4):
headers = {"X-API-Key": key} if key else {}
for attempt in range(tries):
try:
r = requests.get(f"{BASE}{path}", params=params, headers=headers, timeout=10)
except requests.RequestException:
if attempt == tries - 1:
raise
time.sleep(2 ** attempt + random.random())
continue
if r.ok:
return r.json()["data"]
if r.status_code in RETRY and attempt < tries - 1:
wait = float(r.headers.get("Retry-After", 2 ** attempt))
time.sleep(min(wait, 60) + random.random())
continue
error = r.json().get("error", {}) if r.headers.get("Content-Type", "").startswith("application/json") else {}
raise IndiaAPIError(r.status_code, error.get("code", "http_error"), error.get("message", r.text[:200]))
try:
print(get("/pincode/637001")[0]["district"])
except IndiaAPIError as e:
if e.code == "not_found":
print("no such pincode")
else:
raise
A client in JavaScript
const BASE = "https://api.dharun.dev/v1";
const RETRY = new Set([429, 500, 502, 503, 504]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function get(path, { key, tries = 4, signal } = {}) {
for (let attempt = 0; attempt < tries; attempt++) {
let r;
try {
r = await fetch(BASE + path, {
headers: key ? { "X-API-Key": key } : {},
signal: signal ?? AbortSignal.timeout(10_000),
});
} catch (e) {
if (e.name === "AbortError" && signal?.aborted) throw e; // the caller gave up
if (attempt === tries - 1) throw e;
await sleep(1000 * 2 ** attempt + Math.random() * 500);
continue;
}
if (r.ok) return (await r.json()).data;
if (RETRY.has(r.status) && attempt < tries - 1) {
const wait = Number(r.headers.get("Retry-After")) || 2 ** attempt;
await sleep(Math.min(wait, 60) * 1000 + Math.random() * 500);
continue;
}
const body = await r.json().catch(() => ({}));
const err = new Error(body.error?.message ?? r.statusText);
err.status = r.status;
err.code = body.error?.code;
throw err;
}
}
// const branch = await get("/ifsc/SBIN0000001");
Check an IFSC with it
curl "https://api.dharun.dev/v1/ifsc/SBIN0000001?utm_medium=guide"
A 404 here means the code is not in the Reserve Bank's list: tell the user to check it, do not retry. A 429 or 503 is the only kind worth a second try.
In a browser
CORS is open, and the RateLimit-*, Retry-After, X-Quota-* and X-Token-* headers are exposed to scripts, so the same code works in a web page. Keep keys out of public pages: the endpoints most pages call need no key.
The code in this guide is MIT-0: copy it into anything, no credit needed. Updated 2026-11-12.
APIs used: Pincodes, IFSC and banks.