,

Build a Vendor Risk Score in Python With the IndiaCos and eCourtsIndia APIs

A complete Python script that scores vendors from public Indian records: GST and MCA status from IndiaCos, defaulter lists, court cases from eCourtsIndia Case Search v2, stated weights, error handling, caching and costs, with a live run on three real companies.

·

·

eCourtsIndia Knowledgebase

Cover: build a vendor risk score in Python with the IndiaCos and eCourtsIndia APIs, five public signals, a capped 0 to 100 score, about Rs 1.80 per vendor and live test scores of 1, 12 and 100

A vendor risk score you can explain line by line, built only from public Indian records.

This tutorial builds a vendor risk score in Python from a list of GSTINs or CINs. It calls the IndiaCos API for GST, company, defaulter-list and case data, and the eCourtsIndia Case Search v2 API for litigation, adds points from a weight table you can read, and writes a CSV your procurement team can open.

A vendor risk score is a number built from public signals about a supplier, such as registration status, tax filing, defaulter lists and litigation, used to decide which vendors need a closer human look.

eCourtsIndia API accounts start with ₹200 of free credits, no card. Keys live in environment variables, never in code.

Last updated: 3 November 2026

Key takeaways

  • Five public signals: MCA status, GST registration and GSTR-3B recency, annual return recency, defaulter lists, and court cases. Each maps to one or two API calls.
  • Every point traces to one line of a weight table. The total is capped at 100, with bands low (0 to 24), review (25 to 49) and high (50 and above).
  • Real run, 3 November 2026: Billion Insights Ventures scored 1, Tata Consultancy Services 12 and Kingfisher Airlines (under liquidation, on wilful defaulter lists) 100.
  • Cost: ₹1.20 on IndiaCos plus ₹0.60 on eCourtsIndia per CIN at pay-as-you-go rates, so about ₹1,800 for 1,000 vendors. A 7-day cache makes re-runs free.
  • A score is a triage aid, not a credit decision. It tells you whom to read first; a person still reads the records.

What goes into a vendor risk score, and why these signals?

A useful vendor risk score uses signals that are public, current and hard to fake: whether the company exists in good standing, whether it files GST returns, whether lenders have listed it, and what courts record about it. Revenue, references and pitch decks matter too, but they come from the vendor. These come from registers.

Our vendor KYB guide covers the manual version of this check for procurement and finance teams. This post automates it. We keep the model deliberately plain: a points table, not machine learning, because a vendor who is turned down will ask why, and “the model said so” is not an answer you want to give.

SignalAPI callWhy it mattersPoints
MCA status not ActiveIndiaCos GET /company/{cin}Struck off or in liquidation means the entity cannot perform40
No Active GSTINSame call (gst_registrations)Invoices without a live GSTIN break input tax credit15
Last GSTR-3B older than 60 days, or noneSame call (gst_filings) or GET /gst/{gstin}Return filing stops before businesses do10
Last annual return more than 2 years oldSame call (last_annual_return_year)Long non-filing leads to Section 164(2) problems for directors10
Defaulter lists (exact name)IndiaCos GET /defaulter/searchLenders and regulators have already flagged it2 to 30 per list, cap 40
Court-case flagsIndiaCos GET /company/{cin}/casesInsolvency, fraud, recovery and cheque-bounce matters5 to 15 per flag
Pending court caseseCourtsIndia GET /api/partner/v2/searchOpen disputes, counted gently2 × log10(1 + pending), cap 10
Signals, endpoints and weights used in the script. Endpoints from the IndiaCos OpenAPI file and eCourtsIndia API docs, read 3 November 2026.

What do you need before you start?

Python 3 with the requests library, an IndiaCos API key and, for the court half, an eCourtsIndia Partner API token. We tested on Python 3.13 with requests 2.34.

  • IndiaCos key. Sign up at indiacos.com/signup with an email code; there is no approval step. New accounts get ₹50 of credit, about 125 calls. Base URL https://indiacos.com/api/v1, sent as Authorization: Bearer.
  • eCourtsIndia token. Create an account on ecourtsindia.com/api and generate a token that starts eci_live_ (41 characters). New accounts get ₹200 of free credits with no card. Base URL https://webapi.ecourtsindia.com.
  • A vendor list. One GSTIN (15 characters) or CIN (21 characters) per line in a text file.

