,

How to Use the eCourtsIndia API: A Developer’s Complete Guide with Refresh, Bulk Refresh and FAQs

eCourtsIndia API developer guide: all 23 live endpoints, refresh and bulk refresh, the legal-check.v1 schema, pricing, rate-limit codes and MCP answers.

·

·

eCourtsIndia Knowledgebase

How to use the eCourtsIndia API, cover design variant A for the eCourtsIndia blog

If you have ever written a scraper for services.ecourts.gov.in, you already know the pain. The portal is intentionally single threaded. The captcha breaks once a quarter. The PDFs are scanned images without OCR. Half your on-call rotations end with someone restarting a Selenium worker because a session cookie expired in production.

The eCourtsIndia API is what you build on when scraping is no longer your day job. This is the developer’s guide to using it end to end. We will set up authentication, walk through every partner endpoint that is live today (23 of them, in four families: Cases, Causelist, Electoral and LegalCheck), write working code in cURL and Python, and then go deep on the workflows that confuse most teams: search facets and capabilities, litigant vs general search, pagination (partner page size is 200), order retrieval, refresh and bulk-refresh status, cause lists, electoral roll lookup, and LegalCheck.

This is the long version, written from the questions real developers send us while wiring up the API. Every request shown here is taken from the live partner API surface at https://webapi.ecourtsindia.com. Core examples were first verified against the live eCourtsIndia data layer on 1 June 2026. Enum counts, search capabilities, order endpoints, cause-list paths and the sample CNR were re-checked against the live partner API and ecourtsindia.com/api/docs on 23 August 2026. Electoral Solr field mapping, MCP electoral tools, relation codes and q/address behaviour were verified live on 28 August 2026. Case-search Solr mapping (query vs litigants vs caseNumbers, courtLocationFacetPath, courtLevels) was verified live on 29 August 2026. LegalCheck partner routes (submit, status, report, list, search log, models) were verified live on 3 September 2026. Electoral search, EPIC lookup and electoral capabilities are now documented in the public API docs, and the MCP version and tool counts were re-checked on 23 September 2026. The CNR (Case Number Record, the unique 16-character case identifier used across the eCourts system) DLHC010001232024 used throughout is a real Delhi High Court writ petition disposed on 5 January 2024 by Hon’ble Mr. Justice Tushar Rao Gedela. You can run any of these calls yourself.

Last updated: 23 September 2026

What changed in this revision (15 September 2026). Breaking change on LegalCheck: the accepted model id is now eCI-1.0, not eCI-1.2. The published docs state that the current public model is eCI-1.0 for individuals and companies, that omitting config.model selects the same model, and that earlier source model labels are not accepted for new submissions. If your integration still hardcodes eCI-1.2 from an earlier version of this guide, your submits will now be rejected with 400 UNKNOWN_MODEL. Either drop config.model entirely or send eCI-1.0. Alongside that, the legal-check.v1 report has grown a great deal: a five-band risk rating with a published scoring basis, per-match severity and risk contribution, a research_urls block of public eCourtsIndia links, an engine evidence trace, an explicit watchlist block that refuses to report AML and sanctions as clear, and an ai_instructions block written for the model reading the report. Four new LegalCheck terminal failure codes are documented. Endpoint pricing is now published in full and this guide carries the table. Rewritten against ecourtsindia.com/api/docs and the pricing page on 15 September 2026.

Building with an AI coding assistant? We publish a plain text version of this entire reference, written to be pasted straight into ChatGPT, Claude, Cursor or your own model. Drop it into the context window and your assistant can answer eCourtsIndia API questions and write integration code that actually matches our endpoints. Download it here: eCourtsIndia API reference for LLMs (.txt).

How to use the eCourtsIndia API, portrait cover image for the eCourtsIndia blog

What the API gives you

The eCourtsIndia API is a REST interface to a structured layer of 32 crore+ Indian court records, served from https://webapi.ecourtsindia.com with a bearer token. It exposes case retrieval, order text and AI analysis, single and bulk refresh with status polling, full text search (plus a free capabilities catalog), cause lists, court-structure lookups, live enums, electoral roll search, and LegalCheck (scored litigation background checks), so you can build court, identity-verification and due-diligence products without scraping government portals.

The eCourtsIndia data layer covers 32 crore+ case records across the Supreme Court, all 25 High Courts, district and taluka courts in all 36 states and union territories, and 18 tribunal and commission types carrying 44 lakh+ matters. Live courtType values (31 August 2026) are NCLT, NCLAT, ITAT, CGAT (CAT), CESTAT, DRT, DRAT, SAT, TDSAT, APTEL, JAGRITI (consumer commissions), CCI, GST_AAAR, NGT, AFT, SEBI_ORDERS, RCT and GSTAT. GSTAT is live; always read the live enum rather than this list. Every record is structured, deduplicated and continuously refreshed from the official eCourts substrate. Orders and judgments are stored as both digitally signed PDFs and OCR cleaned markdown. The hard parts that every team rebuilds when scraping, court code normalisation, case type variance, party name aliasing, scanned PDF extraction, are already handled.

It serves Solr backed full text search across every uploaded order and judgment in the index, not just metadata. It returns OCR cleaned markdown for the order text, ready to paste into a model. It exposes pre computed AI analysis of orders, including summary, key points, outcome, relief granted and statutory provisions. It accepts bulk refresh requests for up to 50 CNRs in a single POST, and a free status poll so you do not pay twice for the same scrape. It returns live enum dictionaries for case type, status, court code and state. And it searches Indian electoral rolls by name, EPIC, free-text q (names and address) and household, grouped by voter ID, over REST and MCP. LegalCheck is the scored due-diligence layer on top of the same court index: submit an individual or company, poll until complete, and read a legal-check.v1 report.

If you want the strategic case for moving off the portal, we wrote about it in From Case Lookup to Case Intelligence. This post is the engineering version.

Step 1. Sign up and get your bearer token

Every endpoint is authenticated with a bearer token in the Authorization header. Tokens look like this.

eci_live_t3e5s7t9i1n2g4a6c8c0o2u4n6t8x1y3

To get one, register at ecourtsindia.com. New accounts receive ₹200 in free credits, enough to call most endpoints a few hundred times while you build the integration, and no credit card is needed to start. Pricing tiers and per call rates are listed at ecourtsindia.com/api/pricing.

The single most common integration error: the word Bearer. The header value must be Bearer, a space, then the token. If you send the raw token with no prefix, or you misspell the scheme, the API returns INVALID_TOKEN even though the token itself is perfectly valid. This trips up almost every new team.

# WRONG, returns INVALID_TOKEN
-H "Authorization: eci_live_xxx"
# CORRECT
-H "Authorization: Bearer eci_live_xxx"

Creating, naming and rotating tokens

You manage your own tokens from the dashboard at ecourtsindia.com/dashboard/settings. You do not need to email support for a key, and you should not be sharing one key by hand. From the settings page you can generate a token, and you can create more than one. Give each token a clear name tied to where it runs, for example production-backend, staging, nightly-portfolio-job or data-science-notebook. Naming tokens by use is not cosmetic. It is how you read your usage and your logs later, and it is how you revoke one environment without breaking the others. If a notebook key leaks, you rotate just that key.

A token stays valid as long as the account has credit. There is no daily expiry to manage. If a token suddenly starts returning INVALID_TOKEN in production, the usual cause is that it was rotated on the account, so log in, read the current token list under settings, and update your secret store. Treat tokens like any production secret: environment variable in development, secret manager in production, never committed to a repo, never logged.

Do not call these endpoints from browser JavaScript. Your eci_live_ token is a bearer credential, so anything that reaches the browser reaches your users, and a leaked token spends your credits until you rotate it. Call the API from your server and proxy the result to your front end. The same goes for mobile apps: ship the token to your own backend, never into the client bundle.

Every call is logged, and you can see it

You do not need to build your own audit trail just to debug with us. Every API request against your account, successful or failed, is recorded and visible in your dashboard, with the endpoint, the timestamp and the outcome. So when something looks wrong, the first stop is your own logs. You will usually see exactly which call failed and why before you even open a support thread. If you do raise something with us, the fastest path is to send the request_id from the response (it is in meta.request_id on both success and error payloads) along with the CNR. We can pull that exact call from our side.

Step 2. Set the base URL

The partner API is served at one hostname.

https://webapi.ecourtsindia.com

All endpoints in this guide use that as the base. The interactive Swagger documentation lives at ecourtsindia.com/api/docs, and a full Postman collection of every endpoint is available to partner accounts on request.

Step 3. Your first call

