API reference

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
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.

GET

/companies/{cin}

Look up a single company by its Corporate Identification Number (CIN).

ParameterTypeDescription
cinpath, string21-character CIN. Case-insensitive.
cURL
curl "https://api.data91.in/companies/U45201AN2022PTC005613" \
  -H "X-API-Key: 91d_your_api_key"
Response · 200
{
  "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.

GET

/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.

ParameterTypeDescription
namequery, string (optional)Fuzzy match on company name (trigram similarity).
statequery, string (optional)Exact match on registered state.
statusquery, string (optional)Exact match on company status, e.g. "Active".
pin_codequery, string (optional)Exact match on 6-digit pin code.
company_categoryquery, string (optional)Exact match, e.g. "Company limited by shares".
industrial_classificationquery, string (optional)Exact match on industry/business activity type.
incorporation_yearquery, integer (optional)Exact match, decoded from the CIN.
registered_afterquery, date (optional)registration_date >= this date (inclusive), YYYY-MM-DD.
registered_beforequery, date (optional)registration_date <= this date (inclusive), YYYY-MM-DD.
limitquery, integer (optional)Results per page. 1–500, default 20.
cursorquery, string (optional)Opaque cursor (a CIN) from a previous response's next_cursor. Preferred for deep pagination — see below.
offsetquery, integer (optional)Simple pagination offset. Default 0. Gets slower the deeper you page — prefer cursor.
cURL
curl "https://api.data91.in/companies?name=inland&state=Andaman%20and%20Nicobar%20Islands&limit=10" \
  -H "X-API-Key: 91d_your_api_key"
Response · 200
{
  "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.

cURL
# 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"
GET

/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
curl "https://api.data91.in/companies/count?state=Maharashtra" \
  -H "X-API-Key: 91d_your_api_key"
Response · 200
{
  "count": 187342,
  "exact": true
}
GET

/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
curl "https://api.data91.in/companies/export?state=Maharashtra&status=Active" \
  -H "X-API-Key: 91d_your_api_key"
Response · 200 (application/x-ndjson)
{"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 array
Python (requests, streaming)
import 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.

FieldTypeDescription
cinstring21-character Corporate Identification Number. Primary identifier.
company_namestringRegistered company name.
registration_datedate | nullDate of incorporation, ISO 8601 (YYYY-MM-DD).
company_categorystring | nulle.g. "Company limited by shares".
company_classstring | nulle.g. "Private", "Public", "One Person Company".
listing_statusstring | null"Listed" or "Unlisted".
authorized_capitalstring | nullDecimal amount in INR, serialized as a string.
paidup_capitalstring | nullDecimal amount in INR, serialized as a string.
company_rocstring | nullRegistrar of Companies office, e.g. "ROC Delhi".
addressstring | nullRegistered office address, single line.
pin_codestring | null6-digit postal code. Null if the source value was invalid.
statestring | nullRegistered state.
statusstring | nulle.g. "Active", "Strike Off", "Under Liquidation".
sub_categorystring | nulle.g. "Non-government company".
industrial_classificationstring | nullIndustry/business activity type.
cin_nic_codestring | null5-digit NIC industry code, decoded from the CIN.
cin_incorporation_yearinteger | nullIncorporation year, decoded from the CIN.

Errors

StatusMeaning
401Missing, invalid, revoked, or suspended API key.
404No company found for the given CIN.
422Invalid query parameters (e.g. limit out of range).
502The 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

JavaScript (fetch)
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();
Python (requests)
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.

Get started