Export both keys in your shell or your secrets manager. Without them you get a clean 401, which is what we saw when we called both APIs with no key on 3 November 2026:

# IndiaCos, no key
HTTP 401 {"error":"api_key_required","message":"An API key is required (Authorization: Bearer <key>). ..."}

# eCourtsIndia v2 search, no token
HTTP 401 {"error":{"code":"INVALID_TOKEN","message":"Bearer token is invalid or malformed","details":null},
          "meta":{"request_id":"40025cc5-0012-8c00-b63f-84710c7967bb"}}

How does the script call each API?

Four IndiaCos calls at most per vendor and one eCourtsIndia search, all plain GET requests. The paths come from the IndiaCos OpenAPI file at /api/v1/openapi.json and the eCourtsIndia API docs.

  1. GET /gst/{gstin} when the input is a GSTIN. It returns the legal name, status, return filings and, when the business is a company, a company.cin to follow. A GSTIN not yet on record is read live from the GST portal and may answer 202 first; that is not billed, and the script waits for Retry-After.
  2. GET /company/{cin}. One call gives status, the date MCA status was checked, last annual return year, charges, every GSTIN with its status, and recent GST filings.
  3. GET /defaulter/search?q=...&limit=50. The script searches the name without “Limited”, because lists print “Ltd”, “LTD” and “Ltd.”, then keeps only entries whose normalised name equals the company’s, or whose CIN matches.
  4. GET /company/{cin}/cases. A case summary with counts and flags for insolvency, fraud, recovery and cheque bounce.
  5. eCourtsIndia GET /api/partner/v2/search?litigants=...&nameMatchMode=phrase&pageSize=1&facets=caseStatus. One page of one result is enough, because we only need totalHits and the PENDING count in the caseStatus facet. Array parameters repeat the key (litigants=A&litigants=B); a comma is part of a value.

Case Search v2 is the endpoint to build on. The docs mark v1 as “Deprecated soon” with no shutdown date; if you have v1 code, move off v1 now. v2 takes the same parameters and returns the same shape plus data.facetMode, and it computes facet counts only when the match set is small enough, which company names almost always are. The eCourtsIndia API reference covers every endpoint group, and the IndiaCos API guide covers the other IndiaCos lookups.

The full Python script

Here is the whole file, about 220 lines. It compiles with python3 -m py_compile and is the exact file we ran for the results below. Save it as vendor_risk.py.

"""
vendor_risk.py: a transparent vendor risk score from IndiaCos and eCourtsIndia.

Usage:
    export INDIACOS_KEY=...        # from https://indiacos.com/account (keys page)
    export ECOURTS_API_KEY=...     # eci_live_... from your eCourtsIndia dashboard (optional)
    python vendor_risk.py vendors.txt scores.csv

vendors.txt holds one GSTIN (15 characters) or CIN (21 characters) per line.
The score is a triage aid for a human reviewer. It is not a credit decision.
"""
import csv
import datetime as dt
import hashlib
import json
import math
import os
import re
import sys
import time
from pathlib import Path

import requests

INDIACOS = "https://indiacos.com/api/v1"
ECOURTS = "https://webapi.ecourtsindia.com/api/partner/v2/search"
CACHE_DIR = Path(".risk_cache")
CACHE_TTL_DAYS = 7
TODAY = dt.date.today()

# Price per successful call, in rupees (published pricing pages, 3 November 2026).
PRICE = {"indiacos": 0.40, "ecourts": 0.60}  # eCourts v2 search: 0.60 Pay As You Go, 0.20 base

