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.
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.pending. It becomes watching when the case lands, or not_found if eCourts has no such case.YYYY-MM-DD on the Indian calendar, and a court's year 9999 "no date" is treated as no date.unavailable item and sends no events. Adding a CNR that is already taken down returns not_found.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.
| Plan | Watched CNRs | CNRs added per day | Of which not yet held |
|---|---|---|---|
| Free Developer | 10 | 50 | 20 |
| Pay As You Go | 1,000 | 5,000 | 500 |
| Scale | 10,000 | 50,000 | 5,000 |
| Enterprise | From your contract, 100,000 by default | 500,000 | Unlimited |
GET /changes and GET /watchlists/:id/events: 1 credit per page of up to 100 events. An empty page is 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
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string (1 to 100) | Yes | A label for the watchlist. |
| items | Item[] (max 100) | No | CNRs to start watching in the same call. |
| metadata | object (max 2 KB) | No | Anything you want stored with the watchlist. Returned as you sent it. |
Item
| Field | Type | Required | Notes |
|---|---|---|---|
| cnr | string | Yes | The CNR in any common spelling. See CNR lookup for normalisation. A malformed CNR comes back as invalid for that item. |
| reference | string (max 200) | No | Your own id for this case. It is echoed back on every event, in watchlists[].reference. |
| label | string (max 200) | No | A free text label shown in item listings. |
Per item result
| Field | Type | Required | Notes |
|---|---|---|---|
| watching | result | No | The 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). |
| pending | result | No | The CNR is not in the corpus. A live fetch from eCourts started automatically. The item becomes watching when the case lands. |
| not_found | result | No | eCourts 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. |
| invalid | result | No | Not a valid CNR. |
| duplicate | result | No | Already on this watchlist. Duplicates are removed before the cap is counted. |
| cap_exceeded | result | No | Adding 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" }
}'Cursor paginated. Each watchlist has an id with a wl_ prefix, name, itemCount, createdAt, updatedAt and metadata.
One watchlist.
Body { name?, metadata? }.
Deletes the list and its items and returns { deleted: true }. Events already emitted remain readable in /changes.
Body { items: Item[] }, 1 to 100 per call. Returns { results, itemCount }, a result per item as above. Honours Idempotency-Key.
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 }
}Stop watching one CNR on this watchlist. Returns { deleted: true }, or 404 ITEM_NOT_FOUND when the CNR is not on the list.
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | No | Opaque. Store it and pass back pagination.nextCursor from the previous page; never build or edit one. Takes precedence over since. |
| since | ISO timestamp | No | Start point when there is no cursor. It is converted to a feed position once; the cursor you get back continues from there. |
| types | comma list | No | Only these event types, for example case.order_added,case.disposed. |
| cnr | string | No | Only events for this CNR. |
| limit | integer 1 to 100 | No | Default 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 upThe 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.
| Type | Emitted when |
|---|---|
| case.tracking_started | Once per case you watch, after its first fresh refresh completes. The baseline snapshot, never a burst of old orders. |
| case.order_added | A new order appears on the case. One event per order. |
| case.hearing_date_changed | The next hearing date moves. |
| case.status_changed | The case status changes, for example Pending to Disposed. |
| case.stage_changed | The case stage changes. |
| case.disposed | The case is disposed. Carries the nature of disposal and the decision date. |
| case.not_found | A live fetch found no such case at eCourts. |
| webhook.test | Only 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" } }| Code | HTTP | Meaning |
|---|---|---|
| INVALID_CNR | 400 | The CNR is not valid after normalisation. |
| WATCHLIST_NOT_FOUND | 404 | No such watchlist on this account. |
| WATCH_CAP_EXCEEDED | 403 | Your 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_FOUND | 404 | Removing a CNR that is not on the watchlist. |
| VALIDATION_ERROR | 400 | A body or query parameter failed validation. details is an array of "path: message" strings, the same shape for query and body. |
| WATCHLIST_LIMIT_REACHED | 403 | The 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.