Skip to main content
    All articles

    The Complete Guide to Integrating an Indian Court Cases API

    2 June 202618 min readCourtMesh Team
    Cover card headed Endpoint Count Is the Wrong Metric, with the line: coverage, then limits

    Every court data API looks the same on a comparison page. They all list search, case detail, some form of document access, and a number of endpoints that is meant to impress you. Then you put real traffic through one and discover that the thing which decides whether your product works is not the endpoint count at all. It is how much of the Indian record is actually behind the search box, what the API does when a field was never published by the registry, and how many requests per minute you are allowed to make.

    This is a walkthrough of integrating a production Indian court cases API, written in the order you will actually do the work. It uses the CourtMesh API as the concrete example because vague integration advice is useless: you cannot reason about pagination in the abstract, and you certainly cannot design a backfill job without knowing the rate limit. The base URL is https://research.courtmesh.ai/api/v1/prod and the full endpoint reference lives at the API documentation.

    There are exactly twelve endpoints. That number is deliberately unimpressive, and the argument of this piece is that it should be. What differentiates a court data API is the size and provenance of the corpus behind it, and whether it offers anything beyond retrieval. Twelve endpoints roughly 310 million records sourced from official portals is a very different product from forty endpoints over a scraped subset of reported judgments, and no features table will tell you which one you are buying.

    Endpoint Count Is the Wrong Metric

    Endpoints are cheap. A vendor can split one search into six endpoints and call it a richer API. What is not cheap is coverage, and what is not cheap is analysis.

    Coverage first. Indian litigation is not the Supreme Court. It is the Supreme Court, 25 High Courts, the district judiciary and a set of tribunals that carry enormous commercial weight: NCLT and NCLAT for insolvency under the IBC 2016, ITAT and CESTAT for tax and excise, SAT, TDSAT and the DRT for their own domains. An API that indexes reported judgments only will answer your Supreme Court questions beautifully and be silent on the section 138 Negotiable Instruments Act 1881 complaint your client is actually facing in a magistrate's court in Ludhiana. The CourtMesh corpus is roughly 310 million cases pulled from official government portals, principally eCourts, the NJDG and court registries, with no reseller sitting in between.

    Analysis second, and here you should be equally sceptical of us. The keyword corpus and the AI analysed corpus are not the same thing and are not the same size. The semantically embedded, model analysed portion is in the low millions of documents, not 310 million. Anybody claiming that hundreds of millions of Indian judgments have each been read by a model is describing a compute bill that does not exist. Design your integration knowing that a record can be perfectly retrievable and carry no analysis at all.

    The two questions worth asking any court data vendor

    How many records are indexed, and how many of those carry AI derived analysis. The gap between those two numbers is the single most informative fact about a legal data API, and it is almost never on the pricing page. Indexed is not analysed. Build for a world where most records give you metadata and text, and a minority give you issues, holdings and precedent relationships.

    The Order That Works

    Do not start by reading the whole reference. Start by proving each link in the chain in the order a request actually travels. Every step below is a real call you can make in a terminal before you write a line of application code.

    1

    Hit the health endpoint with no credentials

    A GET to /health is the only endpoint that does not require a key. It returns success, a status of healthy, a version and a timestamp. This proves your network path, any corporate proxy, and TLS, all before authentication can confuse the picture. If this fails, nothing else you do matters.

    2

    Add the key and make the cheapest authenticated call

    Send your key as X-API-Key, or as Authorization: Bearer if that suits your HTTP client better. Then call GET /judges/search with a short query term. It is a case insensitive substring match over the combined Supreme Court and High Court judge name lists, it runs no model, and it returns a plain array of names capped at 50. It is the fastest way to confirm that your key is valid and being read from the right header.

    3

    Run your first real search

    POST to /search/cases with a body carrying query, and optionally court, caseType, year and limit. This is the keyword path, backed by OpenSearch over the full corpus. Do it with limit set low, because you are reading the shape of the response, not harvesting data yet.

    4

    Read the envelope before you read the results

    Parse success, data, meta and pagination as a unit. Write your client so that it branches on success first and never on the HTTP status alone. Log meta.responseTime, which comes back as a string such as 412ms, because it will tell you later which of your query shapes are expensive.

    5

    Follow one id into the detail endpoint

    Take the id from a single result and call GET /cases/:id. The path parameter accepts either the internal case id or a case number, which is convenient and also a trap you should understand before you rely on it. Note that analysis is not included here by design, and meta.note says so.

    6

    Then, only if you need them, the follow-on calls

    GET /cases/:id/analysis for the stored AI analysis, GET /cases/:id/related for other documents sharing the same case number plus a timeline, and GET /cases/:id/pdf for a time limited link to the stored document. Each is a separate request against your rate limit, which is why the fan-out from a single search result is the thing you must budget for.

    Authentication, and What the Errors Are Telling You

    Keys carry a prefix, then 32 base64url characters, then a four character suffix, in the shape cm-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXX. Older keys issued with a vv- prefix continue to work. Either header is accepted, so X-API-Key: your-key and Authorization: Bearer your-key are equivalent.

    One property matters more than the rest: keys are stored server side only as bcrypt hashes. The plaintext is displayed once at creation and cannot be recovered afterwards by us, by support, or by anyone. If you lose it you rotate it. Treat that as a feature and store the value in your secret manager at the moment of issue, not in a document you intend to tidy up later.

    The 401 responses are distinct on purpose, and reading them correctly saves an hour of guessing.

    ResponseWhat it actually meansWhat to do
    401 API key required. Provide it in X-API-Key header or Authorization: BearerNo credential arrived at all. Usually a proxy stripping headers, or a client library that drops custom headers on redirect.Log the outbound headers on your side, not the inbound ones. Check for a redirect between your client and the API.
    401 Invalid API key formatSomething arrived but it does not match the expected key shape. Almost always whitespace, a newline from a file read, or the word Bearer duplicated.Trim the value at read time. Assert its length in a startup check rather than at first request.
    401 Invalid API keyWell formed but not recognised. Wrong environment, or a key that was rotated and the old value is still deployed.Confirm which environment the running process is reading from before you assume the key is broken.
    401 API key is deactivatedThe key exists and has been switched off deliberately.This is the revocation path working. Issue a new key rather than reactivating a compromised one.
    429 Rate limit exceededYou exceeded 10 requests per minute on that key. The body carries retryAfter in seconds and resetTime as an ISO timestamp.Back off using retryAfter, not a fixed guess. Do not retry immediately in a loop.
    403 on an AI endpointAuthentication succeeded but the AI credit allowance is exhausted.Distinguish this from 401 in your alerting. It is a billing state, not a credential failure. See API pricing.

    Every authenticated call is logged against the key

    Endpoint, method, status code, response time, sanitised headers, query parameters and body, IP address and user agent are recorded per key, and lastUsedAt is updated on every call. Sensitive header and body values, including authorization, cookie, x-api-key, password, token and secret, are redacted before storage. This is useful to you: per key logging is what makes one key per environment and per workload a genuinely powerful debugging and revocation strategy rather than bureaucracy.

    This is the keyword endpoint, backed by OpenSearch across the full corpus. Only query is required, and it must be at least one character. Everything else narrows.

    FieldAcceptsNotes that matter in practice
    queryString, minimum length 1, requiredThis is lexical matching. It finds documents containing your words, not your idea. If you need meaning, that is the semantic endpoint, not this one.
    courtString, comma separated string, or arrayThe single most effective way to cut result noise. Apply it early, not as an afterthought.
    caseTypeString, comma separated string, or arrayValues are expanded server side from a primary type into the underlying registry codes. Asking for Appeal expands to a set including CA, CRA, LPA and others, so you do not have to memorise every registry abbreviation.
    caseNumberString, comma separated string, or arrayUseful when you already hold the number. Remember that display case numbers repeat across courts and years.
    judgeNameString or array, with aliases judges and judgeAccepting three spellings of the same field is a small kindness that saves a support ticket.
    yearFour digit number or string, or an array of themValidated to a minimum of 1947 and a maximum of the current year. Send 47 and you will get a validation error, not a guess.
    fromDate and toDateStrict YYYY-MM-DDNo lenient parsing. Format your dates once, in one helper, and never build them by string concatenation at the call site.
    page and limitIntegers. limit defaults to 20 and caps at 100The cap is real. Asking for 500 does not get you 500.
    searchAfterString cursorThe right tool for walking deep result sets. Offset paging degrades as you go deeper; a cursor does not.
    sortByrelevance or date, defaults to relevanceAnything else fails validation. Sort by date when you are building a chronology, by relevance when you are researching.

    Two failure modes are worth handling explicitly rather than as generic errors. A very long running query returns 408 at 90 seconds, and an upstream failure returns 502. Neither should be retried with the same query and no delay. A 408 usually means the query is too broad, and the correct response is to add a court or date filter, not to try again harder.

    The Envelope and the Pagination Object

    Every successful response has the same shape: success set to true, data carrying the payload, meta carrying request metadata, and, on list endpoints, pagination. The meta object commonly echoes your query and filters, carries responseTime as a string, and sometimes a note that tells you what to call next. Errors invert it: success false and an error string, with validation failures adding a details array of human readable messages, one per offending field, each prefixed by the field path.

    The pagination object is page, limit, total, totalPages and hasMore. Use hasMore as your loop condition rather than computing it yourself from page and totalPages, because the server has already resolved the edge case you are about to get wrong.

    Write one response handler for the whole API and never write another. The consistency of the envelope is the reason you can do this: a single function that checks success, unwraps data, surfaces details on validation errors, and reads pagination will serve all twelve endpoints. This is worth more to your delivery timeline than any SDK, because it is code you understand and can debug at three in the morning.

    Ten Requests Per Minute Is an Architectural Constraint

    API key requests are limited to 10 per minute per key, as a fixed window. Session requests made by the product's own web application are allowed 200 per minute, which is a useful contrast but not available to you as an integrator. There is no documented per day cap, and you should not assume one exists in either direction.

    Ten per minute sounds restrictive until you notice what it forbids, which is mostly things you should not be doing anyway. It forbids synchronous fan-out. It forbids calling the API from a request handler that a user is waiting on. It forbids treating a court data API as a cache you can hit repeatedly for the same record.

    Design accordingly. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, so your client can steer rather than crash into the wall. On breach you get 429 with retryAfter in seconds and resetTime as an ISO timestamp, and the only correct behaviour is to honour retryAfter.

    • Queue everything. Put API calls behind a job queue with a single rate limited worker per key. A queue turns a hard limit into a throughput characteristic instead of an outage.
    • Cache case records aggressively. A decided judgment does not change. Fetch GET /cases/:id once, store the payload, and serve from your own store. Pending matter status changes, so cache that with a short life instead.
    • Budget the fan-out. One search returning 20 results, followed by detail, analysis and related calls on each, is 61 requests. At 10 per minute that is six minutes of work. Fetch detail lazily when a user opens a result, not eagerly for every hit.
    • Never cache the PDF link. GET /cases/:id/pdf returns an encrypted presigned URL with expiresIn of 3600 seconds. It is per request and time limited. Store the case id and mint a fresh link on demand.
    • Use searchAfter for backfills. Cursor paging is what deep traversal is for. Offset paging over hundreds of thousands of results wastes both your rate budget and the cluster's time.
    • Separate interactive from batch. Different keys for your user facing path and your overnight backfill means the backfill cannot starve a lawyer waiting on a search box.

    Following an id: Detail, Analysis, Related, PDF

    GET /cases/:id accepts either the internal case id or a case number. Convenient, and worth understanding: a case number is a display identifier that repeats across courts and years, so prefer the internal id as your key wherever you have it. Text fields in the response are watermarked per API key owner, which exists so that leaked text can be traced back to the account it came from.

    GET /cases/:id/analysis returns the stored AI analysis, watermarked, with a hasAnalysis flag. When there is none, meta.note points you at the analyze endpoint. The analysis payload is substantial where it exists: summary, detailedSummary, comprehensiveSummary, headnote, holding, keyFacts, issues, courtsReasoning, citedCases, precedentRelationships, arguments, practiceAreas, precedentValue, legalPrinciples, doctrinesApplied, statutoryInterpretation, constitutionalProvisions, benchComposition, factPattern and more. It also carries parallel citation arrays such as summaryCitations and legalPrinciplesCitations, where entry i of the citation array corresponds to entry i of the content array. Keep those arrays aligned when you transform them, because splitting them apart destroys the grounding that makes the analysis checkable.

    GET /cases/:id/related finds every stored document sharing the same case number, sorted by decision date ascending and capped at 50. It returns relatedDocuments, each with id, title, caseNumber, court, decisionDate, caseType and an isCurrent flag, plus timeline entries carrying date, status of either Final Judgment or Hearings / Orders, a statusLabel that prefers the disposal nature where present, and documentId. Timeline entries are produced only for documents that have both a date and a stored PDF, so a sparse timeline is a statement about stored documents, not about the matter. Where the case has no case number at all, both arrays come back empty with an explanatory message.

    Two endpoints trigger work rather than reading it. POST /cases/:id/analyze with a body of force set to false returns 200 immediately with alreadyExists true if analysis is already stored, and otherwise returns 202 with a message that analysis has been started and a status of processing, running in the background. It returns 400 when the case has neither at least 100 characters of text nor any stored document to work from. POST /cases/:id/analyze-consolidated does the same for a whole family of documents sharing one case number, gathering up to 20 related records for a Supreme Court matter or the case plus its five most recent stored orders for a High Court matter, capping combined text at 100,000 characters. It requires a case number and consumes materially more credit. Both consume AI credits.

    Finally, the asynchronous pair. POST /request-timeline with a body carrying case_id starts a job that fetches orders from upstream and returns requestId and status, sometimes with cached true, an orderCount and the orders immediately where the work was already done. GET /get-timeline/:requestId is the poll target, returning status alongside createdAt and updatedAt and, as they appear, startedAt, completedAt, error, orders, orderCount and totalOrderCount. Poll it on a sensible interval, remembering that each poll spends one of your ten requests.

    The Semantic Endpoint Is a Different Animal

    POST /search/cases/semantic takes a query of at least three characters, the same filter fields as keyword search, and an optional filters object whose keys override anything the model extracted from your prose. It runs a pipeline: the query is normalised so that case numbers written as 123-2024 or 123/2024 collapse to 1232024 and are captured separately, a model extracts structured filters such as court, year, case type and case number out of the prose and returns a cleaned query, that query is expanded into legal terminology, an embedding is generated, and a vector search runs with a minimum similarity score of 0.3 and offset based paging. Full case documents are then hydrated from the primary store, and each result carries a score.

    Three things follow for your integration. First, if the cleaned query ends up shorter than three characters, meaning the caller supplied only filters, the request falls back to keyword search and sets meta.fallbackMode to opensearch. Check that flag before you tell your user they ran a semantic search. Second, the endpoint consumes AI credits and returns 403 when the allowance is exhausted, so it needs its own error path. Third, it holds the connection open with keep alive headers and allows up to 10 minutes, which means your HTTP client's default timeout of 30 seconds will kill perfectly healthy requests unless you raise it for this route specifically.

    Semantic search runs against the analysed corpus, which is in the low millions of documents. Keyword search runs against 310 million. Choosing between them is choosing between meaning and reach, and any integration worth building exposes both.

    The Fields That Are Frequently Not There

    This is where naive integrations break, and it is entirely predictable. A case record can carry id, caseNumber, title, court, courtName, caseType, judges, petitioners, respondents, decisionDate, disposalNature, summary, detailedSummary, headnote, holding, keyFacts, filingDate, registrationDate, caseStatus, caseStage, nextHearingDate, lastListedOn, cnr, caseHistory, acts, sections, hasOrders, orderCount, ordersFetchedAt, stateCode, ia_ma_history and courtMetadata carrying state, district and establishment names for District Court records.

    It can carry those. It very often does not carry all of them. Metadata completeness varies by court and by year, and the pattern is consistent: District Court records are thinner than High Court records, which are thinner than Supreme Court records, and older records everywhere are thinner than recent ones. This is not a data quality failure in the API. It is a faithful reflection of what the registry published. The registry is the authority; anything aggregated is a view of what registries made available.

    Summary, headnote and holding are frequently absent on District Court records, because much of that layer is AI derived and only a subset of the corpus is analysed
    decisionDate can be missing on pending matters and on older records where the registry never published a structured date
    judges arrays are sparser in District Court data than in High Court or Supreme Court data
    acts and sections may be empty even where the judgment plainly turns on section 138 of the Negotiable Instruments Act 1881
    nextHearingDate and caseStage are meaningful only while a matter is live, and stale the moment it is disposed
    hasOrders can be true while the timeline is short, because timeline entries require both a date and a stored PDF

    Model every field as optional

    Not most fields. Every field except the identifier you searched by. A parser that assumes decisionDate exists will run cleanly against Supreme Court results for a week and then fall over on a Tuesday when someone searches a District Court in a state whose registry publishes less. Give your schema nullable types everywhere, render absent fields as absent rather than as an empty string, and never let a missing summary become the string undefined in a document your user sends to a client.

    An Integration Shape That Holds Up

    Putting the constraints together produces a fairly specific architecture, and it is the same one most teams arrive at eventually after a painful month.

    One rate limited worker per key

    All outbound calls go through a queue. The worker enforces 10 per minute locally rather than discovering the limit from 429 responses, and honours retryAfter when it does get one. Your application code never calls the API directly.

    Store the raw payload

    Persist the full JSON response next to your parsed row. When you later discover a field you did not map, you reparse from your own store instead of spending rate budget refetching hundreds of thousands of records.

    Key on the internal id

    Case numbers repeat across courts and years and are a display artefact. Your primary key is the internal id. Index the case number and the CNR as secondary lookups, never as identity.

    Split interactive from batch

    A separate key for user facing traffic and for overnight ingestion, with separate alerting. Per key transaction logging means you can see exactly which workload burned the budget.

    One last habit that costs nothing and saves whole afternoons. Log the responseTime string from meta alongside your own measured latency for every call. When the two diverge, the problem is between you and the API, which is a completely different investigation from a slow query. Teams that do this find their proxy and DNS problems in minutes. Teams that do not spend a day blaming a search cluster that answered in 400 milliseconds.

    Beyond the raw HTTP interface, there is an MCP server if what you are building is an AI agent rather than a conventional application, and the endpoint reference with request and response shapes for all twelve endpoints sits at the API documentation. Credit consumption for the AI endpoints, which is the part that governs how freely you can call analyze and semantic search, is set out at API pricing.

    Start with the health check

    Twelve endpoints over roughly 310 million cases from the Supreme Court, all 25 High Courts, the district judiciary and tribunals, sourced from official government portals with no intermediary. Authentication is a single header, the response envelope is identical everywhere, and the rate limit is published rather than discovered. Read the endpoint reference, check credit consumption for the AI endpoints at API pricing, and see the MCP server page if you are wiring this into an agent. Overview and access on the API page.

    Explore CourtMesh
    APICourt DataDevelopersIntegrationIndia
    X LinkedIn