# ---- Weights: every point in the score comes from one of these lines ----
W_STATUS_NOT_ACTIVE = 40      # MCA status is not Active (struck off, under liquidation...)
W_NO_ACTIVE_GSTIN = 15        # no GST registration with status Active
W_GST_RETURNS_STALE = 10      # latest GSTR-3B on record is older than 60 days, or none
W_ANNUAL_RETURN_STALE = 10    # last annual return more than two years old
DEFAULTER_POINTS = {          # per distinct list the entity appears on, as published
    "wd_sf": 30, "wd_nsf": 30,            # wilful defaulters (suit filed / not filed)
    "ld_sf": 15, "sebi_recovery": 15,     # large defaulters suit filed, SEBI recovery
    "gem_debarred": 20, "worldbank": 20,  # procurement debarments
    "broker_default": 10, "suspended": 10, "delisted": 10,
    "esic": 5, "mca_alert": 5, "listing_fines": 2,
}
DEFAULTER_CAP = 40
CASE_FLAG_POINTS = {"insolvency": 15, "fraud": 5, "recovery": 5, "chequeBounce": 5}
PENDING_CAP = 10              # 2 x log10(1 + pending cases), so volume counts, gently


class StopRun(Exception):
    """A problem that every later call would hit too (bad key, empty wallet)."""


class Client:
    def __init__(self, name, base_headers):
        self.name = name
        self.session = requests.Session()
        self.session.headers.update(base_headers)
        self.billed_calls = 0

    def get(self, url, params=None, tries=5):
        key = hashlib.sha256(json.dumps([url, sorted((params or {}).items())]).encode()).hexdigest()
        cached = CACHE_DIR / f"{key}.json"
        if cached.exists():
            age = time.time() - cached.stat().st_mtime
            if age < CACHE_TTL_DAYS * 86400:
                return json.loads(cached.read_text())

        for attempt in range(tries):
            r = self.session.get(url, params=params, timeout=90)
            if r.status_code == 200:
                self.billed_calls += 1
                CACHE_DIR.mkdir(exist_ok=True)
                cached.write_text(r.text)
                return r.json()
            if r.status_code in (404, 422):
                return None   # 404 not found; 422 IndiaCos will not run court searches on a private person's name
            if r.status_code == 401:
                raise StopRun(f"{self.name}: 401, the key is missing, wrong or revoked")
            if r.status_code == 402:
                raise StopRun(f"{self.name}: 402, the wallet or credits are empty; top up and re-run")
            if r.status_code in (202, 429, 500, 502, 503, 504):
                wait = int(r.headers.get("Retry-After", 0)) or min(60, 2 ** attempt * 3)
                if r.status_code == 429 and "quota_exceeded" in r.text:
                    raise StopRun(f"{self.name}: daily cap reached; it resets at midnight IST")
                time.sleep(wait)
                continue
            raise RuntimeError(f"{self.name} {r.status_code}: {r.text[:300]}")
        raise RuntimeError(f"{self.name}: gave up on {url} after {tries} tries")


def norm(name):
    n = re.sub(r"[^A-Z0-9 ]", " ", (name or "").upper().replace("&", " AND "))
    n = re.sub(r"\bLTD\b", "LIMITED", n)
    n = re.sub(r"\bPVT\b", "PRIVATE", n)
    return re.sub(r"\s+", " ", n).strip()


def court_name(legal_name):
    """'KINGFISHER AIRLINES LIMITED' -> 'KINGFISHER AIRLINES', so Ltd and Limited both match."""
    return re.sub(r"\s+(PRIVATE\s+)?LIMITED$", "", norm(legal_name))


def latest(dates):
    good = [dt.date.fromisoformat(d[:10]) for d in dates if d]
    return max(good) if good else None