The simplest sanity check is to fetch the list of states. This is the entry point into the court hierarchy; it needs your Bearer token but is free, no credits are charged. One breaking change to note if you integrated early: the court-structure endpoints moved from the old /api/CauseList/court-structure/* path to /api/partner/causelist/court-structure/*, and the old path is no longer accessible to partners.

cURL

curl -X GET \
"https://webapi.ecourtsindia.com/api/partner/causelist/court-structure/states" \
-H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
-H "Accept: application/json"

Python

import requests
BASE = "https://webapi.ecourtsindia.com"
TOKEN = "eci_live_YOUR_TOKEN_HERE"
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"}
resp = requests.get(f"{BASE}/api/partner/causelist/court-structure/states", headers=HEADERS)
resp.raise_for_status()
print(f"{len(resp.json())} states/UTs available")

Node.js

const BASE = "https://webapi.ecourtsindia.com";
const TOKEN = "eci_live_YOUR_TOKEN_HERE";

const resp = await fetch(`${BASE}/api/partner/causelist/court-structure/states`, {
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    Accept: "application/json",
  },
});
if (!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`);
const states = await resp.json();
console.log(`${states.length} states/UTs available`);

A successful response returns every state and union territory key, plus the Supreme Court, keyed as SC. Codes follow the two letter convention used across the eCourts system: AP for Andhra Pradesh, DL for Delhi, MH for Maharashtra, KA for Karnataka, TN for Tamil Nadu, WB for West Bengal, and so on. The Supreme Court of India is keyed as SC because the eCourts taxonomy treats it as a separate jurisdiction rather than a state.

Reference data: court codes, case types and other enums

The first thing most teams ask for is the court list and the code lists. You do not need a spreadsheet from support for this. There are two free sources of truth, and you should pull from them at build time rather than hardcoding.

The enum endpoint returns the authoritative dictionaries for case type, case status, court code and state code. It is free to call and it is the single source of truth for any code you see in a response.

curl -X GET \
"https://webapi.ecourtsindia.com/api/partner/enums?types=caseType,caseStatus,courtCode,stateCode" \
-H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

As of 23 September 2026 the live enum endpoint returns 269 case-type codes, 71 case status codes, 21 court types (Supreme Court, High Court, district, and 18 tribunal families including GSTAT, RCT, SEBI_ORDERS and GST_AAAR), highCourtCode values (High Court bases plus SCIN; not search-ready; add the bench suffix, so DLHC01 not DLHC), and state codes (the court-structure walk adds SC for the Supreme Court as an extra jurisdiction key). The same call also returns the full courtCode dictionary (thousands of establishment codes across district courts, High Court benches and tribunals) and a stateCode list; walk /api/partner/causelist/court-structure/states for the state and UT keys including SC. High Court search keys still need the bench suffix (DLHC01, KAHC01, HCBM01, and so on). Treat any printed counts as a snapshot: the enum endpoint is always the authoritative live set, and it grows as new courts and forums come online (GSTAT benches and case types are a recent example), so pull the enums at build time rather than baking in a fixed count. See the full list in TXT for reference. A few you will use constantly: WP_C writ petition civil, CS civil suit, CRL_A criminal appeal, BA bail, ABA anticipatory bail, SLP_C special leave petition civil, CC criminal complaint, MACA motor accident claims. High Court codes carry a numeric suffix, for example DLHC01 for Delhi, KAHC01 for Karnataka, HCBM01 and HCBM02 for Bombay.

Wrong codes fail silently. If you pass a case type or court code that does not exist, the search does not error. It returns zero results with a 200 status. So when a query unexpectedly comes back empty, the first thing to check is whether your codes are real. Look them up in the enum endpoint before you blame the data.

The court hierarchy is walked top down when you need district and complex codes for cause lists. Each level is its own call.

GET /api/partner/causelist/court-structure/states
GET /api/partner/causelist/court-structure/states/{state}/districts
GET /api/partner/causelist/court-structure/states/{state}/districts/{districtCode}/complexes
GET /api/partner/causelist/court-structure/states/{state}/districts/{districtCode}/complexes/{courtComplexCode}/courts

The endpoints you will actually use

There are 23 partner endpoints in four families: Cases, Causelist, Electoral and LegalCheck. Most production integrations still start with case search, case detail and cause-list search; the rest exist so you do not invent a second protocol for refresh, enums, signed PDFs, AI analysis, voter-roll lookup or a scored litigation check. The table below is the inventory. Use it as a map, then read the deep sections for the ones you will actually wire.

MethodPathWhat it doesNotes
GET/api/partner/case/{cnr}Full case record: parties, advocates, judges, hearings, orders, IAs, tagged matters, embedded order markdownMetered. Party fields are string arrays. Use judgmentOrders[].orderUrl as the bare filename for order endpoints.
GET/api/partner/searchSolr full-text search across 32 crore+ records with 40+ filters, facets, projection and presence filtersMetered. Repeat array keys (courtCodes=DLHC01&courtCodes=HCBM01). Partner pageSize max 200.
GET/api/partner/search/capabilitiesMachine-readable catalog of sortable, facetable, projectable and presence-filterable fieldsFree. Build against this instead of hardcoding field lists.
GET/api/partner/case/{cnr}/order/{filename}Certified true-copy PDF (watermarked + signed by default)Metered. ?signed=false for the raw court PDF, same cost.
GET/api/partner/case/{cnr}/order-ai/{filename}Extracted text plus structured AI analysis (summary, outcome, statutes, ratio)Metered. First access 10–60s, then cached. Retry if aiAnalysis is null.
GET/api/partner/case/{cnr}/order-md/{filename}Markdown text plus PDF as base64Metered. Can take up to 300s. Prefer files[].markdownContent on case detail for plain text.
POST/api/partner/case/{cnr}/refreshQueue a live re-scrape of one CNR from government serversMetered. Returns 202. Also onboards a CNR that is not in the index yet. POST, not GET.
POST/api/partner/case/bulk-refreshQueue 2–50 CNRs for re-scrapeMetered per request. Deduplicates and reports refreshed / queued / invalid.
POST/api/partner/case/bulk-refresh-statusPoll refresh state for 1–50 CNRsFree. Statuses: PENDING, COMPLETED, FAILED, NOT_REQUESTED, INVALID. Failed refreshes set refunded: true.
GET/api/partner/enumsLive dictionaries: caseType, caseStatus, courtCode, highCourtCode, stateCode, benchType, judicialSection, caseCategory, causeListType, courtTypeFree. Cached 1 hour. Docs mark it as callable without auth. High Court values here are base codes (DLHC), not search keys.
GET/api/partner/causelist/court-structure/*State → district → complex → court walkFree with Bearer token. Breaking change: old /api/CauseList/court-structure/* is no longer a partner path. High Courts appear as districtCode HC. SC is the Supreme Court, labelled “India”.
GET/api/partner/causelist/searchDaily cause-list search by q, judge, advocate, litigant, state, court, date rangeMetered. Offset pagination (limit/offset), not page numbers. URL-encode / in case numbers as %2F.
POST/api/partner/causelist/cnr/batchUpcoming listing check for 1–100 CNRsMetered per distinct CNR. Body {"cnrs":[...]}. Over 100 returns BATCH_SIZE_EXCEEDED.
GET/api/partner/causelist/available-datesDates that have a cause list for a locationFree with auth. At least one of state, districtCode, courtComplexCode, court, courtNo is required.
GET/api/partner/electoral/searchElectoral-roll search by name, EPIC, free text or household, grouped by EPICMetered flat per request. Needs a primary criterion. State codes here are ECI codes (S01), not court DL.
GET/api/partner/electoral/epic/{epic}Exact EPIC lookupSame shape and charge as electoral search. For OCR-style near matches use search with epicFuzzy=true.
GET/api/partner/electoral/capabilitiesName-match modes, facets, filters, paging ceilings for electoral searchFree. Authoritative field catalog, so do not invent Solr fields as query params. maxPageSize and maxPage are 100.
POST/api/partner/legal-checkSubmit an async litigation background check for an individual or companyMetered once per new job. Returns 202. Send Idempotency-Key on retries. At most three jobs in flight per partner.
GET/api/partner/legal-check/{code}Poll job status and progressFree. Honour Retry-After while queued or running. A code owned by another partner returns 404 NOT_FOUND without confirming it exists.
GET/api/partner/legal-check/{code}/reportPartner-safe legal-check.v1 reportFree. Wait for completed or you get 409 NOT_READY. verbosity=compact/standard/full, plus bands, min_score, include=excluded and pretty.
GET/api/partner/legal-checkList this partner’s LegalCheck summariesFree. Summaries only; full reports stay on /report. Filter by status, date, client_ref_no.
GET/api/partner/legal-check/{code}/logWhat the engine actually searched for this job, as a JSON arrayFree. 409 NOT_READY while running; a failed job returns its terminal error code instead of a log. 120s server timeout.
GET/api/partner/legal-check/modelsAccepted model ids and subject typesFree. Current partner model is eCI-1.0 for individual and company. Earlier labels are rejected.
The 23 partner endpoints of the eCourtsIndia API, as listed in the API docs on 23 September 2026.

Successful JSON is wrapped as { "data": { ... }, "meta": { "request_id": "..." } } (docs also show requestId camelCase in examples; log whichever key your client actually receives). CNR pattern is four letters plus twelve digits, for example DLHC010001232024 = Delhi High Court, case 000123, filed 2024. Tokens are eci_live_ plus 32 alphanumeric characters (41 characters including the prefix).

Keep reading for the worked recipes. The numbered groups below are the same surface, explained in the order most teams integrate them.

Cover graphic for the eCourtsIndia API developer guide

1. Case retrieval

This is the workhorse. Given a CNR, fetch the complete case record with parties, advocates, judges, hearings, orders and interlocutory applications.

curl -X GET \
"https://webapi.ecourtsindia.com/api/partner/case/DLHC010001232024" \
-H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

For the Delhi HC writ petition above, you get the case type (WP_C), filing and registration numbers, filing date, judge name, party details, advocate names, the interlocutory applications, and a list of order files. As eCourts has migrated its High Court and District Court records to a newer structured format, the case record now also carries richer fields wherever the source provides them: for High Courts, things like the e-filing number and date, filing type, litigant type, case-type conversion and structured petitioner/respondent lists; for district courts, the processes, IA filings, transfers and hearing-history arrays. The full OCR cleaned text of each order is already embedded in the same response, so for most cases you never need a second call to read the order. More on that in the order retrieval section below.

2. Order retrieval

Three flavours of the same order, depending on what you need downstream.

GET /api/partner/case/{cnr}/order/{filename} -> PDF (signed certified copy)
GET /api/partner/case/{cnr}/order-md/{filename} -> markdown (OCR cleaned)
GET /api/partner/case/{cnr}/order-ai/{filename} -> markdown + structured AI analysis

For most LLM pipelines you want order-md. For lawyer-facing dashboards that need a structured AI analysis (summary, outcome, statutes, ratio), you want order-ai. For evidence and filing, you want the signed PDF.

You now choose signed or raw. The PDF endpoints take a signed flag. The default, signed=true, gives you the watermarked, digitally signed certified true copy that you would file or hand a client. Pass ?signed=false to pull the raw court PDF with no watermark or signature, which is cleaner for your own OCR or text pipeline. The per call cost is the same either way, and the markdown text is unaffected.

3. Case refresh

Three endpoints: single-case refresh, bulk refresh (max 50) and free bulk-refresh status. We cover these in depth further down because this is where most integrations go wrong.

4. Search

Solr backed full text search across case metadata, AI keywords, AI summary text and the full markdown text of every uploaded order. This is the endpoint behind almost every developer question we get, so it has its own dedicated section next.

5. Cause list

Four endpoints for daily court schedules. A cause list is the daily roster a court publishes of the matters it will hear, and who is on the bench. Covered in the cause list section, including the freshness window that catches teams out. There is now a fourth, CNR-first way in: POST /api/partner/causelist/cnr/batch takes 1 to 100 CNRs in one call and checks whether each case is listed for an upcoming hearing and, if it is, when, where, and what it is listed for. Pass a one-element array to check a single case. It is billed per CNR checked, ₹0.30 pay-as-you-go or ₹0.10 on a subscription, and it is the quick way to answer “is anything on my docket listed tomorrow?” without pulling each full cause list. There is no partner GET for a single CNR on the cause list; send a one-item array.

6. LegalCheck

Six endpoints for a scored litigation background check: submit an individual or company, poll until completed, read the legal-check.v1 report, and use list, search log and models for housekeeping. Covered in the LegalCheck section below. Submit is the only charged call in the family, at ₹99 pay-as-you-go or ₹33 on a subscription; status, report, list, search log and models are free. The current model id is eCI-1.0. Since MCP v4.46 an agent can run the same check through submit_legal_check and get_legal_check.

Search recipes developers actually ask us for

The search endpoint is one URL with more than 40 optional filters, and almost every support thread we open is a question about how to use it correctly. The endpoint is GET /api/partner/search. Parameters are case insensitive, so Query, query and QUERY all work, but we will write them in the documented PascalCase form. This section is the part most teams copy into their own wiki, and it pairs well with our longer eCourtsIndia Search Guide.

Recipe 1: replicate the website Case Status search

The most frequent request is some version of this: “I want the same Case Status search the eCourts website has, State then District then Court then Case Type then Case Number then Year, and I want the right CNR back.” There is no separate case status endpoint. You build that search out of the facet filters on /api/partner/search. The relevant parameters are:

What you pick on the websiteAPI parameterExample value
StateStateCodesDL
CourtCourtCodesDLHC01
Case TypeCaseTypesWP_C
YearFilingYears2024
Case NumbercaseNumbers (exact) or Query (ranked)138/2024

So the Delhi High Court writ petition number 138 of 2024 is found like this.

curl -X GET \
"https://webapi.ecourtsindia.com/api/partner/search?CourtCodes=DLHC01&CaseTypes=WP_C&FilingYears=2024&Query=138/2024&Page=1&PageSize=20" \
-H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

Prefer caseNumbers when you already have a number. That filter normalises each value to N/YYYY and exact-matches it against the registration number or the filing number. It accepts court-style strings such as O.S./25794/2021, CRA 529/2007 and 1763 of 2020. That is the precise Case Status lookup. Query=138/2024 still works as a ranked full-text fallback: the matching case typically ranks first, but it is not an exact filter. Read the cnr from the result, then call GET /api/partner/case/{cnr} for the authoritative record. If you search without court, type and year facets, you will get results from many states. The fix is always: add the facets.

Recipe 2: litigant search vs general search vs petitioner and respondent

This is the single biggest source of confusion, so it is worth being precise. There are three different ways to search by a name, and they return very different result sets.

ParameterWhat it searchesLive count for “Virendra Vora” (as of 5 August 2026)
Query=Virendra VoraEverything: party names, advocate, judge, and the full text of every order. It is a keyword search, so it matches the words across all that text and ranks by relevance12,301
Litigants=Virendra VoraParty names only, both petitioner and respondent sides208

Counts verified live on 5 August 2026. They move as new cases are indexed, so treat the numbers as illustrative of the ratio, not as constants.

The takeaway: general Query is a full text search across order text too, so a name will match even when it only appears once inside the body of an order. If you want to find cases where someone is actually a party, use the Litigants field. If you want to pin one side, use Petitioners or Respondents.

# All cases where the name is a party (petitioner OR respondent)
.../api/partner/search?Litigants=Virendra Vora
# Only where the name is the petitioner/appellant
.../api/partner/search?Petitioners=Virendra Vora
# Only where the name is the respondent/defendant
.../api/partner/search?Respondents=Virendra Vora

“A case has 8 respondents and my person is one of them. Will I get it?” Yes, with Litigants. We verified this with the name “Nemin Virendra Vora”. The Litigants filter returns 3 cases, and that set includes a Supreme Court matter and an Assam company petition where Nemin Virendra Vora is one of four respondents, not the first named party. The Litigants field matches the name anywhere in the petitioner or respondent lists, regardless of position. If you were only filling the Petitioners parameter with the first party, that is why you were missing these. Switch to Litigants.

For a deeper walk through of party name search, alias handling and the difference between a person and a company search, see the eCourtsIndia Litigant Search guide.

Recipe 3: “the website shows 11,000 records but the API gave me 150”

This one comes up almost word for word. The cause is two things stacked together.

First, the website default search is a general unquoted search, so it includes every partial and full text match across order bodies. That is the 12,301 number from the table above, not a clean list of party matches. The website is showing you the same large, relevance ranked set, just paginated.

Second, the API paginates. One response is a page, not the whole result set. The default page size is 20. The partner ceiling is 200 (partnerMaxPageSize from GET /api/partner/search/capabilities; anonymous web browsing is capped at 50). Asking for more than 200 returns 400 PAGE_SIZE_EXCEEDED and is not charged. If you read one response and count the array, you get one page, which is where the “I only got 100” reports come from. To pull the whole set you loop the Page parameter until hasNextPage is false.

# Page 1, 100 per page
.../api/partner/search?Query=Virendra Vora&Page=1&PageSize=100
# Page 2 gives results 101 to 200, page 3 gives 201 to 300, and so on
.../api/partner/search?Query=Virendra Vora&Page=2&PageSize=100

Python: walk every page

import requests
BASE = "https://webapi.ecourtsindia.com"
HEADERS = {"Authorization": "Bearer eci_live_YOUR_TOKEN_HERE"}
def search_all(params, page_size=100):
results, page = [], 1
while True:
q = {**params, "Page": page, "PageSize": page_size}
r = requests.get(f"{BASE}/api/partner/search", headers=HEADERS, params=q)
r.raise_for_status()
data = r.json()["data"]
results.extend(data["results"])
if not data.get("hasNextPage"):
break
page += 1
return results
rows = search_all({"Litigants": "Virendra Vora"})
print(len(rows), "party matches")

Node.js: the same page walk, no dependencies

const BASE = "https://webapi.ecourtsindia.com";
const HEADERS = { Authorization: "Bearer eci_live_YOUR_TOKEN_HERE", Accept: "application/json" };

async function searchAll(params, pageSize = 200) {
  const results = [];
  for (let page = 1; ; page++) {
    const qs = new URLSearchParams({ ...params, Page: page, PageSize: pageSize });
    const r = await fetch(`${BASE}/api/partner/search?${qs}`, { headers: HEADERS });
    if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
    const data = (await r.json()).data;
    results.push(...data.results);
    if (!data.hasNextPage) return results;
  }
}

const rows = await searchAll({ Litigants: "Virendra Vora" });
console.log(rows.length, "party matches");

Two things to copy from this rather than reinvent. URLSearchParams does the encoding for you, which is what you want given how often a case number carries a slash. And the loop exits on hasNextPage rather than comparing a running count against totalHits, because totalHits is a relevance-ranked estimate on wide queries and computing your own stop condition from it is the usual way people either miss a page or pay for one that does not exist.

Use Litigants if you want the clean party set rather than the inflated full text count. If you genuinely want all the full text hits, the loop above will fetch them page by page, but be mindful that every page is a metered call. For very large pulls, talk to the team about whether a data partnership fits your use case better than paginating the whole index.

Recipe 4: narrowing a noisy keyword search

Start with the mindset that this is a keyword and full-text engine, not a query language you must learn: the backend tokenises your words, matches them across case text and order bodies, and ranks by relevance, so plain keywords are the robust default. That said, the Query field does understand Solr operators when you need precision: an exact phrase in double quotes, uppercase AND/OR/NOT, grouping with parentheses, and a trailing wildcard such as neglig*. Phrase-plus-boolean combinations work; "specific performance" AND injunction returned 28,165 matters in a live 5 August 2026 check. Two hard rules: never start a term with *, which forces a full-index scan, and do not use operators inside the name fields (Litigants, Petitioners, Respondents), where nameMatchMode is the right tool. Most of the time, though, precision comes from which field you search and which facets you add. Two rules cover almost every case.

Search the right field, not just the general query. The general Query reads the full text of every order, so a common or short name gets noisy fast. Searching AU Small Finance Bank in Query matches those words wherever they appear, including deep inside an unrelated order. If you want the bank’s own cases, put the name in the Litigants field instead, which only looks at the party lists. The field you choose does far more to clean up results than any phrasing trick.

Punctuation is tokenised, so it rarely behaves as a literal. A query like A.P.A.C gets broken on the dots, so the general search can match unrelated text that happens to contain those letters. We had a team report that searching APAC returned a case with no APAC in its metadata, because of a literal mention buried in an order body. The reliable fix is the same one: search the party, advocate or judge field rather than the general query, and add facets to pin the court, case type and year.

To combine terms, facets are still the sharpest tool. Filtering by CourtCodes, CaseTypes and FilingYears narrows a broad keyword search precisely, which is how you get a clean, targeted result set, and unlike operator syntax it cannot be tripped up by tokenisation. Reach for operators when the facets cannot express the constraint, for example an exact phrase or an exclusion.

# Narrow a noisy keyword search with facets
.../api/partner/search?Query=airtel&CourtCodes=DLHC01&CaseTypes=WP_C&FilingYears=2024
# Operators work in Query when you need them (phrase + boolean, verified July 2026)
.../api/partner/search?Query=%22specific%20performance%22%20AND%20injunction

Remember to URL encode the query string in code. A space becomes %20, so Virendra Vora on the wire is Virendra%20Vora.

Useful extra filters

Beyond names, the search supports Advocates, Judges, CaseStatuses, BenchTypes, registration and decision date ranges, HasOrders, HasJudgments, MinOrderCount, and sorting with SortBy and SortOrder. SortBy takes relevance, which is the default, or a structured field such as a filing, decision or hearing date, a filing year, an order, hearing or judgment count, case type, status, court code or CNR, and multi-field chains like orderCount desc, filingDate asc work; an unsupported sort field simply falls back to relevance. Four newer capabilities are worth wiring in. The name filters (Litigants, Petitioners, Respondents, Advocates, Judges) take a nameMatchMode of all (default), any, phrase or fuzzy, where fuzzy tolerates one edit per token and absorbs Virendra/Virender-style spelling drift, and they accept multiple values by repeating the key (litigants=A&litigants=B, OR’d). Presence filters (existsFields/missingFields) return only cases where a field has, or lacks, a value, for example missingFields=decisionDate for still-pending matters. Field projection (fields=cnr,caseStatus,nextHearingDate) trims responses for high-volume scans. And the whole supported surface, every sortable, facetable, projectable and presence-filterable field, is machine-readable from the free GET /api/partner/search/capabilities, so discover it from there rather than hardcoding. Add IncludeFacetCounts=true to get drill down facet counts back with the result set, which is how you build a filter sidebar like the one on the website. Walk page until hasNextPage is false; there is no separate cursor parameter on partner search. Teams building background verification and due diligence flows lean on these heavily, and we wrote a full build along for that in Building a Legal Due Diligence Engine on India’s Court Data.

Solr field map: what case search actually queries

Case search is one Solr document per CNR (unlike electoral search, which is one document per roll occurrence). Partner query is full-text over that document: party and advocate names, judges, CNR, registration number, court/case metadata, acts text, AI keywords, AI order summaries, and the markdown of uploaded orders. Call GET /api/partner/search/capabilities (free) for the live filter/sort/facet lists. Do not invent query params from Solr field names such as courtName=, filingNumber= or aiKeywords=.

Partner paramSolr fieldsHow to use it
querysearchable_text plus order/AI text the API layer searches with itRanked full-text. Operators work (quoted phrase, uppercase AND/OR/NOT, parentheses, trailing wildcard). Live check: query="Piyush Tyagi" + courtCodes=DLHC01 returned both the case where he is a party and W.P.(C) 138/2024, where that name appears only in the order (a cited judgment). Never lead with *.
litigantslitigants (copy of petitioners + respondents), analysed as text_nameParty lists only. Same name on Delhi HC: litigants=Shubham Pratap Singh → 2 cases; query=Shubham Pratap Singh → 595. Use litigants for due diligence; use query for “mentioned in an order”.
petitioners / respondentssame-named text_name fieldsOne side only. nameMatchMode: all / any / phrase / fuzzy. Trailing commas in scraped names are stripped at index time.
advocatespetitionerAdvocates + respondentAdvocatesNot a Solr operator field. Fuzzy spelling via nameMatchMode.
judgesjudgesSame name analyser. Does not always appear in activeFilters, and that is normal.
caseNumbersexact registrationNumber or filingNumber (normalised N/YYYY)The Case Status clone. query=9623/2024 did not find the writ whose filing number is 9623/2024 (slash is tokenised). caseNumbers=9623/2024 + courtCodes=DLHC01 returned both that writ (filing number) and W.P.(C) 9623/2024 (registration number). Always add court and case type when the number is common.
cnrs or GET /case/{cnr}cnr (unique key)query=DLHC010001232024 does find the CNR because it is copied into searchable text, but the case endpoint is the right read. Prefer cnrs= to restrict a search, not to fetch a record.
courtCodes / MCP cccourtCode (string, search-ready)DLHC01 not DLHC; NCLT trailing 0. REST has no cc param.
courtLevelscourtLevel (SC / HC / DC / TRIBUNAL)Live: courtLevels=TRIBUNAL + query=insolvency faceted cleanly to TRIBUNAL. There is still no courtType filter, so pick a tribunal with its court code when you need one bench.
stateCodesmapped onto numeric Solr stateCodeSend DL, not facet id 26. Search results already show stateCode: "DL".
districtCodesnumeric Solr districtCodeFrom court-structure / get_districts, not two-letter codes.
courtLocationPathscourtLocationFacetPathCopy from a prior hit, e.g. 0/DL or 1/TN/05. Do not invent numeric paths such as 0/26.
actsAndSectionsexact actsAndSections stringNeeds stored text like INDIAN PENAL CODE - 302. Prefer query=IPC 302 for statute mentions in orders.
hasOrders / counts / dates / yearssame-named indexed fieldshearingCount is often 0 on High Court rows, so use minOrderCount there. Reliable on NCLT/tribunals.

Indexed on the case document, returned or sortable, but not partner query params: courtName and caseTypeDescription (they still match inside query), caseCategoryCleaned, courtCodeNumeric, courtNo, complexCode (sort / exists filters), aiKeywords (projectable on the hit, not a dedicated filter), dataCleanerLastUpdated (sortable). Order markdown and AI summary text are searched by query but are not returned as search-hit fields. Read them from GET /api/partner/case/{cnr}.

Complete search parameter catalog

Do not hardcode this list. Call GET /api/partner/search/capabilities (free) and treat its arrays as the live contract. What follows is the 23 August 2026 snapshot, so a human or an AI can implement against it without a second round trip while building. Array parameters on the REST surface are passed by repeating the key (litigants=A&litigants=B). A comma inside one value is part of that value, not a separator. Parameters are case-insensitive.

Text / names. query (full-text Solr over metadata + AI keywords + AI summaries + order markdown; see the Solr field map above). advocates, judges, petitioners, respondents, litigants (repeatable; values OR’d; inside each value every token must match unless you change the mode). nameMatchMode: all (default), any, phrase, fuzzy (edit distance 1 per token). Applies only to those five name fields.

Structured filters (repeatable arrays). courtCodes (search-ready, so DLHC01 not DLHC; NCLT needs a trailing 0: NCLTMB0). caseTypes (codes such as WP_C, never “CIVIL”). caseStatuses. judicialSections (CIV, CRIM, WRIT, REV, APP, MISC, PIL, BAIL, URG, ADM). courtLevels: SC, HC, DC, TRIBUNAL (there is no courtType search filter; tribunals are reached by court code). caseNumbers (normalised to N/YYYY, exact match on registration or filing number). cnrs (restrict to known identifiers). Also live on the partner search surface: stateCodes (two-letter, DL not the numeric facet id), districtCodes (numeric), benchTypes, caseCategories, actsAndSections (exact stored text only; prefer query=IPC 302), hasOrders, hasJudgments, year lists (filingYears, registrationYears, firstHearingYears, nextHearingYears, decisionYears), numeric ranges (minOrderCount / maxOrderCount, hearing, judgment, IA, interim-order and case-duration equivalents), and courtLocationPaths (copy values from courtLocationFacetPath in a prior result; do not invent them).

Dates (YYYY-MM-DD). filingDateFrom/To, registrationDateFrom/To, firstHearingDateFrom/To, nextHearingDateFrom/To, lastHearingDateFrom/To, decisionDateFrom/To. The interactive docs table currently shows a subset; the live search contract includes all six pairs.

Facets, projection, presence, sort, page. facets (caseType, caseStatus, courtCode, stateCode, districtCode, filingYear, decisionYear, hasOrders, hasJudgments, benchType, courtLevel). includeFacetCounts (default true). maxFacetValues (default 100, max 1000). facetPrefix, facetContains, courtLocationFacetPrefix (narrows the location facet only), yearFacetField + yearFacetGap. fields to project (cnr is always included). existsFields / missingFields (AND’d; unknown names ignored). sortBy (single field or chain such as decisionDate desc,caseType asc; unknown fields fall back to relevance, they do not error). sortOrder asc|desc. includeExtremeDates lifts the default date-sort bound of 1900-01-01 to NOW+2 years. page (1-based, default 1). pageSize (default 20, partner max 200).

Capabilities snapshot (still accurate 29 August 2026). Sortable fields include score, dataCleanerLastUpdated, every date field, caseDurationDays, orderCount, hearingCount, judgmentCount, interimOrderCount, iaCount, filingYear, decisionYear, filingToFirstHearingDays, caseType, caseStatus, caseCategory, benchType, courtCode, courtCodeNumeric, courtNo, districtCode, stateCode, complexCode, cnr, filingNumber, registrationNumber, judicialSection. Facetable: caseType, caseStatus, courtCode, stateCode, districtCode, filingYear, decisionYear, hasOrders, hasJudgments, benchType, courtLevel. Projectable fields include cnr (always returned), parties, advocates, judges, dates, counts, aiKeywords and courtLocationFacetPath; call capabilities for the full list. Exists/missing filters work on dates, counts, geo codes and hasOrders/hasJudgments. Court levels: SC, HC, DC, TRIBUNAL. Name-match modes: all, any, phrase, fuzzy. Partner REST pageSize max is 200 (partnerMaxPageSize); the capabilities payload also reports maxPageSize: 50 for the anonymous web search. MCP search_cases pageSize max is 200 and accepts comma-separated multi-values; REST arrays are repeated keys. cc is an MCP convenience for one court code, not a REST query param. minHearingCount is unreliable on High Court rows (hearingCount is often 0 in the index); use minOrderCount there. It is reliable for NCLT and other tribunals.

curl -X GET "https://webapi.ecourtsindia.com/api/partner/search/capabilities" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

# Exact case-number lookup (preferred over Query for Case Status clones)
curl -G "https://webapi.ecourtsindia.com/api/partner/search" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
  --data-urlencode "courtCodes=DLHC01" \
  --data-urlencode "caseTypes=WP_C" \
  --data-urlencode "caseNumbers=138/2024"

# Tribunal: there is no courtType filter. Target the bench code.
curl -G "https://webapi.ecourtsindia.com/api/partner/search" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
  --data-urlencode "courtCodes=NCLTMB0" \
  --data-urlencode "caseTypes=CP_IBC" \
  --data-urlencode "courtLevels=TRIBUNAL"

What comes back: the search response envelope

Everything above is what you send. This is what you get, at the top level of data. It is worth reading once because the commonest wrong guess is totalResults, which does not exist. The field is totalHits.

FieldWhat it is
results[]The page of cases. Always carries cnr; the rest depends on what you asked for in fields.
totalHitsTotal matches, not the page size. This is the number the website shows.
page, pageSize, totalPagesWhere you are. pageSize is the one you asked for, capped at 200 on partner accounts.
hasNextPage, hasPreviousPageLoop on hasNextPage. Do not compute it yourself from totalHits.
facetsPer field: values (value to count), totalValues, hasMore, facetType.
activeFilters[]What the server actually applied, each with a displayName. The fastest way to spot a filter it silently ignored.
enumDescriptions.enumLookupCode to human label for every enum field in this response. Saves a call to /enums.
processingTimeMsServer-side time. Useful for telling a slow query from a slow network.
queryThe query string as parsed.

activeFilters is the one to instrument. It lists the filters the server actually applied, which is not always the set you sent. That is how you catch the actsAndSections behaviour documented further down: it never appears in activeFilters even when you pass it, which is your signal that the filter is not doing what you think. If a query comes back emptier or broader than you expected, diff what you sent against activeFilters before you blame the index.

Order and PDF retrieval, the part that confuses everyone once

When you fetch a case, the response carries a files block and a judgmentOrders block. Developers get tripped up by the two different filename forms, so here is exactly what each field is.

"judgmentOrders": [
{ "orderDate": "2024-01-05", "orderType": "Final Order", "orderUrl": "order-1.pdf" }
],
"files": {
"files": [
{
"pdfFile": "DLHC010001232024-order-1.pdf",
"markdownFile": "DLHC010001232024-order-1.md",
"markdownContent": "...full OCR cleaned order text is right here...",
"aiAnalysis": { ... }
}
]
}

Two things to take away.

First, the full order text is already in the case response, inside files.files[].markdownContent. For most use cases you do not need a second call to read an order. Just read that field.

Second, when you do want the standalone PDF or markdown endpoint, pass the bare filename from orderUrl, which is order-1.pdf, not the prefixed storage name DLHC010001232024-order-1.pdf you see in pdfFile. The prefixed name is the internal storage key. The endpoint expects the short form.

# Correct: bare filename from orderUrl
GET /api/partner/case/DLHC010001232024/order/order-1.pdf
# Same case, OCR cleaned markdown
GET /api/partner/case/DLHC010001232024/order-md/order-1.pdf

Signed certified copy by default, raw PDF on request. Both the PDF endpoint and the markdown endpoint return the watermarked, digitally signed certified true copy unless you say otherwise. Add ?signed=false when you only need the underlying court PDF for machine reading. The suggested download filename then switches to an unsigned label, so the two never get mixed up in storage.

# Certified true copy (default): watermark + digital signature
GET /api/partner/case/DLHC010001232024/order/order-1.pdf
# Raw court PDF, no watermark or signature, same cost
GET /api/partner/case/DLHC010001232024/order/order-1.pdf?signed=false

What the order AI analysis actually contains

The order-ai endpoint is far more than a one line teaser. It returns the OCR cleaned extractedText of the order alongside a deep aiAnalysis object, and for a lawyer facing product or a research pipeline this is the part worth wiring up properly. The analysis is organised into four blocks.

  • Foundational metadata. Clean case identifiers, bench composition, judge names, order date, every party with its exact role, and the counsel who appeared on each side.
  • Deep legal substance. The primary legal issues, the statutes cited and how each was applied, the arguments on both sides, the court’s reasoning, and the extracted ratio decidendi with a confidence score.
  • Intelligent insights. An executive summary, a plain language outcome summary written for the litigant, actionable alerts, and any compliance directives flowing from the order.
  • Actionable outputs. Cited case network data, similarity search hints, and topic cluster suggestions you can use to surface related matters.

The fields are deeply nested, so here are the paths you will reach for most often.

# Executive summary
aiAnalysis.intelligent_insights_analytics...ai_generated_executive_summary
# Plain language summary written for the litigant
...plain_language_summary_for_litigants_outcome_focused
# Order nature and outcome
aiAnalysis.foundational_metadata.procedural_details_from_order.order_nature
...disposition_outcome_if_disposed
# Judges and order date
aiAnalysis.foundational_metadata.core_case_identifiers.judge_names
...core_case_identifiers.order_date
# Statutes applied and the court's reasoning
aiAnalysis.deep_legal_substance_context.core_legal_content_analysis.statutes_cited_and_applied
...arguments_and_reasoning_analysis.court_reasoning_for_decision

The analysis is generated on demand the first time you ask for it. The first call to order-ai for a given order can take 10 to 60 seconds while the document is processed, and the result is cached after that, so every later call is fast. If aiAnalysis comes back null, do not treat it as missing. Wait 15 to 30 seconds and retry, up to three times. If you only need raw text and not the analysis, fall back to order-md or the markdownContent already sitting in the case response.

The markdown endpoint can be slow, by design. order-md runs a real time PDF conversion pipeline and can take up to 300 seconds on a heavy order. If you hit a 429 TOO_MANY_CONVERSIONS, the pipeline is saturated, so wait for the duration in the Retry-After header before trying again. For most reads you never need this call at all, because the full order text is already embedded in the case response under files.files[].markdownContent. Reach for order-md when you need a standalone OCR-cleaned markdown render (for example the text was not yet embedded, or you want a fresh conversion). For a watermarked, digitally signed PDF to file or show a client, use the /order/ endpoint with the default signed=true. If you are building case intelligence on top of this, our walkthrough in From Case Lookup to Case Intelligence shows where the AI analysis fits.

The refresh API, in depth

The refresh API is the workflow that catches teams off guard on day one. The semantics are different from “fetch the data right now”, and getting it wrong produces silent staleness in production.

What refresh does, and why it is not instant

When you call POST /api/partner/case/{cnr}/refresh, you are not fetching data from us. You are asking our backend to go to the official eCourts source, pull the latest record for that CNR, parse and normalise it, OCR any new orders, and write the updated record. The call returns immediately and the case enters the refresh queue.

It is worth understanding why this is a queue and not an instant read, because it explains the behaviour you will see. We are reaching through to government court servers in real time. Those servers are the same ones behind the public portal, and they are not always fast or reliable. Some are slow at peak hours, some return partial data, some are briefly down. When a refresh takes longer than usual or has to retry, it is almost always the source server having a moment, not your request. Building in a poll and a sensible timeout, rather than expecting a single instant answer, is what makes an integration robust against that reality.

Refresh is POST, not GET. Calling GET .../refresh returns 405 Method Not Allowed. This is a common first attempt. Use POST.

curl -X POST \
"https://webapi.ecourtsindia.com/api/partner/case/DLHC010001232024/refresh" \
-H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"
How to use the eCourtsIndia API, cover design variant B for the eCourtsIndia blog

Refresh is asynchronous, so do not expect fresh data on the next line. After you queue a refresh, wait, then call GET /api/partner/case/{cnr} and watch for dateModified to advance. How long the round trip takes depends on what changed and on how the source server is behaving. A metadata-only update can settle quickly when the source is responsive. In practice, a refresh that reaches the government court servers typically lands in 2 to 10 minutes, and a case with brand new order PDFs that need OCR, or a slow source server, can take longer. Poll rather than assume, and set a sensible cap (often 10 to 15 minutes for portfolio jobs).

Refresh is idempotent for a short window. Calling refresh on the same CNR more than once within about 15 seconds will not create duplicate jobs or charge you twice for the scrape, so a stray double click or a retry is safe. After you queue a refresh, give it 5 to 10 seconds before you fetch the case again, then watch dateModified as shown above.

Python: refresh then poll until the record moves

import time, requests
BASE = "https://webapi.ecourtsindia.com"
HEADERS = {"Authorization": "Bearer eci_live_YOUR_TOKEN_HERE"}
def refresh_and_fetch(cnr, max_wait=300, poll_every=10):
requests.post(f"{BASE}/api/partner/case/{cnr}/refresh", headers=HEADERS).raise_for_status()
started, baseline = time.time(), None
while time.time() - started < max_wait:
time.sleep(poll_every)
resp = requests.get(f"{BASE}/api/partner/case/{cnr}", headers=HEADERS)
resp.raise_for_status()
modified = resp.json()["data"]["entityInfo"]["dateModified"]
if baseline is None:
baseline = modified
elif modified != baseline:
return resp.json()
return resp.json()

Refresh also works when we do not have the CNR yet

This is a genuinely useful property that is easy to miss. Refresh is not limited to cases already in our index. If you call refresh on a valid CNR that is not yet in the eCourtsIndia database, the backend will go and fetch it from the official source, parse it, and add it. Either refresh endpoint, single or bulk, behaves this way. So the reliable pattern when a CNR is missing is: call refresh on it, wait, then call the case endpoint. The case that came back empty a moment ago will now be populated with proper court data. In other words, you do not need us to already know a case for you to onboard it. Push the CNR through refresh and it gets pulled in.

Search indexing lags behind a refresh, by up to an hour or two. There is one subtlety to plan for. After a refresh, the case detail endpoint, GET /api/partner/case/{cnr}, reflects the fresh data quickly. The full text search index does not update in the same instant. With a full index of 32 crore+ records, rebuilding the search index on every single change is not practical, so reindexing runs in batches, and a freshly refreshed or newly added case can take up to one to two hours to appear or update in /api/partner/search results. The takeaway: for the freshest view of a specific case, read it by CNR through the case endpoint. Use search for discovery, and expect a short lag before brand new changes are searchable.

When to call refresh

Call refresh when you have a reason to believe the cached data is stale. The cache is kept reasonably fresh by background workers, so most reads do not need an explicit refresh first. The most reliable staleness signal is a nextHearingDate that is in the past, which means a hearing has happened and the snapshot predates it. A user pressing “get the latest” in your UI is another good trigger. For monitoring a portfolio overnight, batch the CNRs and use bulk refresh rather than looping the single endpoint.

How freshness works (cached reads vs refresh)

Case detail and search do not live-scrape the government portal on every call. The 32 crore+ case corpus, and the orders already indexed against it, sit on eCourtsIndia servers. That is why GET /api/partner/case/{cnr} and GET /api/partner/search typically return in milliseconds: you are reading our structured layer, not waiting on a district-court CAPTCHA.

We pre-crawl and continuously refresh what we can in the background, but Indian courts list on the order of fifteen lakh matters a day, and there is no reliable push feed for “this CNR just changed.” So a portfolio integration cannot assume every watched case is seconds-fresh unless it asks. When you need the official source re-checked, call refresh. Refresh is the on-demand path: we go to the government servers, re-parse the case, OCR any new orders, and write the update back to our store (typically 2 to 10 minutes).

The cost-efficient pattern: do not refresh your entire docket every night. A typical matter only moves after a hearing. Read the cached case first. Refresh when nextHearingDate is in the past on a still-pending case, when the user explicitly asks for the latest, or when you are onboarding a CNR you do not have yet. For many CNRs, use bulk refresh, then poll status / dateModified, then compare off the case endpoint, not off search, which can lag a refresh by one to two hours.

Bulk refresh for portfolios

If you are refreshing more than two or three cases, do not loop the single endpoint. Use bulk refresh.

curl -X POST \
"https://webapi.ecourtsindia.com/api/partner/case/bulk-refresh" \
-H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{ "cnrs": ["DLHC010001232024", "MHAM030051402019", "HCBM010186932022"] }'

The backend validates each CNR, deduplicates the list, queues the valid ones and reports invalid ones back so you can clean your watchlist. A single bulk-refresh call accepts up to 50 CNRs, so for a larger watchlist, chunk it into batches of 50 rather than sending one giant array. After you queue, poll POST /api/partner/case/bulk-refresh-status with the same CNRs to see which refreshes have landed (and whether a failed attempt was refunded). This is the endpoint behind the weekly routine in Litigation Portfolio Monitoring for General Counsel.

# Free status poll (1–50 CNRs). Same body shape as bulk-refresh.
curl -X POST \
  "https://webapi.ecourtsindia.com/api/partner/case/bulk-refresh-status" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{ "cnrs": ["DLHC010001232024", "MHAM030051402019"] }'

# Typical status values: PENDING, COMPLETED, FAILED, NOT_REQUESTED, INVALID.
# FAILED rows include refunded: true when the scrape credit was returned.

Reconcile the batch from the status response, not from a second call. Each row carries creditsCharged alongside status and refunded. A row that failed shows the original creditsCharged with refunded: true, meaning the credit is back in the pool it came from. A NOT_REQUESTED row shows creditsCharged: null, because you never asked for that one. Storing those three fields per CNR is enough to reconcile a nightly refresh run against your invoice without calling anything else, and it is the cheapest audit trail you will build, since the status poll is free.

Building daily case update alerts

One of the most common things teams build on top of refresh is a watcher: tell me when anything changes on these cases. The eCourtsIndia API does not yet push webhooks, so the production pattern is a scheduled pull and compare. The logic is simple and reliable once you see it.

For each CNR you care about, store a small snapshot of the fields that matter. Once a day, refresh the case, fetch it, and compare the new snapshot against the stored one. If anything you track has changed, raise an alert and save the new snapshot as the baseline. The fields worth watching are usually caseStatus, nextHearingDate, lastHearingDate, orderCount and decisionDate. A change in orderCount means a new order or judgment was uploaded. A change in nextHearingDate means the matter was relisted.

Python: detect changes for a watchlist

import time, requests

BASE = "https://webapi.ecourtsindia.com"
HEADERS = {"Authorization": "Bearer eci_live_YOUR_TOKEN_HERE"}
WATCHED = ("caseStatus", "nextHearingDate", "lastHearingDate", "orderCount", "decisionDate")

def refresh_and_fetch(cnr, max_wait=600, poll_every=15):
    requests.post(f"{BASE}/api/partner/case/{cnr}/refresh", headers=HEADERS).raise_for_status()
    started, baseline = time.time(), None
    while time.time() - started < max_wait:
        time.sleep(poll_every)
        resp = requests.get(f"{BASE}/api/partner/case/{cnr}", headers=HEADERS)
        resp.raise_for_status()
        body = resp.json()["data"]
        modified = body["entityInfo"]["dateModified"]
        if baseline is None:
            baseline = modified
        elif modified != baseline:
            return body
    return resp.json()["data"]

def snapshot(case_data):
    ccd = case_data["courtCaseData"]
    return {k: ccd.get(k) for k in WATCHED}

def detect_changes(cnrs, baselines):
    alerts = []
    for cnr in cnrs:
        case_data = refresh_and_fetch(cnr)
        current = snapshot(case_data)
        previous = baselines.get(cnr)
        if previous is not None and current != previous:
            alerts.append({"cnr": cnr, "before": previous, "after": current})
        baselines[cnr] = current
    return alerts

# baselines = {}  # persist this dict in your DB between cron runs
# alerts = detect_changes(["DLHC010001232024", "MHAM030051402019"], baselines)

Run that on a daily cron, ideally early morning before the working day, and persist each baseline in your own database. For a watchlist of any real size, replace the per case refresh with a single bulk refresh call for all the CNRs first, wait, then fetch and compare each one, which is gentler on your rate limit. Because search indexing can lag a refresh by an hour or two as noted above, always do the comparison off the case endpoint, not off search. The same nightly shape powers the portfolio monitoring workflow in Litigation Portfolio Monitoring for General Counsel.

Cause lists in the real world

Cause lists are daily court schedules, and the API exposes four endpoints: available-dates tells you which dates a court has a list on file (free with authentication), causelist/search does a full text search across cause list entries, causelist/cnr/batch checks up to 100 CNRs at once for upcoming listings, and the court structure walk under /api/partner/causelist/court-structure/* gives you the codes. The 5 am brief workflow built on these is written up in The 5 am Cause List Problem. Three real world facts save a lot of debugging.

The freshness window is short. We keep cause lists for roughly the day before through seven days ahead. Cause lists change right up to the hearing, so holding a long future window would mean serving data that is likely to be revised. The government district sites keep a longer forward window, and High Courts and the Supreme Court typically publish a single day. So if you query a cause list for a date more than a week out, an empty result is expected, not a bug. Keep your lookups inside the window.

Search cause lists by name, not by case number. The causelist/search free text field is built to match advocates, judges, litigants and parties. Searching by a full case number string such as O.S./25794/2021 often returns an empty array even when the case is genuinely listed, because cause list rows are keyed and indexed differently from the case registry. Searching by advocate name, judge name or party name returns the rows reliably. If you need to go from a specific case to its listing, the robust pattern is the mapping approach below.

That said, the q field does accept a case number string directly, so you can paste something like G.R.case/533/2023 straight in. The catch is encoding: a forward slash has to be URL encoded as %2F, so on the wire that becomes q=G.R.case%2F533%2F2023. Even then a listing only exists inside the freshness window, so for anything you depend on, search by advocate, judge or party name and fall back to the mapping pattern below.

Bench number is not a room number. The number you see, for example court number 21, is the court establishment identifier, not a physical room. What advocates actually care about in a cause list is which bench, meaning which judge and bench number, is hearing the matter on a given day, because that changes through the life of a case. So when you map cause list data, carry the bench and judge fields through, since those are the fields with real value to the end user.

Linking a case to today’s list now has a first-class endpoint. POST /api/partner/causelist/cnr/batch takes 1 to 100 CNRs in one call (body { "cnrs": [ ... ] }; for a single case, pass a one-item array) and returns, per CNR, whether the case is listed for an upcoming hearing and what it is listed for. Batch checks are billed at ₹0.30 per CNR pay-as-you-go, ₹0.10 per CNR on a subscription. This replaces the old workaround of maintaining your own CNR-to-establishment-plus-case-number mapping table and picking the matching cause list row by hand; that mapping pattern still works and can save calls when a hearing is far out (if the next hearing is weeks away, there is no point querying, just tell the user when to check back), but for a watchlist the batch endpoint is the clean answer.

The filters and the pagination are not the same as the case search. Cause list search has its own set of filters: listType as CIVIL or CRIMINAL, plus judge, advocate, litigant, state, districtCode, courtComplexCode, court, courtNo, and either a single date or a startDate and endDate range. Two fields are easy to confuse: courtNo is the physical courtroom number, while court is the internal establishment identifier, and they hold different values. Set includeCourtroom=true to get the room assignment back with each row.

Pagination here is offset based, not page based. Unlike the main search, cause list search uses limit and offset. The first page is limit=20&offset=0, the second is limit=20&offset=20, and you keep adding the limit to the offset. When returnedCount is less than your limit, you have reached the last page. Both caseNumber and judge come back as arrays, because one listing can carry more than one of each.

Codes and quirks the API will not warn you about

A handful of behaviours return a clean 200 with the wrong result rather than an error, which makes them hard to spot. These are the ones support sees most, and they pair well with the longer eCourtsIndia Search Guide.

High Court codes need their bench suffix. Searching courtCodes=DLHC returns zero rows with no error, because the index keys High Courts by the full code. Use DLHC01, KAHC01, HCBM01 and so on. The base code without the number is not a real search key.

NCLT codes need a trailing zero. The NCLT benches are a special case: NCLTDL returns nothing, you have to use NCLTDL0 or NCLTMB0. The same silent empty result applies, so if an NCLT search looks dead, check the trailing zero first. There is more on the insolvency benches in our NCLT and NCLAT case status guide.

Do not lean on the acts and sections filter. The actsAndSections filter only matches the exact stored text, something like INDIAN PENAL CODE - 302 rather than IPC 302, and most cases are not indexed with that field at all, so it usually comes back empty. For anything to do with a statute or a section, put it in the general query instead, where the full text search across order bodies finds hundreds of thousands of matches.

Case category and case title are not what you expect. The category enum codes work as search filters, but the caseCategory value inside a case record is free form court text such as “Constitutional Writ”, not a code from the enum. And the search results carry no case title field at all, so build a display title yourself from the petitioners and respondents arrays, for example “ABC Ltd vs XYZ Corp”.

The search response already hands you the labels. You do not need a second enum call just to render a result. Every search response includes an enumDescriptions.enumLookup block that maps the codes in that result set to their descriptions, so WP_C arrives next to “Writ Petition (Civil)” and DISPOSED next to “Disposed”. The same response carries facets with counts, totalHits, totalPages and the activeFilters it applied, which is everything you need to draw a filter sidebar.

How to use the eCourtsIndia API, cover design variant C for the eCourtsIndia blog

Electoral roll search

Three REST endpoints search Indian electoral rolls: GET /api/partner/electoral/search, GET /api/partner/electoral/epic/{epic}, and the free GET /api/partner/electoral/capabilities. The same surface is on MCP as search_electoral_roll, lookup_epic and get_electoral_capabilities. Results are grouped by EPIC, so one person is returned once with an occurrences array of every appearance across years, roll types and revisions. This is a different corpus from court cases. Do not send a court state code such as DL here; electoral geography uses ECI codes such as stateCode=S01 (Andhra Pradesh), S22 (Tamil Nadu), U05 (NCT of Delhi).

Every search needs a primary criterion or the call is rejected with 400 MISSING_SEARCH_CRITERIA and is not charged. Primary criteria: name, epic, q, or a complete household key. A father’s name, a PIN, a village or a state filter cannot start a search. relativeName, age, gender, geography and roll filters only narrow a search that already has a primary criterion.

Household lookup is exclusive. householdRollId, householdPartNumber and householdHouse must be supplied together (400 HOUSEHOLD_INCOMPLETE otherwise) and cannot be combined with name, relativeName, epic or q (400 HOUSEHOLD_EXCLUSIVE). Copy all three from an occurrence’s Household key: line; send the printed house string, not a guessed normalised form.

Paging is stricter than case search. page and pageSize must each be between 1 and 100. Values outside that range are rejected with HTTP 400 rather than clamped, and are not charged. A single query therefore reaches at most 9,900 records; narrow with filters rather than paging deeper. Charge is flat per successful request, however many persons come back. Invalid paging and INTERNAL_ERROR are not charged.

Solr field map: what you can actually query

The electoral API is Apache Solr underneath, one document per roll occurrence. That is why a name search can return the same EPIC once with several occurrences, and why q can hit a village or booth name even when it is not the elector’s name. The partner API does not expose every indexed Solr field as a query parameter. Call GET /api/partner/electoral/capabilities (free) and only send filters from that list. Inventing village=, pincode=, serial= or pcNumber= does nothing useful; those values come back on the occurrence object for display.

Partner paramSolr fields it searchesHow to use it
namenameLatin, nameAlts, native name, plus precomputed nameFolded / namePhoneticPrimary criterion. nameMatchMode: all, any, phrase, fuzzy. Live matchedOn values include nameLatinExact, nameFolded, namePhoneticOnly.
relativeNamerelativeLatin, relativeAlts, native relative, relativeFolded / relativePhoneticFilter only. It cannot start a search. Combine with name. matchedOn: relativeExact, relativeFolded, relativePhoneticOnly.
qsearchable_text (copies Latin + native names, name alts, relative names, and addressText)Primary criterion. Use this for a village, street or booth string, or when you only have a relative’s name. A q of a common locality can return tens of thousands of persons, so always add stateCode.
epic / path /epic/{epic}epicExact EPIC. MCP lookup_epic is the same shape and charge as search. matchedOn: epicExact.
epicFuzzy=truenear-match on epicOCR / photo confusions. A live check: fuzzy on an exact EPIC also returned nearby IDs that differ by a single character (match 90).
Household triplerollId + partNumber + printed house (indexed as houseNorm)Lists everyone at that address on that part. Exclusive: no name/epic/q on the same call.
stateCode, stateName, districtCode, districtName, acNumber, partNumbersame-named geo fieldsECI codes. District codes look like S0106, not court district integers. matchedOn can include stateMatch / districtMatch.
age + ageTolerance (default 3)age / yobApproxmatchedOn: ageWithin2, ageWithinTolerance.
gendergenderSend M, F or O. The live gender facet can also return T; gender=T is rejected as a filter.
relationrelationShort codes: F Father, H Husband, M Mother. Not FTHR.
years, rollTypes, langCodesyear, rollType, langCodeLive roll types include FinalRoll, DraftRoll, SIR-FinalRoll. English parts use langCode=ENG.
includeDeleted, excludeFlaggeddeleted, partFlagsDefaults: include deleted occurrences (true), do not drop flagged parts (false). partFlags such as part_identity_mismatch mean OCR/extraction quality is lower.
facetsfacetable fields onlyLive list: gender, relation, year, rollType, langCode, deleted, geoFacetPath, stateCode, districtCode, acNumber, pincode, ageBand. A name outside this list is rejected. facetPrefix applies only to geoFacetPath. Copy a path from a prior facet response, do not guess S01.

Indexed on the document, returned on occurrences, but not partner filters: village, ward, tehsil, post office, police station, parliamentary constituency (pcNumber / pcName), polling station number/name, serial, section name, PIN. PIN is a facet (facets=pincode) and can appear in matchedOn as pincodeMatch; there is still no pincode= query param. To search a locality string, use q.

Names are stored in three tiers: native script, Latin (whitespace + lowercase + ASCII folding), and C#-precomputed folded/phonetic keys (Solr does not re-phoneticise them). That is why nameMatchMode=fuzzy and why matchedOn can say nameFolded even when the printed spelling differs. Rolls are OCR-derived; spellings drift between revisions.

matchScore and matchedOn say why a row matched, not that two rows are the same human. Common names repeat. Do not merge two EPICs into one person, or assert that a litigant and an elector are the same individual, on a name match alone. Corroborate with EPIC, relative, age and address, and say what is uncertain.

# Name + state (primary criterion is name)
curl -G "https://webapi.ecourtsindia.com/api/partner/electoral/search" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
  --data-urlencode "name=ramesh kumar" \
  --data-urlencode "stateCode=S01" \
  --data-urlencode "age=35" \
  --data-urlencode "pageSize=20"

# q searches names AND address text (village / street / booth)
curl -G "https://webapi.ecourtsindia.com/api/partner/electoral/search" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
  --data-urlencode "q=kotturu" \
  --data-urlencode "stateCode=S01" \
  --data-urlencode "pageSize=5"

# Exact EPIC (REST path or MCP lookup_epic)
curl -X GET "https://webapi.ecourtsindia.com/api/partner/electoral/epic/ABC1234567" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

# Near-match EPIC (OCR confusions)
curl -G "https://webapi.ecourtsindia.com/api/partner/electoral/search" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
  --data-urlencode "epic=ABC1234567" \
  --data-urlencode "epicFuzzy=true"

# Household: copy the three values from any occurrence's Household key
curl -G "https://webapi.ecourtsindia.com/api/partner/electoral/search" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
  --data-urlencode "householdRollId=YOUR_ROLL_ID" \
  --data-urlencode "householdPartNumber=YOUR_PART" \
  --data-urlencode "householdHouse=YOUR_HOUSE"

# Capabilities (free): the field catalog. Do not invent params.
curl -X GET "https://webapi.ecourtsindia.com/api/partner/electoral/capabilities" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

A person object carries epic, Indic and Latin names, relative, relation (F / H / M), gender, approximate year of birth, a normalised matchScore (0–100), matchedOn (why it matched) and occurrences[]. Each occurrence has roll year, roll type, language, assembly constituency, part, serial, house, houseNorm, address, polling station, pincode, deleted (removed in a later revision) and partFlags. groupByEpic defaults to true. Occurrence id looks like 44-109-3-4 (roll, part, page/box internals). Treat it as an opaque row id, not a public identifier.

Verified against the live electoral index and Solr schema on 28 August 2026: name=ramesh kumar + stateCode=S01 returned 5,753 persons; adding relativeName=konaram collapsed that to one EPIC with matchedOn nameLatinExact, relativeExact, stateMatch; q=kotturu + S01 matched ~89,550 persons via address text, not the elector name; a filters-only call (stateName=Odisha, no name/epic/q/household) returned MISSING_SEARCH_CRITERIA and was not charged.

LegalCheck: scored litigation background checks

Six REST endpoints run an asynchronous court-record check for an individual or a company and return a partner-safe legal-check.v1 report: POST /api/partner/legal-check (submit), GET /api/partner/legal-check/{code} (status), GET /api/partner/legal-check/{code}/report (report), GET /api/partner/legal-check (list), GET /api/partner/legal-check/{code}/log (search log), and free GET /api/partner/legal-check/models. This is a productised due-diligence job, not another search filter. You submit once, save data.code, poll the status URL, and fetch the report only after status is completed. Since MCP v4.46 it is also on the MCP tool list as submit_legal_check and get_legal_check; list, search log and models stay REST-only. The product UI lives at legalcheck.ecourtsindia.com. For the engineering thesis behind identity matching, see Building a Legal Due Diligence Engine. If you are weighing a scored check against querying case search yourself, the trade-offs are benchmarked in LegalCheck API: India’s Court-Record BGV API, and the product side for HR and onboarding teams is in LegalCheck: India’s Identity-First Legal Background Check.

Read this before you ship: the model id changed. The docs now list exactly one accepted model, eCI-1.0, described as the current public model for individuals and companies, released on 13 September 2026 against engine build 7.21.2. The same page states that earlier source model labels are not accepted for new submissions. Earlier revisions of this guide documented eCI-1.2, which was the label the engine echoed at the time, and sending it now returns 400 UNKNOWN_MODEL. The safest integration omits config.model altogether, because omitting it selects the current model anyway, and calls GET /api/partner/legal-check/models at build time if it wants to pin a version deliberately. The resolved model, with its gates, outputs, parameters and checksum, comes back inside every report under engine.model, so you can always tell which behaviour produced a given result.

Billing is submit-once, and the figure is now published. A newly accepted job costs ₹99 on Pay As You Go or ₹33 with an active subscription, charged exactly once when the job is accepted. Status, report, list and models reads are free, however many times you call them. A matching idempotent replay returns the original resource without another charge. A terminal processing failure is refunded exactly once to the original credit pools. That makes LegalCheck the only endpoint in the API with a flat published rate rather than a per-call credit deduction, which matters when you are costing a background-check product: one subject is one charge, no matter how many court records the engine ends up screening. The pricing page carries the full rate card, and we have reproduced it further down this guide.

The workflow

Call models first if you want to pin a version, then POST a subject, then poll, then GET the report. The estimate on submit is advisory, not an SLA. The docs now spell the sequence out as a named workflow in five steps: submit with an Idempotency-Key and save data.code, status_url and report_url from the 202; poll the status URL and wait for Retry-After: 5 between polls while the job is queued or running; fetch the report once status is completed; on a failed status record the stable error code rather than blindly resubmitting under a new key, because the accepted-job charge is refunded once; and on LEGAL_CHECK_QUEUE_FULL wait for an in-flight job to finish, since a matching replay with the original key still returns the original resource. When we ran the documented Amit Kumar / Patna example live on 3 September 2026, the submit returned 202 with estimated_seconds: 90, the same key replayed as the same resource with no second job, an early report call returned 409 NOT_READY, and the job completed in about two and a half minutes, screening 1,727 records across 18 states and returning 42 matches at default verbosity, of which 24 survived verbosity=compact. The model label on that run was the older eCI-1.2; on the current engine the same job runs under eCI-1.0 and build 7.21.2. Either way the point stands. That is a screening result, not proof that every match is the same human.

# 1. Models (free). Use only an id this endpoint returns; today that is eCI-1.0.
curl "https://webapi.ecourtsindia.com/api/partner/legal-check/models" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

# 2. Submit. Reuse Idempotency-Key with the same body after a timeout.
curl -X POST "https://webapi.ecourtsindia.com/api/partner/legal-check" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: vendor-1042-attempt-1" \
  -d '{
  "subject_type": "individual",
  "subject": {
    "name": "Amit Kumar",
    "father_name": "Ramesh Kumar",
    "aliases": ["Amit R Kumar"],
    "date_of_birth": "1988-04-12",
    "addresses": ["Patna, Bihar"],
    "gender": "male"
  },
  "config": {
    "model": "eCI-1.0",
    "purpose": "vendor_onboarding",
    "ticket_size": 250000,
    "client_ref_no": "VENDOR-1042"
  }
}'

# Company subject (required field is subject.company_name)
# "subject_type": "company",
# "subject": { "company_name": "Example Private Limited", "cin": "U63030KA2015PTC079123" }

# 3. Poll status. Honour Retry-After (live: 5 seconds while queued/running).
curl "https://webapi.ecourtsindia.com/api/partner/legal-check/LC-A1B2C3D" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

# 4. Report only after status is completed. 409 NOT_READY means keep polling.
curl "https://webapi.ecourtsindia.com/api/partner/legal-check/LC-A1B2C3D/report?verbosity=standard&min_score=40" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

# 5. List this partner's jobs (summaries only; full reports stay on /report)
curl "https://webapi.ecourtsindia.com/api/partner/legal-check?status=completed&page=1&page_size=20" \
  -H "Authorization: Bearer eci_live_YOUR_TOKEN_HERE"

Python: submit, poll, then fetch the report

import time, requests

BASE = "https://webapi.ecourtsindia.com"
HEADERS = {
    "Authorization": "Bearer eci_live_YOUR_TOKEN_HERE",
    "Accept": "application/json",
    "Content-Type": "application/json",
    "Idempotency-Key": "vendor-1042-attempt-1",
}
body = {
    "subject_type": "individual",
    "subject": {"name": "Amit Kumar", "father_name": "Ramesh Kumar", "addresses": ["Patna, Bihar"]},
    "config": {"model": "eCI-1.0", "client_ref_no": "VENDOR-1042"},
}
submit = requests.post(f"{BASE}/api/partner/legal-check", headers=HEADERS, json=body)
submit.raise_for_status()
job = submit.json()["data"]
code = job["code"]  # save this; status_url / report_url are relative paths

while True:
    status = requests.get(f"{BASE}/api/partner/legal-check/{code}", headers=HEADERS)
    data = status.json()["data"]
    if data["status"] in ("completed", "failed"):
        break
    time.sleep(int(status.headers.get("Retry-After", "5")))

if data["status"] != "completed":
    raise SystemExit(data.get("error") or data["status"])

report = requests.get(
    f"{BASE}/api/partner/legal-check/{code}/report",
    headers=HEADERS,
    params={"verbosity": "standard", "min_score": 40},
).json()["data"]
print(report["risk"]["band"], report["summary"]["total_matches"], "matches")

Node.js: the same submit, poll and fetch

const BASE = "https://webapi.ecourtsindia.com";
const HEADERS = {
  Authorization: "Bearer eci_live_YOUR_TOKEN_HERE",
  Accept: "application/json",
  "Content-Type": "application/json",
};

const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));

async function legalCheck(subject, clientRef) {
  const submit = await fetch(`${BASE}/api/partner/legal-check`, {
    method: "POST",
    headers: { ...HEADERS, "Idempotency-Key": `${clientRef}-attempt-1` },
    body: JSON.stringify({
      subject_type: "individual",
      subject,
      config: { client_ref_no: clientRef },
    }),
  });
  if (!submit.ok) throw new Error(`${submit.status} ${await submit.text()}`);
  const { code } = (await submit.json()).data;

  let state;
  for (;;) {
    const poll = await fetch(`${BASE}/api/partner/legal-check/${code}`, { headers: HEADERS });
    state = (await poll.json()).data;
    if (state.status === "completed" || state.status === "failed") break;
    await sleep(Number(poll.headers.get("Retry-After") ?? 5));
  }
  if (state.status !== "completed") throw new Error(state.error ?? state.status);

  const report = await fetch(
    `${BASE}/api/partner/legal-check/${code}/report?verbosity=standard&min_score=40`,
    { headers: HEADERS },
  );
  return (await report.json()).data;
}

const report = await legalCheck(
  { name: "Amit Kumar", father_name: "Ramesh Kumar", addresses: ["Patna, Bihar"] },
  "VENDOR-1042",
);
console.log(report.risk.band_5, report.summary.verified_matches, "verified matters");

Note what this deliberately does not do. It does not send config.model, so it always gets the current model rather than a label that will be rejected later. It builds the idempotency key from your own reference so a network retry replays instead of billing twice. And it reads band_5 and verified_matches rather than score, because a bare score of 0 can mean either a clean subject or an UNRESOLVED one, and those two need different handling.

Submit body

subject_type is required: individual or company. For an individual, subject.name is required; father_name is the usual corroborating field. Optional individual fields: aliases (string array), date_of_birth (YYYY-MM-DD), addresses (string array), gender, notes. For a company, subject.company_name is required; optional corroborators are directors, registered_addresses, cin, gst, pan and company_type. Those identifiers are partner-supplied context. Their presence does not mean the API independently verified a registry filing. config.model defaults to the current public model, which is eCI-1.0, and the safest thing you can do is leave it out entirely. If you do send it, send only an id that GET /api/partner/legal-check/models returns today. Anything else, including the eCI-1.2 label this guide carried until September 2026, comes back as 400 UNKNOWN_MODEL. config.purpose, config.ticket_size and config.client_ref_no are your business metadata; client_ref_no is the filter you will use on the list endpoint.

Make retries idempotent. Send Idempotency-Key (1–255 characters) on submit. Reuse the same key only with the same body. The live replay of a successful submit returned 202 and the original data.code. The same key with a different body returns 409 IDEMPOTENCY_CONFLICT. Empty or invalid bodies return 400 VALIDATION_ERROR. An unknown config.model returns 400 UNKNOWN_MODEL.

Status and concurrency

Statuses are queued, running, completed and failed. While queued or running, honour Retry-After. A failed status includes only a stable error code and a partner-safe message; keep data.code and meta.request_id for support. Resources are scoped to the authenticated partner. The docs are explicit that reading a code owned by another partner returns 404 NOT_FOUND without disclosing whether it exists, so someone else’s code is deliberately indistinguishable from a typo. Treat any non-2xx on status or report as not yours to read, and do not build retry logic that probes for codes.

Each partner may have at most three jobs queued or running. A fourth new submit returns 429 LEGAL_CHECK_QUEUE_FULL. Wait for one to finish; do not rotate idempotency keys to bypass the cap. If new submits are disabled, you get 503 LEGAL_CHECK_UNAVAILABLE; existing reads still work. Progress counters and current_stage are optional hints. The documented example shows current_stage: court_search with progress: { completed: 2, total: 5 }, but on the live job we ran they stayed at 0/0 and null until the job finished. Drive your client off status, never off the counters.

A failed LegalCheck now names why it failed. Four terminal codes are documented alongside the older ones, and all four arrive as 409. SEARCH_UNAVAILABLE means the job ended because court search was temporarily unavailable. LLM_UNAVAILABLE means model processing was temporarily unavailable. MAX_RESTARTS means the job could not complete within the retry limit, which in practice usually means a very common name with nothing to separate it. INTERNAL_ERROR is the safe catch-all terminal failure. All four are genuine failures, so the accepted-job charge is refunded once. The right handling is to record the code, keep data.code and meta.request_id for support, and resubmit later under a fresh idempotency key rather than retrying the old one, because the old key still resolves to the failed resource.

The report

Fetch GET /api/partner/legal-check/{code}/report only after status is completed. Query parameters: verbosity (compact returns confirmed matches only, standard is the default, full adds uncapped party lists, order details, scoring evidence and any available electoral family data), bands, min_score (0–100, default 40, and confirmed matches are always retained regardless of it), include=excluded to pull excluded records into matches whatever their score, and pretty=1 for indented JSON with identical content.

A correction worth flagging, because this guide had it wrong. bands does not take high, medium and low. It takes identity bands: confirmed, probable, possible and excluded. Passing explicit bands overrides verbosity, min_score and include together, so ?bands=confirmed,probable is the precise way to get exactly those two populations without inheriting a verbosity preset as well. The default filter the engine reports back to you, in engine.match_filter, is confirmed plus probable and possible with a score of at least 40, and engine.query_params echoes the whole accepted grammar so you never have to guess.

verbosity=full is still partner-safe. The docs call this out as a gotcha in its own right: full verbosity increases the amount of allowlisted case detail, but it does not expose private engine inputs, model processing material or internal search data. What it adds is the evidence you need in order to argue with the result, which is exactly what a compliance reviewer wants. Treat the report’s limitations and disclaimer as part of the result rather than boilerplate you strip on ingest.

The payload is schema_version: legal-check.v1, and it is a great deal bigger than it was. At the top sit subject_echo (what you asked for, played back), risk (the rating and how it was arrived at), summary (the counts) and matches[] (the records). Below those sit six blocks that are easy to skip and should not be: watchlist, monitoring, report, links, ai_instructions, provenance and engine. Most integration bugs we see come from reading risk.score and ignoring everything that qualifies it.

The risk block, and what it is actually measuring

risk carries a numeric score, a three-way band such as AMBER, a five-way band_5 such as MEDIUM, a headline string, a rationale array, a qualitative confidence and identity_confidence as a number out of 100. The part that matters is the nested scoring object, because it publishes the method rather than asking you to trust a number. It states the method as a weighted roll-up of per-match risk_contribution, saturating, then clamped into the band window. It gives you the band_window the score was clamped into, the score_uncalibrated before clamping, and counted_matches. And it states the basis in plain words: confirmed matters brought against the subject only. Matters the subject filed do not count. Procedure inside a matter already counted does not count twice. Probable and possible matches never affect the rating at all.

"risk": {
  "score": 32,
  "band": "AMBER",
  "band_5": "MEDIUM",
  "headline": "MEDIUM risk. 5 verified matters.",
  "confidence": "high",
  "identity_confidence": 97,
  "scoring": {
    "method": "weighted roll-up of per-match risk_contribution, saturating, then clamped into the band window",
    "band_window": [25, 49],
    "score_uncalibrated": 32,
    "counted_matches": 2,
    "basis": "confirmed matters brought AGAINST the subject only",
    "context": { "ticket_size": null, "purpose": null }
  }
}

The one band you must special-case is UNRESOLVED. The report’s own reading rules are blunt about it: a band_5 of UNRESOLVED means records exist under this name and none of them could be attributed to this subject. It is not a clean result, and the accompanying risk.score of 0 must not be read as one. If your onboarding flow maps a score to a traffic light, UNRESOLVED has to route to manual review rather than to green. This is the single most consequential line in the whole schema, and it is the one a careless adapter is most likely to flatten.

The summary block now slices the matches several ways

summary gives you total_matches (how many rows this payload carries, which moves with your query parameters) alongside verified_matches and records_screened, which do not. Then it breaks the same set down by by_band (confirmed, probable, possible, excluded), by_status (pending, disposed, unknown), by_nature (criminal, civil, consumer, regulatory), by_court_tier and by_category (litigation, fir, defaulter, aml, regulatory). The genuinely new and useful one is by_role, which splits matters into against_subject, brought_by_subject, procedural and unknown. A person who has filed forty recovery suits and a person who has forty recovery suits filed against them are not the same risk, and until this block existed you had to work that out yourself. Beside it sit serious_against_subject, moderate_against_subject and status_unverified, plus courts_covered, oldest_case_year, newest_activity, next_hearing_date and pending_matters.

Inside a single match

Each element of matches[] is a full case record plus the engine’s reasoning about it. The case side is familiar: cnr, case_number, filing_number, case_type, cause_title, nature, status and the whole date series, a court object with code, tier, state and a link, then judges, advocates, acts, sections, acts_and_sections_raw, a parties object with per-party matched flags and counts, and a counts object for orders, interim orders, judgments, hearings, interlocutory applications and duration in days.

Use status_normalized, not status. The raw status is whatever the register wrote, and the docs warn that a raw PENDING may be stale. status_normalized is the cleaned value and status_stale tells you when the engine thinks the register has not moved in a while. This is the same staleness problem the refresh API solves for case tracking, and the same answer applies: if a matter is load-bearing for your decision, refresh the CNR and look again.

The reasoning side is where the new fields live. match_confidence and match_band say how sure the engine is that this record is your subject, and match_breakdown shows its working across name, father_name, address, dob and pan, each reading full, partial, close or not_provided. severity and severity_reason grade the matter itself, convicted is a flat boolean, risk_contribution is the number that fed the roll-up and counted_in_risk_score tells you whether it fed it at all. subject_party pins down which side your subject was on, carrying a role_group of against_subject, brought_by_subject, procedural or unknown. If you build one thing on top of this schema, build a reviewer view that puts match_breakdown next to the cause title, because that is what turns an argument about a name into a decision.

At verbosity=full each match also carries an engine object holding the evidence trace: identifying and hard booleans, a why string, and a trace array of typed items each with a likelihood ratio, for example a name item for a father’s name appearing in the party string with lr: 400, and a geo item for locality with lr: 2. caps and anchors name the rules that bounded the conclusion. You do not need any of it to ship, but you will want all of it the first time a customer disputes a match.

research_urls: the report hands you the public pages

Every match carries urls and research_urls, and the docs are explicit that you should prefer these over reconstructing links from the CNR. urls gives the case page, the latest order, the judgment, an enumerated orders[] list and a matching true_copies[] list of certified-copy PDFs. research_urls goes further: the subject’s litigant page, a filtered subject_pending_matters link, subject_in_this_court, the directory page for every party, advocate and judge on the matter, a court_docket link, acts_search links, and an upcoming_hearings cause-list search for the subject by name. Every one is a public eCourtsIndia page that needs no API key, which means you can drop them straight into a report a non-technical reviewer will open. A links.url_templates block publishes the patterns themselves, including the slug rule: lowercase the name and replace each run of whitespace with a single hyphen, keeping all other punctuation exactly as the register wrote it.

The watchlist block, and why it says not_searched

This is the most quietly principled thing in the schema. The report carries a watchlist object with slots for sanctions, pep, adverse_media, warnings, willful_defaulter, mca_disqualified_director and an aml_total. On a court-record check every one of them comes back empty, with status: "not_searched" and a note saying so in as many words: those sources are not part of this check, the categories are present so an incumbent adapter maps cleanly, and they are never silently reported as clear. If you are replacing a screening vendor whose payload populates these fields, do not let your mapping layer turn an empty array into a pass. Read watchlist.status first. There is also a monitoring object with enabled and monitor_id, which is where ongoing monitoring will surface when it lands.

ai_instructions and provenance: the report explains itself

Because these reports are increasingly read by a model rather than a person, the payload carries an ai_instructions block addressed to that model. It points at a rules file at legalcheck.ecourtsindia.com/report-rules-llms.txt, states plainly that the report is not an identity certificate and not legal advice, then explains the bands, the links, the unresolved records and what to do when the rules file cannot be fetched. One line in it is worth memorising whether you are a model or not: being named is not wrongdoing. Another explains why two reports on the same subject can differ, which is that engine.model names the frozen matching behaviour while engine.version names the code build, so you compare engine.model.id before concluding that the underlying court records moved.

provenance closes the report with the audit trail: the authoritative source, records_searched as both a number and a human label (for example, 32 crore and rising), a generated_by string naming the engine build and the model together, the case and order URL templates, the reading rules, the attribution line and the disclaimer. Keep the disclaimer. Results are public court records plus probabilistic identity matching, and the report says in its own words that you should verify before adverse action. A match is not proof of wrongdoing.

engine.model, gates and the search log

engine is the provenance of the decision rather than of the data. It carries the build version, an evaluation counter, coverage and stats (records found against records attributed), a limitations array, the verbosity and match_filter that produced this particular payload, and a search_log_url pointing at /api/partner/legal-check/{code}/log when you need to see what the engine actually searched. Nested inside it, engine.model publishes the frozen behaviour: id and namespace, a status of current, the release date, the engine build range it applies from, a base_chain, an amendments list, a checksum and a named list of gates. Those gate names are the house rules of Indian identity matching written down, things like score/mass-party-cannot-identify, score/one-dispute-counts-once, score/honorific-not-a-given-name, score/patronymic-stem and intake/electoral-epic-purity. There is also an engine.electoral object recording whether the electoral roll was consulted to corroborate kin, and why not when it was not. If you are building the sort of due-diligence pipeline we described in Building a Legal Due Diligence Engine, store engine.model.id and engine.model.checksum against every report you keep. It is the only way to explain, a year later, why a rerun disagreed with the original.

The search log is a real endpoint, and it is free

The search_log_url in engine is not decoration. GET /api/partner/legal-check/{code}/log is the twenty-third partner endpoint, it landed in the 15 September 2026 deploy, and like every other LegalCheck read it costs nothing. It returns data as a JSON array rather than an object, one entry per search the engine actually ran. That is how you answer the question every due-diligence team eventually asks, which is why did it not find the case I know about. Without the log you are guessing at the query; with it you can see whether the name was searched at all, in which courts, and how many rows came back.

Three behaviours to code for. It is only available once the job is completed, so a poll against a job still running returns 409 NOT_READY, exactly like the report. A job that failed returns its own terminal error code rather than a partial log, because a job that did not finish has no attributable search history to publish. And a code belonging to another partner returns 404 NOT_FOUND without confirming it exists, the same as status and report. Give it a longer client timeout than the other reads: the server allows 120 seconds here against 60 elsewhere.

The four URL fields, and the one trap in them

Every report carries a report block with html_url, json_url, print_url and search_log_url, plus engine.search_log_url and risk.scoring.rubric_url. On the partner API the first three are all the same path, your report URL. The rendered HTML and print views belong to the signed-in LegalCheck web app, not to the API, so html_url is not a hosted page you can send to someone. Do not build a share-this-report link out of it. Render your own view from the JSON, or send people to legalcheck.ecourtsindia.com to sign in. search_log_url is the only one of the four that points somewhere genuinely different.

List returns summaries only, never full reports. Each row carries code, status, subject_type, subject_name, model, client_ref_no, risk_band, identity_confidence, queued_at, completed_at, status_url and report_url, wrapped in items with page, page_size and total. Filters: page (default 1), page_size (1–100, default 20), status (queued, running, completed or failed), from and to (YYYY-MM-DD, on creation date) and client_ref_no (exact). Having risk_band and identity_confidence on the list row is what makes a reconciliation dashboard possible without refetching every report, so set client_ref_no at submit time even if you think you will not need it.

Errors you will hit, and how to debug them

Handle these at the client layer, not in your business logic.

StatusCodeWhat it meansWhat to do
401INVALID_TOKENToken missing, malformed, or the Bearer prefix was left offSend Authorization: Bearer eci_live_.... If it still fails, read the current token from the dashboard.
401TOKEN_INACTIVE / TOKEN_EXPIREDToken was deactivated or has expiredGenerate a fresh token in settings and update your secret store.
403ACCOUNT_INACTIVEThe partner account is suspendedCheck billing, then contact support.
402INSUFFICIENT_CREDITSNot enough credit left for the callTop up; current rates are on the pricing page.
402SUBSCRIPTION_REQUIREDAn active subscription is needed before credits can be spentSubscribe from the pricing page.
400INVALID_CNRCNR is not the 16 character formatCheck the pattern: four letters then twelve digits.
400PAGE_SIZE_EXCEEDEDYou asked for more than the endpoint maximum (200 on case search, 100 on electoral search)Cap pageSize at 200 for case search (100 for electoral) and paginate.
400 / 404INVALID_FILENAME / ORDER_NOT_FOUNDOrder filename is missing, malformed, or not on that case (the prefixed storage name CNR-order-1.pdf typically returns ORDER_NOT_FOUND)Pass the bare filename from orderUrl, like order-1.pdf.
404CASE_NOT_FOUNDCNR is not in the index, or a typoConfirm the format. If it is genuinely missing, call refresh to fetch and add it, then read it.
404ORDER_NOT_FOUNDThe order file does not exist on that caseRe-read the case to get current orderUrl values.
405Method not allowedWrong HTTP methodRefresh is POST, not GET.
429RATE_LIMIT_CONCURRENTMore than 10 requests in flight at onceLower your worker count. This one clears in milliseconds, so a short retry is enough.
429RATE_LIMIT_MINUTEMore than 100 requests in the last minuteBack off with exponential jitter: 1s, then 2s, then 4s.
429RATE_LIMIT_HOURMore than 3,000 requests in the last hourSpread the job out, or ask for a higher ceiling.
429RATE_LIMIT_DAYMore than 50,000 requests todayResume tomorrow, or move to an Enterprise ceiling.
429TOO_MANY_CONVERSIONSThe PDF conversion pipeline is saturatedWait for the Retry-After header before retrying order-md.
500INTERNAL_ERRORRare, usually transient or a source server hiccup (not charged)Retry with backoff, capped at four. Sustained 500s go to support with the request_id.
400PAGE_INVALID / PAGE_SIZE_INVALIDPage or pageSize below 1 (electoral and some search calls)Do not send 0. These are rejected rather than clamped, and not charged.
400PAGE_EXCEEDEDElectoral page above 100Stay within 1–100.
400MISSING_SEARCH_CRITERIAElectoral search with no name, epic, q or complete household keyAdd a primary criterion. Filters alone are not enough.
400HOUSEHOLD_INCOMPLETE / HOUSEHOLD_EXCLUSIVEHousehold lookup missing a part, or combined with name/epic/qSend all three household fields and nothing else, or do a name/EPIC search.
400EMPTY_REQUEST / TOO_MANY_CNRSBulk refresh (status) with no CNRs, or more than 50Send 1–50 (status) or 2–50 (refresh) valid CNRs.
400BATCH_SIZE_EXCEEDEDMore than 100 CNRs on cause-list batch checkChunk into batches of 100.
400INVALID_OFFSETNegative offset on cause-list searchStart at 0.
400MISSING_FILTERAvailable-dates called with no location filterPass at least one of state, districtCode, courtComplexCode, court, courtNo.
400VALIDATION_ERRORLegalCheck request body or query could not be acceptedSend subject_type plus subject.name (individual) or subject.company_name (company).
400UNKNOWN_MODELRequested LegalCheck model is not supportedCall GET /api/partner/legal-check/models and use a returned id (currently eCI-1.0), or omit config.model entirely.
409NOT_READYLegalCheck report requested while the job is still queued or runningPoll status and honour Retry-After.
409IDEMPOTENCY_CONFLICTIdempotency key reused with a different LegalCheck bodyReuse a key only with the exact same JSON.
429LEGAL_CHECK_QUEUE_FULLThree LegalCheck jobs already queued or running for this partnerWait for one to finish. Do not rotate keys to bypass the cap.
503LEGAL_CHECK_UNAVAILABLENew LegalCheck submits temporarily disabledExisting status/report/list reads still work. Retry submit later.
404NOT_FOUNDRequested resource was not found or is not visible to this partner. Covers both a court-structure lookup that matched nothing and a LegalCheck code owned by another partnerWalk the court tree from states rather than guessing codes. Do not probe for LegalCheck codes.
409SEARCH_UNAVAILABLELegalCheck ended because court search was temporarily unavailableTerminal. The job charge is refunded once. Resubmit later under a fresh key.
409LLM_UNAVAILABLELegalCheck ended because model processing was temporarily unavailableTerminal and refunded. Same handling as SEARCH_UNAVAILABLE.
409MAX_RESTARTSLegalCheck could not complete within the retry limitTerminal and refunded. Usually a very common name. Add father_name or an address and resubmit.
409INTERNAL_ERRORLegalCheck ended with a safe terminal failureKeep data.code and meta.request_id and send both to support.
400INVALID_PARAMETERA request parameter is invalidCheck the value against the capabilities endpoint for that family.
400MISSING_PARAMETERA required parameter is missingThe message names it. Most often a path segment or a required body field.

Every response carries a request_id. Look in meta.request_id on both success and error payloads. Your full API request log, including failures, is visible in your dashboard, so you rarely need to build special storage just to debug. When you do raise something with support, send the request_id and the CNR and we can pull that exact call.

What each call costs

Pricing is now published per endpoint, so you can cost an integration before you write it. Every plan runs on the same rate card with one multiplier. Pay As You Go is billed at about three times base, Enterprise pays base. It is exactly 3× on every case, order, cause-list and LegalCheck call, and 2.86× on electoral, which is the one row that does not follow the rule. New accounts get ₹200 in free credits and no card is required. Pay As You Go is a prepaid deposit pack of ₹1,000, ₹2,500 or ₹5,000 whose credits never expire. Enterprise Monthly is ₹10,000 a month for 10,000 credits at base rate, and there is now an Enterprise Annual tier at ₹1,00,000 a year for the same 10,000 credits a month. All figures are before 18% GST. The trade-off is simple: Enterprise credits are cheaper per call but do not roll over between months, while Pay As You Go costs three times as much per call and never expires.

EndpointPay As You GoEnterprise
Case Search₹0.60₹0.20
Case Detail₹1.50₹0.50
Case Refresh₹0.15₹0.05
Bulk Case Refresh₹0.15 × N₹0.05 × N
Cause List Search₹3.00₹1.00
CNR Cause List Check (batch)₹0.30 × N₹0.10 × N
Order Download (PDF)₹3.75₹1.25
Order Markdown & PDF₹5.25₹1.75
Order + AI Summary₹7.50₹2.50
Electoral Roll Search / EPIC Lookup₹1.00₹0.35
LegalCheck, per accepted job₹99₹33

Free, with a token and no credit deduction: the court-structure walk, cause-list available dates, the live enum reference, search capabilities, electoral capabilities, bulk-refresh status, and every LegalCheck read (status, report, list, models and the search log). Rejected requests are not charged, but empty ones are. A 400 validation error, a 404 CASE_NOT_FOUND and a 500 all log at zero. A successful call is charged even when it returns nothing, so a search that matches zero cases is a 200 and costs exactly what one matching ten thousand costs. That is the real price of the wrong-court-code trap described above: you do not get an error, you get an empty page you paid for. Validate codes against the free enum endpoint before you search in a loop. Failed refreshes are refunded with refunded: true, and a LegalCheck that fails terminally is refunded once. Three ratios are worth internalising before you design anything. An AI order summary costs roughly twelve times a case search. A refresh costs a tenth of a case detail. And a LegalCheck costs about what 165 case searches cost on the same plan, which is the whole argument for knowing when to run a scored check and when to query the index yourself. We worked that decision through in the LegalCheck API benchmark, and the portfolio-monitoring side of the same maths is in Litigation Portfolio Monitoring for General Counsel.

These are the default rates. Billing resolves a per-partner override first and only then falls back to the published card, and a good number of accounts are on negotiated rates. If yours is one of them, the dashboard shows what actually applies to you, and that is what you are billed at rather than the numbers above.

Rate limits, in plain numbers

Every partner account starts on the same default limits. They are generous enough that you will only brush against them if you parallelise hard on day one, and you can ask for higher ceilings on an Enterprise plan.

LimitValue
Per minute100 requests
Per hour3,000 requests
Per day50,000 requests
Concurrent10 requests

Each ceiling returns its own code, so you can tell a burst problem from a quota problem without parsing the message: RATE_LIMIT_CONCURRENT, RATE_LIMIT_MINUTE, RATE_LIMIT_HOUR and RATE_LIMIT_DAY. Branch on those rather than on a generic 429, because the right response to each is different: a concurrency rejection clears in milliseconds and wants a short retry, a daily rejection wants you to stop until tomorrow. None of the four carries a Retry-After header, so use your own backoff clock for them. The only places you get a real Retry-After are TOO_MANY_CONVERSIONS on the markdown pipeline and the LegalCheck status poll, and in both of those you should honour the header exactly rather than guessing. LegalCheck also adds a separate cap on top of the request limits: at most three jobs queued or running per partner, and a fourth new submit returns 429 LEGAL_CHECK_QUEUE_FULL.

Set your client timeout to match ours

The server enforces hard request timeouts, and a client default that is shorter than ours will abandon calls we are still serving. Case detail, case search, electoral search, EPIC lookup, cause-list search, bulk refresh and bulk-refresh-status all cut off at 60 seconds. The LegalCheck search log allows 120 seconds. The three order endpoints, the PDF, the AI analysis and the markdown, run to 300 seconds, because any of them may be fetching and converting a scanned PDF from a government server on demand. The practical failure this causes is specific: a client default of 30 seconds will time out on order endpoints that would have succeeded, and you will conclude the order is missing when it was merely slow. Give the order endpoints their own longer timeout rather than one global value.

These are the default ceilings. Rate limits resolve per partner before they fall back to the published numbers, and some accounts run on raised ones. If your account has a negotiated ceiling, the dashboard shows what actually applies to you, and that is what you are limited at rather than the table above.

What teams build on the API

Four integration patterns come up again and again when developers tell us what they are wiring the API into. None of them needs anything beyond the endpoints covered above.

  • Practice-management sync. Push nextHearingDate into Google Calendar or Outlook, and file every new order PDF into Drive, SharePoint or your document management system. Run it off the daily watcher described above so only stale CNRs get refreshed.
  • CRM and credit enrichment. Attach a litigation footprint to a borrower, vendor or customer record by searching litigants for the legal name and its known aliases, so a credit or procurement team sees exposure before it approves a limit. When you need a scored, identity-matched answer instead of raw hits, send the subject to LegalCheck.
  • Due-diligence dashboards. Search a deal target’s legal names across the petitioner and respondent fields, facet by court level and status, and show pending against disposed matters on one screen. The identity-matching problem underneath is covered in Building a Legal Due Diligence Engine.
  • AI assistants and RAG. Pull order markdown into your model’s context so a research copilot answers from real order text, with the CNR as the citation key. For agent setups, skip the glue code and point the model at the hosted MCP server described in the next section. How the underlying records are cleaned and structured is explained in Building India’s Legal Data Engine.

One rule holds for all four. The JSON is for your app, and the court’s own signed PDF is for the bench. When a record ends up in a filing or an evidence bundle, cite the downloaded order rather than the API response. And if a client only wants to know when their matter moves, you may not need to build anything: case tracking with WhatsApp and email alerts already runs on the website, as shown in how to enable case alerts for any court case, with rates on the pricing page.

Prefer an AI agent? The same data over MCP

Everything in this guide is also available over the Model Context Protocol, so an AI coding assistant or agent framework can reach the same court, statute and electoral data without hand-writing HTTP calls. Add https://mcp.ecourtsindia.com/mcp as a custom connector and click Connect so the client signs in at eCourts (OAuth). Do not run a local stdio server. If your host cannot do OAuth, the same URL accepts ?token=YOUR_API_KEY as a fallback. The MCP speaks Streamable HTTP and spends the same credits as the REST API. There are 39 tools (server v4.46, checked 23 September 2026): cases and orders, district/taluka cause lists, the free statute book (search_statute, lookup_provision, lookup_case_provisions, map_provision, get_cpc_rule, lookup_offence, list_instruments), and electoral rolls (search_electoral_roll, lookup_epic, free get_electoral_capabilities). LegalCheck is on MCP too: submit_legal_check screens a person or company (₹99, or ₹33 on a subscription, charged once) and the free get_legal_check waits for the job and returns the report. Nineteen of the 39 tools never consume credits. In Claude Code the one-line setup is claude mcp add --transport http ecourts-india https://mcp.ecourtsindia.com/mcp, then run /mcp to sign in. See the live tool catalogue and per-client setup. For a walkthrough, see our step-by-step guide to connecting the eCourtsIndia MCP to Claude.

FAQs

How do I get the court list and all the codes?

How to use the eCourtsIndia API, square social cover for the eCourtsIndia blog

Call GET /api/partner/enums?types=caseType,caseStatus,courtCode,stateCode for the code dictionaries, and walk GET /api/partner/causelist/court-structure/... from state to district to complex to court for the hierarchy. Both are free; the hierarchy walk needs your Bearer token but is not billed. Do not hardcode, pull at build time.

Where do I create and manage my API keys?

At ecourtsindia.com/dashboard/settings. You can generate multiple tokens and name each one by use, for example production, staging and nightly-job. Naming them lets you read usage per environment and revoke one without breaking the others.

Where can I see my API call logs?

In your dashboard. Every request, successful or failed, is logged with the endpoint, timestamp and outcome. You do not need to store request metadata yourself just to debug. Keep the request_id from responses if you want fast correlation with support.

Why does my search return cases from many states?

Because you did not add facets. The general Query searches the whole index. Add StateCodes, CourtCodes, CaseTypes and FilingYears to narrow it the way the website’s Case Status form does.

What is the difference between the litigant filter and the general query?

Litigants searches only the party fields, both sides. The general Query is a full text search that also reads the body of every order, so it returns far more, including mentions of the name inside an order rather than as a party. Live check on Delhi HC: litigants=Shubham Pratap Singh returned 2 cases; the same string in query returned 595. Use Litigants when you want actual parties.

Why do I only get one page of results?

Results are paginated. Partner case search allows up to 200 rows per page (default 20). Loop Page until hasNextPage is false. Electoral search is capped at page 100 and pageSize 100.

How do I get a precise result instead of a noisy one?

Search the right field and add facets first. For a name, use the Litigants (or Petitioners/Respondents) field instead of the general Query, then narrow with CourtCodes, CaseTypes and FilingYears. That removes the noise that comes from a name matching deep inside an unrelated order body. When a facet cannot express the constraint, Query also accepts operators: quoted phrases, uppercase AND/OR/NOT, parentheses and trailing wildcards.

The order field gives me a name like DLHC010001232024-order-1.pdf. How do I open it?

That is the internal storage name. Pass the bare filename from orderUrl, which is order-1.pdf, to GET /api/partner/case/{cnr}/order/order-1.pdf. In most cases you do not even need that call, since the full order text is already in files.files[].markdownContent on the case response.

Is the refresh API synchronous?

No. It queues a re scrape from the official source and returns immediately. Poll GET /api/partner/case/{cnr} until dateModified advances. Metadata updates settle quickly, but new order PDFs that need OCR, or a slow government server, can take a few minutes. It is also POST, not GET.

What happens if I refresh a CNR that is not in your database?

It gets fetched and added. Refresh is not limited to known cases. Call refresh on any valid CNR you do not see, wait, then read it through the case endpoint and it will be populated from the official source. Either the single or the bulk refresh endpoint works this way.

I refreshed a case but search still shows the old data. Why?

The case endpoint updates quickly, but the full text search index reindexes in batches, not on every change, because the dataset is enormous. A freshly refreshed or newly added case can take up to one to two hours to appear or update in search. For the freshest view of a specific case, always read it by CNR rather than relying on search.

Why does refresh sometimes take longer or fail once?

Because refresh reaches through to the official government court servers in real time, and those servers are not always fast or reliable. A slow response or an occasional retry is usually the source having a moment, not your request. Build in a poll with a timeout, and retry transient failures with backoff.

How do I get notified when a case changes?

There are no webhooks yet, so run a daily job that refreshes and fetches each watched CNR, compares caseStatus, nextHearingDate, orderCount and decisionDate against your stored snapshot, and alerts on any change. See the daily alerts section above for working code. Use bulk refresh for the watchlist and compare off the case endpoint, not search.

How does Enterprise pricing and top up work?

The Enterprise plan is a monthly subscription of ₹10,000 that becomes API credit you spend on calls. The real benefit is the rate: per call prices on Enterprise are typically about one third of the Pay As You Go rate, so the same rupee of credit goes roughly three times as far. The monthly subscription credit is use it or lose it within the billing cycle, so if you spend ₹8,000 in a month, the remaining ₹2,000 expires at the end of that cycle. You can top up at any time during the month, and top ups are charged at the same Enterprise rate. Crucially, top up credit does not expire and carries forward. The system always spends your monthly subscription credit first and only draws on top up credit once the monthly credit is used up, which is what makes topping up safe: an extra top up will never be wasted, it simply rolls into the next cycle and is consumed after that cycle’s subscription credit. Current rates and plan details are on the pricing page.

Can I fetch cause lists for next month?

No. The cause list window is roughly one day back to seven days forward, because lists change up to the hearing. Queries outside that window return empty by design.

How do I cite an API call in research?

Use the request URL, the request date and the case identifier, for example "Retrieved via eCourtsIndia API endpoint GET /api/partner/case/DLHC010001232024 on 1 June 2026". For aggregate dataset citations, contact us for a reference.

What is the difference between a signed and an unsigned order PDF?

By default the order endpoints return the watermarked, digitally signed certified true copy, which is what you file or show a client. Add ?signed=false to get the raw court PDF with no watermark or signature, which is better for your own OCR or text extraction. The cost is the same and the markdown text does not change.

The order AI analysis came back null. Is the order missing?

No. The AI analysis on order-ai is generated on demand the first time it is requested, which takes 10 to 60 seconds, then cached. A null just means it is still processing. Wait 15 to 30 seconds and retry, up to three times. If you only need text, use order-md or the markdownContent already in the case response.

What are the API rate limits?

How to use the eCourtsIndia API, X share card for the eCourtsIndia blog

The defaults are 100 requests a minute, 3,000 an hour, 50,000 a day, and 10 concurrent requests. Crossing one returns a 429, so back off exponentially and retry. Enterprise plans can raise these ceilings.

Why is my case data not up to date?

How to use the eCourtsIndia API, square social cover for the eCourtsIndia blog

Because GET /api/partner/case/{cnr} reads the eCourtsIndia cache, not a live scrape of the government portal. The cache is large (32 crore+ cases) and kept reasonably fresh by background workers, but a hearing that happened today will not appear until that CNR is refreshed. The usual signal is a nextHearingDate that is already in the past on a still-pending case. Call POST /api/partner/case/{cnr}/refresh (or bulk refresh for a watchlist), wait for dateModified to advance, then read the case again. Do not refresh every CNR on every cron tick, because a typical case only changes after a hearing.

Do you scrape the government portal on every case or search call?

How to use the eCourtsIndia API, square social cover for the eCourtsIndia blog

No. Case detail and search hit data that already sits on eCourtsIndia servers, which is why those calls are fast (typically milliseconds). Refresh is the separate, asynchronous path that re-scrapes a CNR from the official source when you need it. Order PDFs and AI analysis can still fetch or convert on demand the first time; see the order sections above.

How should I refresh a daily watchlist without wasting credits?

How to use the eCourtsIndia API, square social cover for the eCourtsIndia blog

Be selective. Store a small snapshot per CNR (caseStatus, nextHearingDate, orderCount, decisionDate). Refresh only when the cached nextHearingDate is before today (and the case is not disposed), when a user clicks “get the latest,” or when you are adding a new CNR. Use POST /api/partner/case/bulk-refresh for the batch, then poll POST /api/partner/case/bulk-refresh-status (or watch dateModified on each case). Compare changes off the case endpoint, not search. There is no separate “delta price” for unchanged cases. The saving comes from not refreshing CNRs that are not stale.

What do case detail and refresh cost per call?

How to use the eCourtsIndia API, square social cover for the eCourtsIndia blog

Rates are published on the pricing page and can change, so treat this as a snapshot of the live table. As of August 2026: case detail GET /api/partner/case/{cnr} is ₹0.50 on Enterprise (1×) and ₹1.50 on Pay As You Go (3×); single or bulk case refresh is ₹0.05 per CNR on Enterprise and ₹0.15 on Pay As You Go. Cause-list CNR batch checks remain ₹0.10 / ₹0.30 per CNR as noted above. Always confirm current figures on the pricing page before you model volume.

Are failed refreshes charged? How do I check refresh status?

How to use the eCourtsIndia API, square social cover for the eCourtsIndia blog

Billing is per successful request. Failed refreshes are refunded, and the bulk refresh status payload marks refunded: true when credits for that CNR were returned. After you queue a refresh, poll POST /api/partner/case/bulk-refresh-status with the CNRs (works for single or bulk queues) instead of hammering case detail. Government source outages and delays are outside our control; build a timeout and retry with backoff. Transient INTERNAL_ERROR responses are not charged.

Is there an unlimited flat monthly subscription for API calls?

How to use the eCourtsIndia API, square social cover for the eCourtsIndia blog

No. Pricing is per successful API call (Pay As You Go or Enterprise credit). We do not offer an unlimited flat plan against this corpus. Enterprise gives you ₹10,000 of monthly subscription credit at the 1× rate, with optional top-ups that carry forward. See the Enterprise FAQ above and the pricing page.

Is query and CNR log data kept confidential?

How to use the eCourtsIndia API, square social cover for the eCourtsIndia blog

CNRs and request metadata identify client matters. We retain API logs for billing, debugging and support on your account. We do not use partner query logs for our own commercial products or to compete on your matters. Self-serve log deletion is not available on every plan today; larger enterprise agreements can include retention controls, so contact sales if you need that in writing.

What happens if eCourts blocks or changes upstream? Is there an SLA?

How to use the eCourtsIndia API, square social cover for the eCourtsIndia blog

There is no contractual uptime SLA on government court portals. When upstream sites change, throttle or block scrapers, we treat recovery as a core product priority, because our business depends on keeping the layer available, but we cannot guarantee a fixed restore time. If data is obtainable from the official source in India, we work to restore access. Design clients for retries, refresh status polling, and graceful degradation when a source is temporarily down.

How do I look up a case by number exactly?

Use caseNumbers on GET /api/partner/search. Each value is normalised to N/YYYY and exact-matched against registration or filing number, so a common number can hit two cases (live: 9623/2024 on Delhi HC matched both a filing number and a registration number). Combine with courtCodes and caseTypes. Query=138/2024 is a ranked full-text fallback, not an exact filter.

Does the API search electoral rolls?

Yes. REST GET /api/partner/electoral/search (name, epic, q or household), /epic/{epic}, and free /capabilities. MCP: search_electoral_roll, lookup_epic, get_electoral_capabilities. Grouped by EPIC. Primary criterion required. ECI stateCode values such as S01, not court DL. Solr field map is in this guide, so do not invent village= or pincode= filters.

How do I connect the MCP without pasting a token in the URL?

Add https://mcp.ecourtsindia.com/mcp as a custom connector and click Connect so the host opens the eCourts login (OAuth). Leave Client ID and Secret empty. ?token= remains a fallback for clients that cannot do OAuth. Do not run a local stdio server.

Can I filter electoral search by village, PIN or polling station?

No. Those fields are stored on each occurrence. Village, street and booth text are searchable via q (Solr searchable_text includes addressText). PIN is a facet (facets=pincode), not a query filter. Call free GET /api/partner/electoral/capabilities and only send listed filters.

Why did query miss a case I know exists by filing number?

Because query tokenises punctuation. A filing number such as 9623/2024 is not a reliable full-text match. Use caseNumbers=9623/2024 (exact on filing or registration number) plus courtCodes. Copy courtLocationFacetPath from a hit (e.g. 0/DL); do not send invented paths such as 0/26.

Does the API run a LegalCheck / litigation background check?

Yes. Six REST endpoints under /api/partner/legal-check: POST submit (charged once), GET status, GET report, GET list, GET search log, and free GET models. Submit is asynchronous: save data.code, poll status (honour Retry-After), and fetch the report only after completed. Since MCP v4.46 an agent can run it with submit_legal_check and get_legal_check.

When is a LegalCheck charged? Are status and report calls free?

A newly accepted submit is charged exactly once. Status, report, list and models reads are free. Replaying the same Idempotency-Key with the same body returns the original job without another charge. The same key with a different body returns 409 IDEMPOTENCY_CONFLICT. Terminal processing failures are refunded once. Confirm the current rate on the pricing page.

Why did GET report return 409 NOT_READY?

The job is still queued or running. Poll GET /api/partner/legal-check/{code} and honour Retry-After (live: 5 seconds). Do not hammer the report URL. Typical completion is on the order of 1–3 minutes; estimated_seconds on submit is advisory, not an SLA.

Is a LegalCheck match proof that the case belongs to my subject?

No. Matches are probabilistic identity screening over public court records. Use match_band, match_confidence, limitations, provenance and the disclaimer together. Compact verbosity returns confirmed matches only. Common names produce many candidates. A live Amit Kumar / Patna example screened 1,727 records and returned 42 matches. Verify identity before adverse action.

Which LegalCheck model should I send?

Send eCI-1.0, or simply omit config.model, because omitting it selects the current public model anyway. The docs state that the current public model is eCI-1.0 for individuals and companies and that earlier source model labels are not accepted for new submissions. If you are still sending eCI-1.2 from an older version of this guide, your submits now fail with 400 UNKNOWN_MODEL. Call GET /api/partner/legal-check/models at build time rather than hardcoding an id.

What does a LegalCheck cost?

A newly accepted job costs ₹99 on Pay As You Go or ₹33 with an active subscription, charged once when the job is accepted. Status, report, list and models reads are free however many times you call them, a matching idempotent replay is not charged again, and a terminal processing failure is refunded once to the original credit pools. It is a flat rate per subject regardless of how many court records the engine ends up screening.

What does a LegalCheck risk band of UNRESOLVED mean?

It means records exist under that name and none of them could be attributed to your subject. The report’s own reading rules say plainly that this is not a clean result and that the accompanying risk.score of 0 must not be read as one. Route UNRESOLVED to manual review, never to an automatic pass. The unresolved rows carry the fields that would settle them, such as parent or spouse names in the party strings, age, court district, co-parties and counsel.

Does LegalCheck cover AML, sanctions or PEP screening?

No. It is a court-record check. The report carries a watchlist block with slots for sanctions, PEP, adverse media, willful defaulter and MCA disqualified director, but on every court-record check it returns status: not_searched with a note saying those sources are not part of the check. The categories exist so an existing screening adapter maps cleanly, and the documentation is explicit that they are never silently reported as clear. Read watchlist.status before mapping an empty array to a pass.

What values does the LegalCheck report bands parameter accept?

Identity bands: confirmed, probable, possible and excluded. Not severity words. Passing explicit bands overrides verbosity, min_score and include together, so ?bands=confirmed,probable returns exactly those two populations. The default filter, echoed back to you in engine.match_filter, is confirmed plus probable and possible with a score of at least 40.

Why did two LegalCheck reports on the same person differ?

Either the court register changed, or the two reports ran under different models. The report tells you which. engine.model names the frozen matching behaviour that produced it, including a release date, a named list of gates and a checksum, while engine.version names the code build. Compare engine.model.id before concluding that the underlying records moved. A newer model can also emit fields an older one does not, so a missing field is not necessarily a missing record.

Can I check my credit balance from code?

Not today. Balance, transactions and the API log live behind the dashboard’s user login, not behind your partner token, so an eci_live_ key is for the data endpoints only and cannot read its own balance. Two things to do instead. Treat 402 INSUFFICIENT_CREDITS as a first-class state in your client rather than a generic failure, so a job pauses cleanly instead of failing row by row. And turn on the low-balance email in the dashboard so a top-up is never a three in the morning surprise. If you want a balance endpoint on the partner token, tell us. It is a small change and demand decides it.

What has changed

Two breaking changes are described in prose above, which is no use to someone integrating today who wants one place to check what moved under them. This is that place.

DateChangeWhat breaks
15 Sep 2026GET /api/partner/legal-check/{code}/log addedAdditive, and free. It is the twenty-third documented endpoint.
15 Sep 2026LegalCheck accepts eCI-1.0 onlySending eCI-1.2 returns 400 UNKNOWN_MODEL. Omit config.model and you always get the current model.
2026Court structure moved under /api/partner/causelist/...The old /api/CauseList/court-structure/* path is no longer available to partner tokens.
2026Electoral roll and LegalCheck families addedAdditive. Nothing existing changed.

The general rule, so you can write your parser once and leave it alone: we add fields to responses without warning, and we do not remove one without noting it here. Parse defensively and ignore unknown keys rather than failing on them. A response that grows a field should never break your client.

TL;DR

Base URL is https://webapi.ecourtsindia.com. Authenticate with Authorization: Bearer eci_live_..., and remember the Bearer prefix is mandatory. Create and name your tokens at the dashboard, where every call is also logged. Pull court codes and case types from the free enum endpoint instead of hardcoding, and pull the search’s own capabilities (valid filters, sort fields, facets) from the free GET /api/partner/search/capabilities. To replicate the website Case Status search, prefer caseNumbers plus courtCodes and caseTypes; Query plus facets is the ranked fallback. Electoral search is a separate family under /api/partner/electoral/* and MCP search_electoral_roll / lookup_epic. LegalCheck is six REST endpoints under /api/partner/legal-check: POST once, poll status, GET the legal-check.v1 report after completed. Reads are free, the job costs ₹99 pay-as-you-go or ₹33 on a subscription, and MCP v4.46 wraps it as submit_legal_check and get_legal_check. The accepted model id is eCI-1.0, so omit config.model or send that, because eCI-1.2 now fails with 400 UNKNOWN_MODEL. On the report, bands takes identity bands (confirmed, probable, possible, excluded), a risk.band_5 of UNRESOLVED must route to manual review rather than green, a watchlist.status of not_searched must never be mapped to clear, and status_normalized beats the raw status. Use the Solr field map above; call capabilities before inventing params. Use Litigants for party search, with nameMatchMode=fuzzy for spelling variants, and the general Query only when you want full text including order bodies. Query accepts Solr operators (quoted phrases, uppercase AND/OR/NOT, parentheses, trailing wildcards; never lead with *), though plain keywords plus facets stay the robust default. Paginate with Page and PageSize, maximum 200 per page on partner case search (100 on electoral search). Order text is already embedded in the case response, and the order endpoint wants the bare order-1.pdf filename. Refresh is an asynchronous POST that also pulls in CNRs we do not have yet; poll dateModified, and note that search can take one to two hours to reflect a refresh. Build case alerts with a daily refresh, fetch and compare, and check listings with the batch cause-list endpoint, POST /api/partner/causelist/cnr/batch, which takes up to 100 CNRs at once. Cause lists cover one day back to seven days forward and search best by name. Court-structure lookups now live under /api/partner/causelist/court-structure/* with your Bearer token (free, no billing). Order PDFs default to a signed certified true copy, add signed=false for the raw court PDF. The order AI analysis is generated on demand, so retry if it comes back null. Default rate limits are 100 calls a minute, 3,000 an hour and 50,000 a day. Every response carries a request_id for support. Case and search reads hit our cached 32 crore+ layer in milliseconds; call refresh when nextHearingDate is stale rather than refreshing every CNR every night.


Keep building

More for developers and power users on the eCourtsIndia blog and site: the eCourtsIndia Search Guide, the Litigant Search Guide, Building a Legal Due Diligence Engine, From Case Lookup to Case Intelligence, and Litigation Portfolio Monitoring for General Counsel. Postman Workspace, Postman Doc

Start at the API home, read the live docs, check pricing, manage keys at the dashboard, or run a search at ecourtsindia.com/search.

Sources

eCourtsIndia API landing page, ecourtsindia.com/api. API pricing, ecourtsindia.com/api/pricing. API docs, ecourtsindia.com/api/docs. Live partner API surface, https://webapi.ecourtsindia.com. Search behaviour, enum counts, order endpoints and the sample CNR DLHC010001232024 (Shubham Pratap Singh vs Kendriya Vidyalaya Sangathan, W.P.(C) 138/2024, Delhi High Court) were re-verified against the live partner API and ecourtsindia.com/api/docs on 23 August 2026. Search capabilities and enum counts were pulled live on 23 August 2026; the case-type count (269), the 23-endpoint inventory and the corpus figure were re-checked on 23 September 2026. The electoral Solr field map and MCP electoral tools were verified on 28 August 2026. The case-search Solr field map was verified on 29 August 2026. The LegalCheck workflow, the 42-match Amit Kumar screening result and the idempotency behaviour were run live on 3 September 2026. The LegalCheck model change to eCI-1.0, the full legal-check.v1 report schema, the four new terminal failure codes, the error catalogue, the rate-limit table and the complete endpoint pricing table were re-verified against ecourtsindia.com/api/docs (page revised 14 September 2026) and ecourtsindia.com/api/pricing on 15 September 2026, and the MCP tool count against the live tool catalogue and /health (v4.46, 39 tools) on 23 September 2026. Report reading rules are published at legalcheck.ecourtsindia.com/report-rules-llms.txt.

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