Monitoring | API Reference Guide | CourtMesh
    Skip to main content

    Monitoring

    Put CNRs on a watchlist and CourtMesh checks every one of them at least once a day. Changes arrive as events you can poll from a feed or receive by signed webhook. Daily monitoring is included free within your plan's cap.

    Watchlists, items and events
    A watchlist is a named set of CNRs. Items are identified by CNR.

    How it works

    • Every watched case is refreshed at least once per IST day. A case watched by several accounts is refreshed once and each account gets its own events.
    • After a refresh, the case is compared with its last snapshot: case status, case stage, next hearing date, last listed date, order count, latest order date and disposal nature. A difference becomes an event.
    • case.tracking_started carries the baseline snapshot. A CNR that was already in our record is watching at once, and the event is sent when its first fresh refresh completes (at once if the record is already current); that first refresh is folded into the baseline, so it adds no case.order_added events. A CNR we fetch live for you is fresh, so the event is sent as soon as the case lands.
    • Adding a CNR we do not hold starts a live fetch from eCourts and returns pending. It becomes watching when the case lands, or not_found if eCourts has no such case.
    • After that, a burst of new orders announces only the newest 20, oldest first, and an order dated more than 7 days before you started watching is never announced to you. Dates are YYYY-MM-DD on the Indian calendar, and a court's year 9999 "no date" is treated as no date.
    • A case taken down at a party's request is shown as an unavailable item and sends no events. Adding a CNR that is already taken down returns not_found.
    • Events are kept for 90 days. Delivery by webhook is at least once, so dedupe on the event id.

    Caps and pricing

    The watch cap is the number of distinct CNRs you watch across all your watchlists, so the same CNR on two lists uses one slot. An account can have up to 50 watchlists and 10 webhook endpoints. The Free tier can watch: the daily check is our fetch, not your live fetch quota. Two daily limits apply to adding, counted per Indian calendar day: every add that results in a watched or pending item counts toward a daily add allowance per account, and each CNR you add that we do not hold yet starts a live fetch, so those have their own, lower daily limit. Items over either come back as cap_exceeded in the per item results, with a message.

    PlanWatched CNRsCNRs added per dayOf which not yet held
    Free Developer105020
    Pay As You Go1,0005,000500
    Scale10,00050,0005,000
    EnterpriseFrom your contract, 100,000 by default500,000Unlimited
    • The daily check of every watched CNR: free.
    • Watchlist management, webhook management, webhook delivery and retries: free.
    • GET /changes and GET /watchlists/:id/events: 1 credit per page of up to 100 events. An empty page is free.
    POST

    /watchlists

    Free

    Creates a watchlist, optionally with up to 100 items. The response carries one result per item. A partial success is still HTTP 200, with caps counted after duplicates are removed. The request is refused with 403 WATCH_CAP_EXCEEDED only when your cap is already full and at least one new CNR was sent. This call honours the Idempotency-Key header.

    Request body

    FieldTypeRequiredNotes
    namestring (1 to 100)YesA label for the watchlist.
    itemsItem[] (max 100)NoCNRs to start watching in the same call.
    metadataobject (max 2 KB)NoAnything you want stored with the watchlist. Returned as you sent it.

    Item

    FieldTypeRequiredNotes
    cnrstringYesThe CNR in any common spelling. See CNR lookup for normalisation. A malformed CNR comes back as invalid for that item.
    referencestring (max 200)NoYour own id for this case. It is echoed back on every event, in watchlists[].reference.
    labelstring (max 200)NoA free text label shown in item listings.

    Per item result

    FieldTypeRequiredNotes
    watchingresultNoThe case is held and now monitored. A case.tracking_started event carries the baseline, sent as soon as its first fresh refresh completes (at once if our record is already current).
    pendingresultNoThe CNR is not in the corpus. A live fetch from eCourts started automatically. The item becomes watching when the case lands.
    not_foundresultNoeCourts has no such case, so a case.not_found event is emitted and the item stays with status not_found. A Supreme Court CNR that is not in the corpus cannot be fetched live and is not_found at once, with no event. If eCourts had nothing for the CNR in the last 24 hours it is not asked again: adding it is not_found with a message and no new fetch starts. A case taken down at a party's request is also not_found, and is stored as an unavailable item. A not_found, error or unavailable item still counts toward your cap until you remove it.
    invalidresultNoNot a valid CNR.
    duplicateresultNoAlready on this watchlist. Duplicates are removed before the cap is counted.
    cap_exceededresultNoAdding this CNR would pass your plan's watch cap, your daily add allowance, or the daily limit on adding CNRs we do not hold yet. The message says which.
    curl -X POST https://research.courtmesh.ai/api/v1/prod/watchlists \
      -H "X-API-Key: cm-YOUR_KEY_HERE" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Loan book, Delhi",
        "items": [
          { "cnr": "DLHC010084212021", "reference": "LOAN-42" },
          { "cnr": "MHAU010123452022", "reference": "LOAN-77", "label": "Aurangabad bench" }
        ],
        "metadata": { "team": "collections" }
      }'

    Manage watchlists

    GET

    /watchlists

    Free

    Cursor paginated. Each watchlist has an id with a wl_ prefix, name, itemCount, createdAt, updatedAt and metadata.

    GET

    /watchlists/:id

    Free

    One watchlist.

    PATCH

    /watchlists/:id

    Free

    Body { name?, metadata? }.

    DELETE

    /watchlists/:id

    Free

    Deletes the list and its items and returns { deleted: true }. Events already emitted remain readable in /changes.

    Items

    POST

    /watchlists/:id/items

    Free

    Body { items: Item[] }, 1 to 100 per call. Returns { results, itemCount }, a result per item as above. Honours Idempotency-Key.

    GET

    /watchlists/:id/items

    Free

    Cursor paginated, with an optional status filter (watching, pending, not_found, error or unavailable). Each item shows its latest snapshot and when it was last checked and last changed. An unavailable item is a case taken down at a party's request: caseId, court, title, snapshot, lastCheckedAt and lastChangedAt are all null.

    {
      "success": true,
      "data": [
        {
          "cnr": "DLHC010084212021",
          "reference": "LOAN-42",
          "label": null,
          "status": "watching",
          "caseId": "65f2a9c4b1d3e8a7c6540012",
          "court": "High Court of Delhi",
          "title": "Example Pvt Ltd v. State",
          "lastCheckedAt": "2026-11-02T03:10:00.000Z",
          "lastChangedAt": "2026-10-28T06:42:00.000Z",
          "snapshot": {
            "caseStatus": "Pending",
            "caseStage": "Arguments",
            "nextHearingDate": "2026-12-04",
            "lastListedOn": "2026-11-10",
            "orderCount": 7,
            "latestOrderDate": "2026-11-10",
            "disposalNature": null
          },
          "addedAt": "2026-11-02T09:00:00.000Z"
        }
      ],
      "pagination": { "nextCursor": "eyJ...", "hasMore": false }
    }
    DELETE

    /watchlists/:id/items/:cnr

    Free

    Stop watching one CNR on this watchlist. Returns { deleted: true }, or 404 ITEM_NOT_FOUND when the CNR is not on the list.

    GET

    /changes

    1 credit per page

    Every event across all your watchlists, oldest first. One credit per page of up to 100 events, and an empty page is free. pagination.nextCursor is always returned, even on an empty page, so a client can store it and poll forward indefinitely; it is opaque, so store it and send it back rather than building one, and since is converted to a feed position once. The same query and the same pricing apply to GET /watchlists/:id/events, filtered to one watchlist. An event appears in the feed about 2 seconds after it is created, so a client polling forward from a cursor never skips one. A types value outside the known list simply matches nothing.

    Query parameters

    FieldTypeRequiredNotes
    cursorstringNoOpaque. Store it and pass back pagination.nextCursor from the previous page; never build or edit one. Takes precedence over since.
    sinceISO timestampNoStart point when there is no cursor. It is converted to a feed position once; the cursor you get back continues from there.
    typescomma listNoOnly these event types, for example case.order_added,case.disposed.
    cnrstringNoOnly events for this CNR.
    limitinteger 1 to 100NoDefault 100. Events come oldest first, in the order they were recorded.
    # First poll: everything since a point in time
    curl "https://research.courtmesh.ai/api/v1/prod/changes?since=2026-11-01T00:00:00Z&types=case.hearing_date_changed,case.order_added" \
      -H "X-API-Key: cm-YOUR_KEY_HERE"
    
    # Every later poll: pass back the cursor you stored
    curl "https://research.courtmesh.ai/api/v1/prod/changes?cursor=eyJ..." \
      -H "X-API-Key: cm-YOUR_KEY_HERE"
    
    # One watchlist only
    curl "https://research.courtmesh.ai/api/v1/prod/watchlists/wl_8f3k2m/events" \
      -H "X-API-Key: cm-YOUR_KEY_HERE"

    A polling loop with the SDKs:

    import { CourtMeshClient } from '@courtmesh/sdk';
    
    const client = new CourtMeshClient({ apiKey: process.env.COURTMESH_API_KEY });
    
    let cursor = await loadCursor(); // from your own store, undefined on the first run
    for (;;) {
      const page = await client.listChanges({ cursor });
      for (const event of page.data) {
        await handle(event); // dedupe on event.id
      }
      cursor = page.pagination.nextCursor;
      await saveCursor(cursor);
      if (page.data.length < 100) break; // caught up
    }
    from courtmesh import CourtMesh
    
    client = CourtMesh(api_key="cm-YOUR_KEY_HERE")
    
    cursor = load_cursor()  # from your own store, None on the first run
    while True:
        page = client.list_changes(cursor=cursor)
        for event in page.data:
            handle(event)  # dedupe on event["id"]
        cursor = page.next_cursor
        save_cursor(cursor)
        if len(page.data) < 100:
            break  # caught up

    Event types

    The feed and the webhook body are the same event object: id (with an evt_ prefix), type, createdAt, cnr, caseId, watchlists (the id and your reference for each of your watchlists that include the CNR) and data.

    TypeEmitted when
    case.tracking_startedOnce per case you watch, after its first fresh refresh completes. The baseline snapshot, never a burst of old orders.
    case.order_addedA new order appears on the case. One event per order.
    case.hearing_date_changedThe next hearing date moves.
    case.status_changedThe case status changes, for example Pending to Disposed.
    case.stage_changedThe case stage changes.
    case.disposedThe case is disposed. Carries the nature of disposal and the decision date.
    case.not_foundA live fetch found no such case at eCourts.
    webhook.testOnly from the webhook test endpoint.

    The data payload for each type, with the other event fields omitted:

    // case.tracking_started: the baseline, sent once after the first fresh refresh of the case
    { "type": "case.tracking_started", "cnr": "DLHC010084212021",
      "data": { "snapshot": { "caseStatus": "Pending", "caseStage": "Arguments", "nextHearingDate": "2026-11-10", "orderCount": 6 } } }
    
    // case.order_added: one event per new order
    { "type": "case.order_added", "cnr": "DLHC010084212021",
      "data": {
        "order": { "orderNumber": "7", "orderDate": "2026-11-10", "orderType": "Order", "pdfAvailable": true },
        "snapshot": { "caseStatus": "Pending", "nextHearingDate": "2026-12-04", "orderCount": 7 } } }
    
    // case.hearing_date_changed
    { "type": "case.hearing_date_changed", "cnr": "DLHC010084212021",
      "data": { "previous": "2026-11-10", "current": "2026-12-04", "snapshot": { "nextHearingDate": "2026-12-04" } } }
    
    // case.status_changed
    { "type": "case.status_changed", "cnr": "DLHC010084212021",
      "data": { "previous": "Pending", "current": "Disposed", "snapshot": { "caseStatus": "Disposed" } } }
    
    // case.stage_changed
    { "type": "case.stage_changed", "cnr": "DLHC010084212021",
      "data": { "previous": "Arguments", "current": "Judgment reserved", "snapshot": { "caseStage": "Judgment reserved" } } }
    
    // case.disposed
    { "type": "case.disposed", "cnr": "DLHC010084212021",
      "data": { "disposalNature": "Allowed", "decisionDate": "2026-12-04", "snapshot": { "caseStatus": "Disposed" } } }
    
    // case.not_found: a live fetch found no such case at eCourts
    { "type": "case.not_found", "cnr": "DLHC010084219999", "data": { "reason": "No such case at eCourts" } }

    Errors and scopes

    CodeHTTPMeaning
    INVALID_CNR400The CNR is not valid after normalisation.
    WATCHLIST_NOT_FOUND404No such watchlist on this account.
    WATCH_CAP_EXCEEDED403Your plan's cap is already full and the request sent at least one new CNR. Nothing is created. Otherwise, over-cap items are reported as cap_exceeded per item.
    ITEM_NOT_FOUND404Removing a CNR that is not on the watchlist.
    VALIDATION_ERROR400A body or query parameter failed validation. details is an array of "path: message" strings, the same shape for query and body.
    WATCHLIST_LIMIT_REACHED403The account already has 50 watchlists.

    API key scopes: monitoring:read covers watchlists, items, events, the changes feed, webhooks and deliveries, and monitoring:write covers everything that changes them. A key with no explicit scopes keeps full access. See Errors for the shared response envelope.