def check(identifier, icos, eci):
    row = {"input": identifier, "notes": []}
    gst, cin = None, None
    if len(identifier) == 15:
        gst = icos.get(f"{INDIACOS}/gst/{identifier}")
        if gst is None:
            row["notes"].append("GSTIN not found")
            return row
        cin = (gst.get("company") or {}).get("cin")
    else:
        cin = identifier

    company = icos.get(f"{INDIACOS}/company/{cin}") if cin else None
    if cin and company is None:
        row["notes"].append("CIN not found")
    rec = company or {}
    name = rec.get("name") or (gst or {}).get("legal_name", "")
    row.update(cin=cin or "", name=name)

    # 1. Register status
    status = rec.get("official_status") or rec.get("status") or (gst or {}).get("status", "")
    row["status"] = status
    points = {"status": W_STATUS_NOT_ACTIVE if status and status.lower() != "active" else 0}

    # 2. GST: any Active registration, and how recent the last GSTR-3B is
    regs = rec.get("gst_registrations") or ([{"status": gst.get("status")}] if gst else [])
    row["gstins_active"] = sum(1 for g in regs if g.get("status") == "Active")
    points["gst_active"] = 0 if row["gstins_active"] else W_NO_ACTIVE_GSTIN
    filings = rec.get("gst_filings") or (gst or {}).get("return_filings") or []
    last_3b = latest(f.get("filed_on") for f in filings if f.get("form") == "GSTR3B")
    row["last_gstr3b"] = last_3b.isoformat() if last_3b else ""
    points["gst_returns"] = 0 if last_3b and (TODAY - last_3b).days <= 60 else W_GST_RETURNS_STALE

    # 3. MCA annual return (a brand-new company has not filed one yet; that is not a red flag)
    ar = rec.get("last_annual_return_year")
    inc = rec.get("incorporation_date")
    new_company = inc and (TODAY - dt.date.fromisoformat(inc)).days < 730
    row["last_annual_return"] = ar or ""
    if ar is None and new_company:
        points["annual_return"] = 0
        row["notes"].append("incorporated under 2 years ago")
    else:
        points["annual_return"] = W_ANNUAL_RETURN_STALE if (ar or 0) < TODAY.year - 2 else 0
    row["charges_open"] = rec.get("charges_open", "")

    # 4. Defaulter lists: exact name or CIN only. A similar name is not the same entity.
    #    Search without "Limited" (lists print "Ltd", "LTD", "Ltd."), then keep exact matches.
    hits = icos.get(f"{INDIACOS}/defaulter/search", {"q": court_name(name), "limit": 50}) or {}
    mine = [h for h in hits.get("results", [])
            if norm(h.get("name")) == norm(name) or (cin and h.get("cin") == cin)]
    lists = sorted({l["key"] for h in mine for l in h.get("lists", [])})
    row["defaulter_lists"] = " ".join(lists)
    points["defaulters"] = min(DEFAULTER_CAP, sum(DEFAULTER_POINTS.get(k, 0) for k in lists))

    # 5. Court cases: IndiaCos flags, then eCourtsIndia v2 search for the live pending count
    cases_url = f"{INDIACOS}/company/{cin}/cases" if cin else f"{INDIACOS}/gst/{identifier}/cases"
    cases = icos.get(cases_url) or {}
    flags = cases.get("flags") or {}
    row["case_flags"] = " ".join(f"{k}:{v}" for k, v in flags.items() if v)
    points["case_flags"] = sum(p for k, p in CASE_FLAG_POINTS.items() if flags.get(k))
    pending = cases.get("pending", 0)
    row["court_source"] = "indiacos"
    proprietor = "proprietor" in ((gst or {}).get("constitution") or "").lower()
    if proprietor:
        row["notes"].append("proprietorship: no name search on a private individual")
    if eci and not proprietor:
        res = eci.get(ECOURTS, {"litigants": court_name(name), "nameMatchMode": "phrase",
                                "pageSize": 1, "facets": "caseStatus"}) or {}
        data = res.get("data", {})
        row["court_records"] = data.get("totalHits", "")
        pending = (data.get("facets", {}).get("caseStatus", {}).get("values", {})).get("PENDING", 0)
        row["court_source"] = "ecourtsindia"
    row["pending_cases"] = pending
    points["pending"] = min(PENDING_CAP, round(2 * math.log10(1 + pending)))

    total = min(100, sum(points.values()))
    row["score"] = total
    row["band"] = "high" if total >= 50 else "review" if total >= 25 else "low"
    row["points"] = " ".join(f"{k}:{v}" for k, v in points.items() if v)
    row["notes"] = "; ".join(row["notes"])
    return row


