# PincodesInfo API Documentation

A free RESTful API for Indian postal data — PIN codes, post office names, districts, states, delivery status and geographic coordinates.

**Coverage** (live figures from `/api/stats.php`):

| | |
|---|---|
| Post office records | 165,609 |
| Distinct PIN codes | 19,591 |
| States & UTs | 36 |
| Districts | 753 |

---

## 🔗 Base URL

```
https://www.pincodesinfo.in/api/
```

> **Use the `www.` host.** `https://pincodesinfo.in/...` answers `301` and redirects to `www`, so every call to the apex costs you an extra round trip.

---

## 📋 Endpoints at a glance

| Endpoint | Returns | Paginated |
|---|---|---|
| `GET /api/details.php?pincode={pin}` | one PIN, **all** its offices | no |
| `GET /api/pincode/{pin}` | one PIN, offices as search results | **yes** |
| `GET /api/search.php?q={query}` | search by name / district / state | **yes** |
| `GET /api/state/{state-slug}` | districts in a state, with counts | no |
| `GET /api/district/{district-slug}` | offices in a district | **yes** |
| `GET /api/nearby.php?lat={lat}&lng={lng}` | 5 nearest offices | no |
| `GET /api/stats.php` | dataset totals | no |

All responses are `application/json; charset=utf-8` and carry a top-level `success` boolean.

---

## 1. PIN code details — `details.php` (recommended for a single PIN)

```
GET /api/details.php?pincode={pincode}
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `pincode` | string | yes | 6-digit PIN code |

**Prefer this over `/api/pincode/{pin}`.** A PIN code frequently maps to more than one post office — `110001` has **23** — and this endpoint returns all of them in a single response with no paging.

```bash
curl "https://www.pincodesinfo.in/api/details.php?pincode=110001"
```

```json
{
  "success": true,
  "pincode_info": {
    "pincode": "110001",
    "district": "NEW DELHI",
    "state": "DELHI",
    "division": "New Delhi Central Division",
    "region": "DivReportingCircle",
    "circle": "Delhi Circle"
  },
  "total_offices": 23,
  "offices": [
    {
      "office_name": "Baroda House SO",
      "office_type": "PO",
      "delivery_status": "Non Delivery",
      "taluk": null,
      "latitude": "28.61741670",
      "longitude": "77.21291670"
    }
  ]
}
```

---

## 2. PIN code as search — `/api/pincode/{pincode}`

```
GET /api/pincode/{pincode}
```

Internally this is the search endpoint with the PIN as the query, so it returns the **paginated search shape**, not the grouped shape above.

```bash
curl "https://www.pincodesinfo.in/api/pincode/110001"
```

```json
{
  "success": true,
  "query": "110001",
  "total": 23,
  "page": 1,
  "limit": 20,
  "total_pages": 2,
  "results": [
    {
      "id": 29478,
      "pincode": "110001",
      "office_name": "Baroda House SO",
      "office_type": "PO",
      "delivery_status": "Non Delivery",
      "division": "New Delhi Central Division",
      "region": "DivReportingCircle",
      "circle": "Delhi Circle",
      "taluk": null,
      "district": "NEW DELHI",
      "state": "DELHI",
      "latitude": "28.61741670",
      "longitude": "77.21291670",
      "created_at": "2026-03-24 11:32:45"
    }
  ]
}
```

> ⚠️ **Check `total_pages`.** The page size is 20, so a PIN with more than 20 offices — `110001` among them — is split across pages. Reading only page 1 silently drops offices. Either follow `page=2…` or use `details.php`.

---

## 3. Search — `search.php`

```
GET /api/search.php?q={query}&page={n}&state={state}
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `q` | string | yes¹ | office name, district, state, or partial PIN |
| `state` | string | yes¹ | filter by state |
| `page` | integer | no | page number, default `1` |

¹ At least one of `q` or `state` must be present, otherwise the response is `success: false`.

```bash
curl "https://www.pincodesinfo.in/api/search.php?q=Connaught%20Place"
```

Returns the same paginated shape as section 2.

> **There is no `/api/search` route.** Earlier versions of this document showed `/api/search?q=…`; that path returns **404**. The `.php` extension is required. Only `/api/pincode/…`, `/api/state/…` and `/api/district/…` have extensionless rewrites.

---

## 4. State — `/api/state/{state-slug}`

```
GET /api/state/{state-slug}
```

Slug is lowercase with hyphens: `maharashtra`, `tamil-nadu`, `delhi`.

```bash
curl "https://www.pincodesinfo.in/api/state/maharashtra"
```

```json
{
  "success": true,
  "state": "maharashtra",
  "total_districts": 36,
  "districts": [
    { "name": "AHMEDNAGAR", "pincode_count": 88, "office_count": 669 }
  ]
}
```

> This response has **its own shape** — `state` / `total_districts` / `districts`, with no `results` array and no pagination fields. Do not assume one schema across endpoints.
>
> `pincode_count` counts **distinct PIN codes**; `office_count` counts **post office records**. They are different numbers and are not interchangeable.

