District Court Cases API | CNR Lookups and Order History | CourtMesh
    Skip to main content
    District and subordinate courts

    District court records, with the coverage caveats printed on the tin

    This is where most Indian litigation actually happens and where the data is hardest. Establishment names are inconsistent, case type codes are not catalogued, the judge index does not reach the subordinate judiciary, and the analysed subset that powers semantic search leans heavily on higher courts. Everything below is written so you can size those limits before you write code against them.

    Open the documentation
    740
    establishment values
    verbatim strings in the district layer
    16
    characters in a CNR
    state, district, establishment, number, year
    0
    district judges indexed
    the judge lookup covers higher courts only
    100
    results per page
    cursor pagination for anything deeper

    The honest shape of subordinate court data

    Volume and depth run in opposite directions as you move down the judicial hierarchy. The corpus passes 310 million records overall and the subordinate courts contribute the bulk of that count, because they handle the bulk of the litigation. What arrives with those records, though, is mostly registry material: parties, dates, case types, orders. Long reasoned judgments of the kind reporters publish are rarer here by nature rather than by omission.

    That has a direct consequence for retrieval. The vector index behind semantic search covers the analysed subset of the corpus, roughly two million judgments, and that subset is weighted towards the Supreme Court and the High Courts, because those are the documents worth analysing at depth. So a semantic query will feel thin over district material while keyword search over the same layer feels rich. Neither observation is a defect. Knowing which is which is the difference between a working integration and a disappointed one.

    Rather than publish a coverage percentage you would have to take on trust, the API hands you per record evidence. Search results carry hasDocuments and hasAnalysis, and the case record adds documentCount. Sample an establishment, count the flags, and you have measured your own coverage in one request.

    Anatomy of a CNR

    The Case Number Record identifier is the one genuinely dependable key in the subordinate system. It is allotted at the court establishment and it survives renumbering, recategorisation and movement between courts inside that establishment, which is exactly what defeats every other identifier down here.

    SegmentWidthMeaning
    State2The state or union territory the establishment sits in
    District2The district inside that state
    Establishment2The specific court complex within the district
    Filing number6Sequential number allotted at that establishment
    Year4Year of filing, which is why a CNR sorts chronologically

    There is no cnr parameter. Send the number as the query string and let the parser handle it: it detects an alphanumeric prefix followed by a four digit year, strips separators, and searches both the normalised and the original form. That is a text match rather than a primary key lookup, so read the record that comes back before you trust it.

    Three levels, two routes, different reach

    The index models a court in three levels. Which of them you can filter on depends on which search route you are using, and the difference is larger here than anywhere else in the hierarchy, because a district establishment can contain a family court, a commercial court, a small causes court and a dozen magistrates.

    Court type

    Index level

    District Court

    Available on the semantic route as filters.courtType. Not a parameter on the keyword route.

    Establishment

    Index level

    District Court of Pune, District Court of Mumbai Small Causes Court, Chief Judicial Magistrate, Hooghly

    The court parameter on both routes. 740 values exist across the district layer.

    Individual court

    Index level

    Family Court Agra, Commercial Court Agra, Judge Small Cause Court, Jr. Civil Courts, Boath

    Available on the semantic route as filters.courtName. On the keyword route, put it in the query text.

    Your first district query

    Base URL https://research.courtmesh.ai/api/v1/prod, key in the X-API-Key header or as a bearer token, and query as the only required field. Every response uses the same envelope: success, data, meta and, on list routes, pagination.

    Two establishments, one financial year

    curl -X POST "https://research.courtmesh.ai/api/v1/prod/search/cases" \
      -H "X-API-Key: cm-YOUR_KEY_HERE" \
      -H "Content-Type: application/json" \
      -d '{
        "query": "specific performance agreement to sell possession",
        "court": ["District Court of Pune", "District Court of Ahmednagar"],
        "fromDate": "2021-04-01",
        "toDate": "2024-03-31",
        "limit": 50
      }'

    Measure before you build

    The most useful thing to run against a new establishment is not a search but a profile. This counts what the records actually carry, which is the number your integration lives or dies by.

    Python: profiling an establishment

    import requests
    from collections import Counter
    
    BASE = "https://research.courtmesh.ai/api/v1/prod"
    HEADERS = {"X-API-Key": "cm-YOUR_KEY_HERE", "Content-Type": "application/json"}
    
    # Establishment values are verbatim strings. They are not tidy, because the
    # source registries are not tidy: some read "District Court of Agra",
    # others "Chief Judicial Magistrate, Hooghly".
    ESTABLISHMENTS = [
        "District Court of Agra",
        "District Court of Mumbai City Civil Court",
        "District Court of BENGALURU",
    ]
    
    def profile(establishment):
        """Measure what a court's records actually carry before you rely on them."""
        body = {"query": "decree", "court": establishment, "limit": 100}
        r = requests.post(f"{BASE}/search/cases", json=body, headers=HEADERS, timeout=120)
        rows = r.json()["data"]
    
        tally = Counter()
        for row in rows:
            tally["records"] += 1
            tally["with_document"] += 1 if row.get("hasDocuments") else 0
            tally["with_analysis"] += 1 if row.get("hasAnalysis") else 0
            tally["with_decision_date"] += 1 if row.get("decisionDate") else 0
        return tally
    
    for name in ESTABLISHMENTS:
        print(name, dict(profile(name)))

    Order history, and the thing it is not

    Subordinate matters live in adjournments. A suit reaches judgment through dozens of dated orders, and the sequence of those orders is usually more informative than any single one of them. Two routes cover this: POST /request-timeline, which takes case_id in the body and starts a job, and GET /get-timeline/:requestId, which returns status and, on completion, orders, orderCount and totalOrderCount. A case whose orders were already fetched answers immediately with cached set.

    What this is not is a cause list. There is no listing route on this API and no next hearing date on any response, so nothing here will tell you when a matter comes up next. Order history is a backward looking record. Any product that needs forward listing information has to get it somewhere else, and we would rather write that sentence than let you discover it in production.

    Node: start a job, then poll it

    const BASE = "https://research.courtmesh.ai/api/v1/prod";
    const headers = {
      "X-API-Key": process.env.COURTMESH_API_KEY,
      "Content-Type": "application/json",
    };
    
    // Order history is a job, not a synchronous read. Start it, then poll.
    async function orderHistory(caseId) {
      const started = await fetch(`${BASE}/request-timeline`, {
        method: "POST",
        headers,
        body: JSON.stringify({ case_id: caseId }),
      }).then((r) => r.json());
    
      // A cached case answers immediately with orders already attached.
      if (started.data.cached) return started.data.orders;
    
      const requestId = started.data.requestId;
    
      for (let attempt = 0; attempt < 20; attempt += 1) {
        await new Promise((resolve) => setTimeout(resolve, 5000));
        const poll = await fetch(`${BASE}/get-timeline/${requestId}`, { headers })
          .then((r) => r.json());
    
        if (poll.data.status === "completed") {
          console.log(poll.data.orderCount, "of", poll.data.totalOrderCount);
          return poll.data.orders;
        }
        if (poll.data.status === "failed") throw new Error(poll.data.error);
      }
      throw new Error("timeline job still running");
    }

    What the district layer is good for

    Party exposure checks

    Search a counterparty name with no court filter and you see the whole footprint at once, which for most companies is overwhelmingly subordinate court litigation rather than reported appeals.

    Order cadence analysis

    Run the timeline job across a portfolio and you can measure how long matters actually sit between orders in a given establishment, which no single record will tell you.

    Establishment benchmarking

    Profile several establishments on the same query and the differences in record depth become visible, which is the honest basis for deciding where automated monitoring is worth running.

    Escalation tracing

    Start from a district matter, carry the parties and the impugned order into a High Court scoped search, and reconstruct the appellate path that no shared identifier gives you for free.

    District Court API questions

    Is there a cnr parameter for CNR lookups?
    No, and nothing in the request body maps onto one. Pass the CNR as the query string instead. The query parser recognises an alphanumeric prefix followed by a four digit year and normalises separators before searching, which is why a CNR pasted straight from an eCourts page usually works. Treat it as a text match rather than a key lookup, and confirm the record you get back before you build on it.
    What do the sixteen characters of a CNR mean?
    A CNR is a fixed width identifier issued by the eCourts system: two characters for the state, two for the district, two for the court establishment inside that district, six for the sequential filing number at that establishment, and four for the year of filing. Because the number is allotted at the establishment, it survives renumbering, transfer between courts within the establishment and changes of case type. That is what makes it the most stable identifier a subordinate court matter has.
    How do I scope a search to one district or one court establishment?
    Through the court parameter, using the verbatim establishment string. There are 740 of them in the district layer and they are inconsistent by nature, because they are what each state registry published: District Court of Agra sits alongside Civil Judge Sr. Divn, Hooghly and District Court of Mumbai Motor Accident Claims T. Pass an array when a district has several establishment strings, and expect to build a small lookup for the districts you care about rather than guessing the format.
    Can I filter by case type codes such as O.S. or Sessions Case?
    Not reliably. The case type catalogue holds 669 codes drawn from the Supreme Court and the 25 High Courts, and it contains no district entries, so a district case type will not expand and a primary type such as Writ or Appeal will expand into High Court codes rather than subordinate ones. Put the case type in the query text for this layer and rely on court, date and free text instead.
    Does the API return cause lists or the next hearing date?
    No. There is no cause list route and no next hearing field on any response. What exists is order history: POST /request-timeline starts a job for a case and returns a requestId, and GET /get-timeline/:requestId returns status along with orders, orderCount and totalOrderCount once it finishes. That tells you what has already happened on a matter. It does not tell you when the matter is next listed, and we would rather say so than let a listing product be inferred from a timeline endpoint.
    Are district judges searchable through the judge lookup?
    No. GET /judges/search reads two lists, 1,069 Supreme Court names and 3,590 High Court names, merged into 4,561 unique entries. The subordinate judiciary is not in that index, so passing a district judge name to judgeName will usually return nothing. Search the officer's name as free text in query, and accept that coverage of presiding officer names in the underlying records is uneven.
    How does district coverage compare with High Court and Supreme Court data?
    It is broader and thinner at the same time. The corpus runs past 310 million records overall and the district layer supplies much of that volume, but subordinate court records skew towards registry metadata and orders rather than long reasoned judgments, and reporting conventions mean far less of it has ever been written up. The AI analysed and vector indexed subset, roughly two million judgments, is drawn mostly from higher court material. So keyword search reaches district matters well while semantic search reaches them sparsely.
    How can I tell how good a particular record is before relying on it?
    Read the flags the response already gives you. Every search result carries hasDocuments and hasAnalysis, and the case record adds documentCount. Those three tell you whether a stored document exists, whether structured analysis has been derived, and how many files sit behind the record. Profiling a sample of an establishment on those flags takes one request and is far more useful than any coverage percentage we could quote you.
    Can I follow a matter from the district court up through appeal?
    Partly, and it takes work. Case numbers change at every tier, so GET /cases/:id/related will not bridge them: it groups documents that share one case number and returns a timeline of that single matter. To follow a dispute upward, carry the party names and the impugned order details into a fresh search scoped to the relevant High Court, then to the Supreme Court. The link is evidentiary rather than structural, which means a human should confirm it.
    What are the operational limits on this layer?
    The same as everywhere else on the surface: 10 requests per minute for an API key, a maximum of 100 results per page, and 429 responses carrying retryAfter when you exceed the cap. Because district volume is large, use the searchAfter cursor returned by the keyword route for deep pagination instead of walking page numbers, and remember that only GET /health is reachable without a key.