def main(src, dst):
    ids = list(dict.fromkeys(l.strip().upper() for l in open(src) if l.strip()))  # dedupe, keep order
    icos = Client("IndiaCos", {"Authorization": f"Bearer {os.environ['INDIACOS_KEY']}"})
    eci_key = os.environ.get("ECOURTS_API_KEY")
    eci = Client("eCourtsIndia", {"Authorization": f"Bearer {eci_key}"}) if eci_key else None
    rows = []
    try:
        for i in ids:
            rows.append(check(i, icos, eci))
    except StopRun as e:
        print(f"Stopped early: {e}", file=sys.stderr)
    cols = ["input", "cin", "name", "status", "gstins_active", "last_gstr3b", "last_annual_return",
            "charges_open", "defaulter_lists", "case_flags", "court_source", "court_records",
            "pending_cases", "points", "score", "band", "notes"]
    tmp = Path(dst + ".tmp")
    with open(tmp, "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore")
        w.writeheader()
        w.writerows(rows)
    tmp.replace(dst)                                  # never leaves a half-written CSV behind
    cost = icos.billed_calls * PRICE["indiacos"] + (eci.billed_calls * PRICE["ecourts"] if eci else 0)
    print(f"{len(rows)} vendors scored, about Rs {cost:.2f} spent (cached calls cost nothing)")


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

Run it with python vendor_risk.py vendors.txt scores.csv. If ECOURTS_API_KEY is not set, the script still runs and uses IndiaCos’s own pending count from the case summary, and marks the row court_source: indiacos so a reviewer knows which source the number came from.

How are the points calculated, and why these weights?

Register failures weigh most, defaulter lists next, and court volume least. The weights are a starting point that you should argue with, not a standard.

  • Status (40). A company that is struck off or under liquidation cannot deliver a contract. On its own this puts a vendor in the review band; with anything else it goes high.
  • GST (15 and 10). No Active GSTIN means the vendor cannot issue a valid tax invoice from that registration. A GSTR-3B more than 60 days old is an early warning, because filing often stops before trading does.
  • Annual return (10). More than two years without one is the road to the Section 164(2) disqualification our director check guide explains. A company under two years old gets no points for having filed nothing yet; the script notes it instead.
  • Defaulter lists (2 to 30, capped at 40). A wilful-default classification scores 30; a large default with a suit, SEBI recovery or a procurement debarment 15 to 20; a stock exchange listing fine only 2. The cap stops one borrower on many lists from drowning every other signal.
  • Court flags (5 to 15). Insolvency scores 15, fraud, recovery and cheque bounce 5 each. Labour, tax and consumer flags are reported in the CSV but not scored, because large employers and taxpayers carry them as a matter of course.
  • Pending cases (up to 10). Two times log10 of one plus the pending count. One pending case scores 1, about 30 score 3, about 400 score 5. A logarithm keeps a large, litigious and perfectly healthy company from looking worse than a small one in real trouble.

What did the score return on three real vendors?

On 3 November 2026 the script scored our own company 1, Tata Consultancy Services 12 and Kingfisher Airlines 100. We chose them because they span the range and are all public companies or ourselves.

How we ran it: the IndiaCos half ran live against the REST API with our key, 10 billable calls for a fresh run of three vendors (₹4.00). We had no eCourtsIndia REST token in the test environment, so we ran the same three Case Search queries (litigants in phrase mode) through the eCourtsIndia MCP’s search_cases tool, which reads the same index, and fed those counts into the script’s cache. Every value below came from a live call that day. Columns trimmed:

input,name,status,gstins_active,last_gstr3b,last_annual_return,defaulter_lists,court_records,pending_cases,points,score,band
09AAOCB3302M1ZH,BILLION INSIGHTS VENTURES PRIVATE LIMITED,Active,1,2026-10-17,,,1,1,pending:1,1,low
L22210MH1995PLC084781,TATA CONSULTANCY SERVICES LIMITED,Active,87,2026-10-19,2025,listing_fines,1414,434,defaulters:2 case_flags:5 pending:5,12,low
L85110KA1995PLC018045,KINGFISHER AIRLINES LIMITED,Under Liquidation,0,,2013,delisted ld_sf suspended wd_sf,745,115,status:40 gst_active:15 gst_returns:10 annual_return:10 defaulters:40 case_flags:15 pending:4,100,high
VendorWhat the records showedScore
Billion Insights Ventures (GSTIN input)Active (MCA status checked 28 October 2026), one Active GSTIN, GSTR-3B for September filed 17 October 2026, incorporated 15 January 2026 so no annual return yet, no defaulter entries, no case flags, one pending writ petition in a High Court, in which the company is one of eight respondents1, low
Tata Consultancy ServicesActive, 87 Active GSTINs, GSTR-3B filed 19 October 2026, annual return 2025, one entry on the list of listed companies fined for non-compliance, as published by the exchanges (BSE and NSE, ₹1,26,440), 6 cheque-bounce and 3 labour flags, 1,414 court records naming it with 434 pending12, low
Kingfisher AirlinesUnder Liquidation, no Active GSTIN and no GSTR-3B on record, last annual return 2013, on the wilful defaulters and large defaulters (suit filed) lists plus BSE and NSE suspension and delisting records, fraud, recovery and 37 cheque-bounce flags, 745 court records with 115 pending100 (capped from 134), high
Live results, 3 November 2026: IndiaCos REST API and eCourtsIndia MCP search_cases. Defaulter entries as published.

