Introduction
The 91data API serves cleaned, unified Indian business data — company records sourced from MCA and government registries. It's a read-only REST API: two endpoints, JSON in and out, authenticated with an API key.
Base URL: https://api.data91.in
Authentication
Every request must include your API key in the X-API-Key header. Requests without a valid key return 401 Unauthorized.
Get a key from your dashboard — Owners and Developers can issue and revoke keys.
curl "https://api.data91.in/companies/U45201AN2022PTC005613" \
-H "X-API-Key: 91d_your_api_key"Rate limits
Your plan includes a daily call quota. Going over it does not block requests — calls beyond your quota are still served, but flagged for overage billing. When a response is served over-quota, it carries an X-Quota-Status: over-quota response header. See your Billing tab for your current plan and overage rate.
Quota is counted per HTTP request, not per row — a single /companies/export call streaming a million matching rows counts once against your quota, the same as any other call.
/companies/{cin}
Look up a single company by its Corporate Identification Number (CIN).
| Parameter | Type | Description |
|---|---|---|
| cin | path, string | 21-character CIN. Case-insensitive. |
curl "https://api.data91.in/companies/U45201AN2022PTC005613" \
-H "X-API-Key: 91d_your_api_key"{
"cin": "U45201AN2022PTC005613",
"company_name": "INLAND ACE CONSTRUCTION PRIVATE LIMITED",
"registration_date": "2022-08-25",
"company_category": "Company limited by shares",
"company_class": "Private",
"listing_status": "Unlisted",
"authorized_capital": "1500000.00",
"paidup_capital": "1500000.00",
"company_roc": "ROC Andaman",
"address": "No 5, Ward No 7, Madhupur Diglipur,Andaman,Andaman Islands,Andaman and Nicobar Islands,744202-India",
"pin_code": "744202",
"state": "Andaman and Nicobar Islands",
"status": "Active",
"sub_category": "Non-government company",
"industrial_classification": "Construction",
"cin_nic_code": "45201",
"cin_incorporation_year": 2022
}Returns 404 if no company matches the CIN.
/companies
Search companies by name (fuzzy match) and/or exact-match filters, with pagination. The response is wrapped in a results envelope alongside pagination metadata — see Pagination at scale below for how to page through large result sets efficiently.
| Parameter | Type | Description |
|---|---|---|
| name | query, string (optional) | Fuzzy match on company name (trigram similarity). |
| state | query, string (optional) | Exact match on registered state. |
| status | query, string (optional) | Exact match on company status, e.g. "Active". |
| pin_code | query, string (optional) | Exact match on 6-digit pin code. |
| company_category | query, string (optional) | Exact match, e.g. "Company limited by shares". |
| industrial_classification | query, string (optional) | Exact match on industry/business activity type. |
| incorporation_year | query, integer (optional) | Exact match, decoded from the CIN. |
| registered_after | query, date (optional) | registration_date >= this date (inclusive), YYYY-MM-DD. |
| registered_before | query, date (optional) | registration_date <= this date (inclusive), YYYY-MM-DD. |
| limit | query, integer (optional) | Results per page. 1–500, default 20. |
| cursor | query, string (optional) | Opaque cursor (a CIN) from a previous response's next_cursor. Preferred for deep pagination — see below. |
| offset | query, integer (optional) | Simple pagination offset. Default 0. Gets slower the deeper you page — prefer cursor. |
curl "https://api.data91.in/companies?name=inland&state=Andaman%20and%20Nicobar%20Islands&limit=10" \
-H "X-API-Key: 91d_your_api_key"{
"results": [
{
"cin": "U45201AN2022PTC005613",
"company_name": "INLAND ACE CONSTRUCTION PRIVATE LIMITED",
"state": "Andaman and Nicobar Islands",
"status": "Active"
/* ...remaining fields, same shape as GET /companies/{cin} */
}
],
"next_cursor": "U45201AN2022PTC005613",
"has_more": true
}Pagination at scale
The dataset behind this API is several million rows. offset-based paging works fine for shallow pages or narrow filtered results, but it gets slower the deeper you page — Postgres has to scan and discard every row before the offset on each request. Paging to row 3,000,000 via offset=3000000 means scanning 3 million rows just to skip them.
cursor-based pagination avoids this entirely — it's a direct index seek, so page 50,000 is exactly as fast as page 1, regardless of how deep you go. Take the next_cursor from a response and pass it back as cursor on the next request; stop when has_more is false. Cursor pagination is only available when nameisn't set — name search is ordered by relevance, not CIN, so there's no stable cursor to hand back; use offset there instead.
For pulling large slices of the dataset rather than browsing a few pages, see Bulk export below — it's built for that specifically and is more efficient than paging through thousands of /companies calls.
# First page
curl "https://api.data91.in/companies?state=Maharashtra&limit=100" \
-H "X-API-Key: 91d_your_api_key"
# Next page — use the previous response's "next_cursor"
curl "https://api.data91.in/companies?state=Maharashtra&limit=100&cursor=U74899MH2021PTC123456" \
-H "X-API-Key: 91d_your_api_key"/companies/count
Returns the number of companies matching a filter, without fetching the records themselves — same filter parameters as /companies (no limit/cursor/offset). An unfiltered call returns an instant estimate (Postgres's own table statistics) rather than scanning several million rows to count them — check the exact field in the response to tell which you got. Filtered calls run a real, exact count.
curl "https://api.data91.in/companies/count?state=Maharashtra" \
-H "X-API-Key: 91d_your_api_key"{
"count": 187342,
"exact": true
}/companies/export
Streams matching companies as newline-delimited JSON (NDJSON) — one compact JSON object per line, not wrapped in an array. Built for bulk/large-scale consumption: syncing a filtered slice of the dataset into your own system, rather than paging through it a few hundred rows at a time. Same filter parameters as /companies, plus cursor to resume a large export across multiple requests (results are always ordered by CIN — pass the last CIN you received as cursor to continue where you left off, e.g. after a dropped connection).
Process the response as a stream rather than buffering the whole body — see the Python example below. A single export call is capped at 2,000,000 rows; for anything larger, resume with cursor across multiple calls.
curl "https://api.data91.in/companies/export?state=Maharashtra&status=Active" \
-H "X-API-Key: 91d_your_api_key"{"cin": "U74899MH2021PTC123456", "company_name": "ACME PRIVATE LIMITED", "state": "Maharashtra", "status": "Active", ...}
{"cin": "U74899MH2021PTC123457", "company_name": "ACME TRADING LLP", "state": "Maharashtra", "status": "Active", ...}
{"cin": "U74899MH2021PTC123458", "company_name": "ACME LOGISTICS PVT LTD", "state": "Maharashtra", "status": "Active", ...}
...one JSON object per line, not wrapped in an arrayimport requests, json
url = "https://api.data91.in/companies/export"
params = {"state": "Maharashtra", "status": "Active"}
headers = {"X-API-Key": "91d_your_api_key"}
with requests.get(url, params=params, headers=headers, stream=True) as res:
res.raise_for_status()
for line in res.iter_lines():
if not line:
continue
company = json.loads(line)
# process one company at a time — memory usage stays flat
# regardless of how many millions of rows match
print(company["cin"], company["company_name"])Response fields
/companies/{cin}, /companies (inside results), and /companies/export (each NDJSON line) all return the same company object shape.
| Field | Type | Description |
|---|---|---|
| cin | string | 21-character Corporate Identification Number. Primary identifier. |
| company_name | string | Registered company name. |
| registration_date | date | null | Date of incorporation, ISO 8601 (YYYY-MM-DD). |
| company_category | string | null | e.g. "Company limited by shares". |
| company_class | string | null | e.g. "Private", "Public", "One Person Company". |
| listing_status | string | null | "Listed" or "Unlisted". |
| authorized_capital | string | null | Decimal amount in INR, serialized as a string. |
| paidup_capital | string | null | Decimal amount in INR, serialized as a string. |
| company_roc | string | null | Registrar of Companies office, e.g. "ROC Delhi". |
| address | string | null | Registered office address, single line. |
| pin_code | string | null | 6-digit postal code. Null if the source value was invalid. |
| state | string | null | Registered state. |
| status | string | null | e.g. "Active", "Strike Off", "Under Liquidation". |
| sub_category | string | null | e.g. "Non-government company". |
| industrial_classification | string | null | Industry/business activity type. |
| cin_nic_code | string | null | 5-digit NIC industry code, decoded from the CIN. |
| cin_incorporation_year | integer | null | Incorporation year, decoded from the CIN. |
Errors
| Status | Meaning |
|---|---|
| 401 | Missing, invalid, revoked, or suspended API key. |
| 404 | No company found for the given CIN. |
| 422 | Invalid query parameters (e.g. limit out of range). |
| 502 | The upstream data service is unreachable — retry shortly. |
For /companies/export, the status code and headers are only meaningful for the initial connection — once streaming starts, a failure partway through ends the connection rather than returning a clean error body. If a stream cuts off unexpectedly, resume with cursor set to the last CIN you successfully received.
More examples
const res = await fetch("https://api.data91.in/companies/U45201AN2022PTC005613", {
headers: { "X-API-Key": "91d_your_api_key" }
});
if (!res.ok) throw new Error(`91data API error: ${res.status}`);
const company = await res.json();import requests
res = requests.get(
"https://api.data91.in/companies/U45201AN2022PTC005613",
headers={"X-API-Key": "91d_your_api_key"},
)
res.raise_for_status()
company = res.json()Ready to integrate?
Create an account and issue your first API key.