---

## 5. District — `/api/district/{district-slug}`

```
GET /api/district/{district-slug}
```

```bash
curl "https://www.pincodesinfo.in/api/district/mumbai"
```

Runs the district name through search, so the response is the paginated search shape from section 2.

---

## 6. Nearby offices — `nearby.php`

```
GET /api/nearby.php?lat={latitude}&lng={longitude}
```

| Parameter | Type | Required |
|---|---|---|
| `lat` | float | yes |
| `lng` | float | yes |

Takes **coordinates, not a PIN code** — `?pincode=` returns `"Latitude and Longitude are required"`. Returns the 5 nearest offices, each with an extra `distance` field.

```bash
curl "https://www.pincodesinfo.in/api/nearby.php?lat=28.6173&lng=77.2129"
```

```json
{
  "success": true,
  "count": 5,
  "results": [
    { "pincode": "110001", "office_name": "…", "distance": 0.12, "…": "…" }
  ]
}
```

---

## 7. Dataset statistics — `stats.php`

```bash
curl "https://www.pincodesinfo.in/api/stats.php"
```

```json
{
  "success": true,
  "stats": {
    "pincodes": 19591,
    "post_offices": 165609,
    "states": 36,
    "districts": 753,
    "generated": "2026-09-11T08:16:42+05:30"
  }
}
```

---

## 📑 Field reference

Fields in a search `results[]` entry:

| Field | Type | Notes |
|---|---|---|
| `id` | integer | internal row id; **not stable across data refreshes** |
| `pincode` | string | 6 digits, leading zeros preserved — keep it a string |
| `office_name` | string | |
| `office_type` | string | `HO`, `BO`, `PO`, `SO` — see note below |
| `delivery_status` | string | `Delivery` or `Non Delivery` |
| `division` / `region` / `circle` | string | India Post administrative hierarchy |
| `taluk` | null | **always `null`** — see note below |
| `district` / `state` | string | uppercase, as published by India Post |
| `latitude` / `longitude` | string | decimal degrees as strings; may be absent for some offices |
| `created_at` | string | when the row entered this database, **not** when the post office opened |

### Notes on the data

- **`taluk` is `null` for every record.** The source extract does not populate it. It is kept in the response for backward compatibility; do not build on it.
- **`office_type`**: the dataset uses `PO` where some India Post exports use `SO`. Filter on the values you actually observe rather than an assumed set.
- **Coordinates**: supplied by India Post and not independently surveyed. A small share are known to be wrong; treat them as approximate and validate before using for routing or distance-critical work.
- **`created_at` is a database timestamp**, not postal history. It changes when the dataset is reloaded.

---

## 🔒 Rate limits

Enforced by nginx, per IP address, ahead of the application:

| | |
|---|---|
| Sustained rate | **30 requests / minute** |
| Burst | ~20 extra, so roughly **36 back-to-back** requests before throttling |
| Response when exceeded | `429 Too Many Requests` |

Measured: 45 rapid requests → the first 36 returned `200`, requests 37 onward were rejected.

> ⚠️ **The 429 body is HTML, not JSON.**
>
> ```html
> <html><head><title>429 Too Many Requests</title></head>…
> ```
>
> A client that calls `json.loads()` on every response will raise a parse error here. **Check the status code before parsing the body.**

### Rate-limit headers

Successful responses include:

```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1789121562
```

> These come from the application layer and **do not describe the limit that actually stops you**. nginx throttles at ~30/min regardless of what `X-RateLimit-Remaining` reports. Treat 30/min as the real budget and use these headers only as a rough signal.

### Staying within the limits

- Cache results. Postal data changes rarely — a PIN's offices are stable for months.
- For bulk work, space requests ~2 seconds apart rather than bursting.
- Back off on `429` instead of retrying immediately.

---

## 🌐 CORS

```
Access-Control-Allow-Origin: https://www.pincodesinfo.in
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Requested-With
```

The allow-origin header is **always** `https://www.pincodesinfo.in`, whatever `Origin` you send. Verified: a request carrying `Origin: https://example.com` still gets `https://www.pincodesinfo.in` back.

**Consequence:** browser JavaScript on any other domain **cannot** read these responses — the browser blocks them. Call the API **from your server**, then serve the result to your own front end. Server-side clients (curl, axios, requests, Guzzle) are unaffected, since CORS is a browser rule.

---

## ⚠️ Errors

Application-level errors return JSON:

```json
{
  "success": false,
  "error": "Search query or State filter is required",
  "results": []
}
```

| HTTP | Source | Body | Meaning |
|---|---|---|---|
| `200` | app | JSON, `success: false` | request understood, nothing to return, or a bad parameter |
| `301` | nginx | — | you called the apex host; use `www.` |
| `404` | nginx | HTML | no such route — check for a missing `.php` |
| `429` | nginx | **HTML** | rate limit exceeded |
| `500` | app | JSON | server-side failure |