Read TCS’s row carefully. The listing-fine entry is shown as published by the exchanges, and the script gives it the lowest weight in the table, 2 points against 30 for a wilful-default classification, because an exchange fine for non-compliance is a different kind of fact. The entry tells you only that the company appears on that published list; read the exchange’s notice before you draw any conclusion from it. Its court volume is large because it is a large company: IndiaCos’s case summary counted 756 records filed by TCS against 581 naming it as respondent. A score of 12 says “standard onboarding”, which is the right answer for a company of that size whose registers show it active and filing.

Our own row shows how to read a young company. It was incorporated on 15 January 2026, so there is no annual return to read yet, and its directors are not yet on the company record: check the company’s director list as well as the DIN record before you rely on either. The script treats the missing return as “new”, not “clean” or “risky”, and says so in the notes column. The one pending matter, a writ petition in a High Court in which the company is one of eight respondents, adds a single point; a pending case is not a finding against anyone.

The bug the live run caught

Our first live run scored Kingfisher Airlines 94, not 100, with no defaulter lists at all. The reason: the script searched the defaulter lists for “KINGFISHER AIRLINES LIMITED”, and the lists print “Ltd.”, “LTD” and “Ltd”. The search found nothing. Searching without the suffix returned 9 records, all of which normalise to the exact legal name. That is why the script searches broad and filters strict, and it is a good reason to test any scoring code on a vendor whose answer you already know.

How does the script handle errors, retries and rate limits?

It stops the whole run on problems every later call would hit (a bad key, an empty wallet, a daily cap), retries the temporary ones with backoff, and treats not-found as an answer. Both APIs bill only successful calls, so retries of failed calls cost nothing.

StatusIndiaCoseCourtsIndiaWhat the script does
200Success, billed ₹0.40 (including an empty search)Success, billedCaches and returns the JSON
202GSTIN being read live from the GST portal; not billedNot used on searchWaits for Retry-After, retries
401api_key_requiredINVALID_TOKEN, TOKEN_INACTIVE, TOKEN_EXPIREDStops the run
402insufficient_balanceINSUFFICIENT_CREDITS, SUBSCRIPTION_REQUIREDStops the run, keeps the CSV so far
404not_foundNot foundRecords “not found” and moves on
422individual: no court searches on private individuals’ namesSkips the court summary
429quota_exceeded: 100,000 calls a day per key, resets midnight ISTRATE_LIMIT_EXCEEDED: 100 a minute, 3,000 an hour, 50,000 a day, 10 at onceDaily cap stops; per-minute limits back off and retry
5xxcases_unavailable (503): retry after a minuteINTERNAL_ERROR (500, not charged)Backs off 3, 6, 12, 24 seconds and retries
Error handling, from the IndiaCos API docs and the eCourtsIndia error code reference, read 3 November 2026.

The privacy rule deserves a word. If the input is a GSTIN held by a proprietorship, the business name is often a person’s name. IndiaCos refuses court searches on private individuals’ names with a 422, and the script also skips the eCourtsIndia name search for proprietorships and says so in the notes. For a person, use an identity-aware check such as the LegalCheck API, with a lawful purpose, rather than a raw name search.

What does it cost to score 1,000 vendors?

About ₹1,800 at pay-as-you-go rates, or ₹1,400 with eCourtsIndia’s base rate, for 1,000 vendors given by CIN. The arithmetic uses prices published on 3 November 2026.

ItemCalls per vendorRatePer vendor1,000 vendors
IndiaCos: company, defaulter search, cases3₹0.40₹1.20₹1,200
IndiaCos: extra GST call when the input is a GSTIN1₹0.40₹0.40₹400
eCourtsIndia Case Search v21₹0.60 PAYG, ₹0.20 base₹0.60 or ₹0.20₹600 or ₹200
Total (CIN input)4₹1.80 or ₹1.40₹1,800 or ₹1,400
Cost to score vendors, from indiacos.com/api/docs and ecourtsindia.com/api/pricing, read 3 November 2026. Prices before 18% GST on top-ups.

IndiaCos top-ups come in ₹500, ₹5,000 and ₹50,000 packs plus GST (1,250, 12,500 and 1,25,000 calls). eCourtsIndia pay-as-you-go deposits are ₹1,000, ₹2,500 and ₹5,000 plus GST, and the credits never expire. A list endpoint on IndiaCos returns at most 100 records per call, which matters if you extend this to /companies lead lists, but not for one-at-a-time lookups like these.

How do caching and idempotency work here?

Every call is a GET, so re-running is safe; the cache makes it free. The script hashes the URL and parameters, stores each successful response in .risk_cache/ for seven days, and reads from there first.

  • Resume after a stop. If a 402 stops the run at vendor 600, top up and run again. The first 600 come from the cache at no cost, and the CSV is rewritten in full.
  • No half-written files. The CSV is written to a temporary file and moved into place, so a crash never leaves a truncated report for someone to trust.
  • Duplicates are dropped. The input list is de-duplicated in order, so a vendor listed twice is billed once.
  • Freshness is a choice. Seven days suits onboarding. For monthly monitoring, clear the cache before each run, or lower CACHE_TTL_DAYS. IndiaCos’s /gst/{gstin}?refresh=true re-reads a GSTIN from the portal unless it was read in the last 30 days.
  • Keep the cache internal. The IndiaCos licence allows internal business use, commercial included, and showing individual records to your own users; reselling, redistributing or building a competing database needs a written licence. A cache on your own server for your own reviews is the first kind.

How should you use the score, and how should you not?

Use the band to decide who reads the file and how soon, never to decide the outcome on its own. A practical routing:

BandScoreAction
Low0 to 24Standard onboarding; re-score at renewal
Review25 to 49A reviewer reads the records behind the points and asks the vendor to explain
High50 and aboveEscalate to legal or credit; no advance payment until reviewed
Suggested routing by band. Adjust to your own risk appetite.

Then extend it where your risk sits. Add a director pass using /company/{cin}/directors and /director/{din} from the director check guide. Lenders should add collateral and recovery suits from the borrower due diligence workflow. If you would rather ask than code, the same tools work from Claude or ChatGPT through the IndiaCos and eCourtsIndia MCP servers. For the broader design of a due diligence engine on court data, see building a legal due diligence engine on the eCourtsIndia API, and for production concerns such as refresh and SLAs, from scraping to production.

What this does not tell you

The score summarises what public registers say on the day you run it. It does not judge the vendor.

  • A name match is not identity. The court count is a phrase search on the company name; it can include a namesake company, and our study of same-name false matches shows how often that happens.
  • Defaulter entries are as published. A borrower may have settled, appealed or been removed, and a similar name is not the same entity. The script keeps exact matches only for that reason.
  • Pending is not guilt, and disposed is a status, not an outcome. The score counts pending cases; it does not know who is right in any of them.
  • Records are not certified copies. MCA and GST data are as read on the dates shown; partner data such as charges and filings was refreshed 23 October 2026 at the time of writing.
  • It is not a credit decision. No financials, bureau scores, sanctions or references are in it. Do not use it as the sole basis to refuse anyone.

Sources and method

  • IndiaCos OpenAPI (paths and parameters), API docs (errors, billing, limits) and llms.txt (licence and coverage), read 3 November 2026.
  • eCourtsIndia API docs (Case Search v2, array parameters, error codes, rate limits) and pricing, read 3 November 2026.
  • Live IndiaCos REST calls on 3 November 2026 for 09AAOCB3302M1ZH, U62090UP2026PTC241369, L22210MH1995PLC084781 and L85110KA1995PLC018045 (GST, company, defaulter search, cases), using the script above.
  • eCourtsIndia MCP search_cases, litigants in phrase mode, for “Billion Insights Ventures”, “Tata Consultancy Services” and “Kingfisher Airlines”, 3 November 2026; the same index the v2 REST search reads.
  • Unauthenticated calls to both APIs on 3 November 2026 for the 401 examples.
  • Script compiled with python3 -m py_compile and run on Python 3.13 with requests 2.34.