Note that a failed lookup can still arrive as `200` with `success: false`. **Check `success`, not just the status code.**

---

## 🚀 Quick start

**Node.js**

```javascript
const axios = require('axios');

const api = axios.create({
  baseURL: 'https://www.pincodesinfo.in/api',
  timeout: 10000,
});

// All offices for one PIN, unpaginated.
async function getPincode(pin) {
  const { data } = await api.get('/details.php', { params: { pincode: pin } });
  if (!data.success) throw new Error(data.error);
  return data;
}

getPincode('110001').then(d =>
  console.log(`${d.pincode_info.district}: ${d.total_offices} offices`)
);
```

**Python**

```python
import requests

BASE = "https://www.pincodesinfo.in/api"

def get_pincode(pin: str) -> dict:
    r = requests.get(f"{BASE}/details.php", params={"pincode": pin}, timeout=10)
    if r.status_code == 429:
        raise RuntimeError("rate limited — back off")   # body is HTML, not JSON
    r.raise_for_status()
    data = r.json()
    if not data.get("success"):
        raise ValueError(data.get("error"))
    return data

d = get_pincode("110001")
print(d["pincode_info"]["district"], d["total_offices"])
for o in d["offices"]:
    print(" ", o["office_name"], o["office_type"])
```

**PHP**

```php
<?php
$pin = '110001';
$url = "https://www.pincodesinfo.in/api/details.php?pincode=" . urlencode($pin);

$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
$body = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($code === 429) {
    throw new RuntimeException('Rate limited — back off');  // HTML body
}

$data = json_decode($body, true);
if (!($data['success'] ?? false)) {
    throw new RuntimeException($data['error'] ?? 'Request failed');
}

foreach ($data['offices'] as $office) {
    echo $office['office_name'], ' — ', $office['office_type'], PHP_EOL;
}
```

**Bulk lookups** — stay under 30/min:

```python
import time, requests

for pin in pincodes:
    try:
        data = get_pincode(pin)
    except RuntimeError:
        time.sleep(60)          # 429 — wait out the window
        data = get_pincode(pin)
    handle(data)
    time.sleep(2)               # ~30 requests per minute
```

---

## 💡 Working with this API

1. **Treat `pincode` as a string.** `007` is a valid prefix; integer parsing destroys leading zeros.
2. **Use `details.php` for a single PIN.** One call, every office, no paging to get wrong.
3. **Read `total_pages` whenever you use a paginated endpoint**, or you will silently lose records.
4. **Check `success` before reading `results`.** A `200` does not guarantee data.
5. **Check the status code before `json_decode`.** The 429 body is HTML.
6. **Cache aggressively.** Postal records change on a scale of months.
7. **Call from your server, not the browser** — CORS allows only this site's own origin.

---

## 📞 Support

- Website: <https://www.pincodesinfo.in>
- Contact form: <https://www.pincodesinfo.in/contact.php>
- Security reports: [`/.well-known/security.txt`](https://www.pincodesinfo.in/.well-known/security.txt)

---

## 📄 Terms

Free for personal and commercial use, offered as-is with **no uptime or accuracy guarantee**. Source data is published by India Post; verify anything you rely on for deliveries, payments or compliance.

- [Terms of Service](https://www.pincodesinfo.in/terms-of-service.php)
- [Privacy Policy](https://www.pincodesinfo.in/privacy-policy.php)

---

## 🔄 Changelog

### 2026-09-11 — documentation corrected against the live API

Every endpoint, limit and response in this document was re-tested against production. Corrections:

- **Base URL** is now `www.` — the apex `301`s, so old examples cost an extra redirect on every call.
- **Removed `/api/search?q=`** — that route returns **404**. The working path is `/api/search.php?q=`.
- **Rate limit** was documented as 300/min. The limit actually enforced is **30/min** (nginx, ~36 burst), and the `429` body is **HTML, not JSON** — the previously documented JSON error body is never sent for rate limiting.
- **`X-RateLimit-Limit`** reports `100`, not the documented `300`, and describes neither the enforced limit; noted as unreliable.
- **Removed the "common response format" claim.** `/api/state/` returns `state` / `total_districts` / `districts` and has no `results` array.
- **`taluk` documented as always `null`** — confirmed empty for all 165,609 records.
- **Corrected the `/api/pincode/110001` example**: `total` is 23 across 2 pages, not 15 on one. Added a warning that a single PIN can span pages.
- **CORS clarified**: allow-origin is always this site's own origin, so cross-domain browser calls are blocked. Call server-side.
- **Documented `details.php`, `nearby.php` and `stats.php`**, which existed but were absent from this document. `nearby.php` takes `lat`/`lng`, not `pincode`.
- Added dataset scale, `office_type` / coordinate accuracy / `created_at` caveats.

### 1.0 — December 2025

Initial release: PIN, name, district and state search; pagination; coordinates; CORS.