Frequently asked questions

How do I build a vendor risk score in Python?

Pick public signals (company status, GST registration and filing, annual returns, defaulter lists, court cases), fetch each with an API call, give each signal a stated number of points, add them up with a cap and map the total to bands. Keep the weights in one table so every score can be explained. See the IndiaCos API docs.

Which API verifies GSTIN status in Python?

The IndiaCos API’s GET /api/v1/gst/{gstin} returns the legal name, status, registration and cancellation dates, return filings by period and the linked company. Call it with the requests library and an Authorization: Bearer header. Each successful call costs ₹0.40, and new accounts get ₹50 of credit. Read the IndiaCos API guide.

How do I search Indian court cases by company name with an API?

Use eCourtsIndia Case Search v2, GET /api/partner/v2/search on webapi.ecourtsindia.com, with litigants set to the company name and nameMatchMode set to phrase. Add facets=caseStatus to get pending and disposed counts in the same call. Array parameters repeat the key. Move off v1, which is marked deprecated. See the eCourtsIndia API docs.

What does it cost to score 1,000 vendors?

With CINs as input, each vendor takes three IndiaCos calls at ₹0.40 and one eCourtsIndia search at ₹0.60 pay-as-you-go, so about ₹1.80 a vendor and ₹1,800 for 1,000, or ₹1,400 at the eCourtsIndia base rate. A GSTIN input adds one call. Failed calls are free, and caching makes re-runs free. See eCourtsIndia pricing.

How should my code handle 401, 402 and 429 errors?

Stop the run on 401 (bad or missing key) and 402 (empty wallet or credits), because every later call will fail the same way; keep what you have and resume after fixing it. Back off and retry on per-minute 429s and 5xx errors, and stop on a daily cap. Both APIs bill only successful calls. Read the developer quickstart.

Is a vendor risk score enough to reject a supplier?

No. It is a triage aid that tells you which vendor files to read first. A high score means a person should read the records behind the points, ask the vendor to explain and take advice where needed. A name match is not identity and a defaulter entry is as published, so never refuse anyone on the number alone. See checking a vendor or employer.

Can I store IndiaCos API results in my own database?

For your own internal use, yes: the IndiaCos licence allows internal business use, commercial included, and showing individual records to your users. Reselling or redistributing the data, building a competing database, bulk scraping and AI training need a written licence from support@indiacos.com. Read the IndiaCos usage terms.

Important: This tutorial shows how to combine public company, tax, defaulter-list and court records into a triage score. It is not legal advice and not a credit, procurement or employment decision. Records are shown as published on the date read and are not certified copies. A name match is not identity; a defaulter-list entry is as published and a similar name is not the same entity; pending is not guilt, and disposed is a status, not an outcome. The weights are examples, not a standard. IndiaCos and eCourtsIndia are built by Billion Insights Ventures Pvt. Ltd., Noida (CIN U62090UP2026PTC241369), a private company not affiliated with any court or government body. Billion Insights Ventures Pvt. Ltd. holds ISO 9001:2015, ISO/IEC 20000-1:2018 and ISO/IEC 27001:2022 certification (9001, 20000-1, 27001). Corrections: support@ecourtsindia.com.

Run it on three vendors you already know well. If the score surprises you, read the points column before you change a weight.

And for the court half, get an eCourtsIndia API token (₹200 free) or read the overview of all three Billion Insights APIs.

eCourtsIndia is a private legal-technology platform. It is not affiliated with, associated with, or endorsed by the Government of India, the Supreme Court of India or its e-Committee, or any court. Official case information is published on ecourts.gov.in. Always verify details against official court records or certified copies. This article is general information, not legal advice. Spotted an error? Write to support@ecourtsindia.com.

Search 35 crore+ Indian court case records, free

One search across the Supreme Court, all 25 High Courts, district courts and 18 tribunal and commission types. Hearing alerts, AI summaries and an API for developers.