> ## Documentation Index
> Fetch the complete documentation index at: https://docs.countrystatecity.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors & Rate Limits

> The two error shapes the API returns, the 403 plan gate, the 429 rate limit, and the usage headers

The Country State City API returns errors in two shapes. Which one you get depends on the endpoint and the kind of failure, so write your error handling to read both.

## The two error shapes

**Structured envelope** — used by plan-gate `403` responses, `429` rate limits, and the `?fields=`/`?sort=` validation errors:

```json theme={null}
{
  "status": "error",
  "message": "<human-readable message>",
  "details": { ... }
}
```

`details` is optional. It's present when the error carries structured metadata, like the `403` and `429` responses below. Other errors in this shape return just `status` and `message`.

**Bare error object** — used by a small set of geographical `404` responses: country details, state details, region details, subregion details, and currency by country:

```json theme={null}
{
  "error": "<human-readable message>"
}
```

<Warning>
  There is no single envelope that covers every failure. Read the message from `message` **or** `error`, and treat `details` as optional.
</Warning>

```javascript Reading either shape theme={null}
// Normalises both error shapes into one object.
async function readError(response) {
  const body = await response.json().catch(() => ({}));

  return {
    status: response.status,
    message: body.message ?? body.error ?? 'Unknown error',
    details: body.details ?? null
  };
}
```

<Tip>
  Each endpoint page lists the exact error bodies that endpoint returns. When you need the precise wording for one endpoint, check its page — for example [Get Currency by Country](/api/endpoints/get-currency-by-country) or [Parse Phone Number](/api/endpoints/parse-phone-number).
</Tip>

## HTTP status codes

| Code | Meaning                                                              |
| ---- | -------------------------------------------------------------------- |
| 400  | Bad Request — malformed or invalid query parameter                   |
| 401  | Unauthorized — missing or invalid API key                            |
| 403  | Forbidden — authenticated, but the request isn't allowed (see below) |
| 404  | Not Found — the requested resource doesn't exist (see below)         |
| 409  | Conflict — the resource already exists                               |
| 429  | Too Many Requests — usage limit exceeded (see below)                 |
| 503  | Service Unavailable — temporary infrastructure issue, safe to retry  |

<Note>
  A `401` is rejected by the authentication layer before any plan or usage check runs. See [Authentication](/api/authentication) for its response body and the usual causes.
</Note>

## 403 — Forbidden

A `403` means your API key authenticated fine, but the request isn't allowed. Most `403` responses are **plan gates**, and those carry a `details` object.

### Plan-gate 403

Returned when the endpoint, query parameter, or feature you're using isn't included in your plan — for example, calling [Fuzzy Search](/api/endpoints/fuzzy-search) on a Community plan:

```json 403 - Feature not available theme={null}
{
  "status": "error",
  "message": "This feature is not available on your current plan.",
  "details": {
    "feature": "fuzzySearch",
    "currentTier": "community",
    "requiredTier": "professional",
    "upgradeUrl": "https://app.countrystatecity.in/pricing"
  }
}
```

<ResponseField name="details.feature" type="string">
  Internal name of the gated feature (e.g. `fuzzySearch`, `searchEndpoint`, `fieldsFiltering`, `sortParameter`, `regionsApi`, `currencyApi`).
</ResponseField>

<ResponseField name="details.currentTier" type="string">
  Your plan's tier at the time of the request.
</ResponseField>

<ResponseField name="details.requiredTier" type="string | null">
  The minimum tier that unlocks this feature, or `null`. See **Usage limits by tier** below for what each tier includes.
</ResponseField>

<ResponseField name="details.upgradeUrl" type="string">
  Direct link to upgrade your plan.
</ResponseField>

<Note>
  `requiredTier` is `null` when there is no higher active plan the API can safely recommend, or when the live plan catalogue could not be read. The gate itself is still real — only the "upgrade to this tier" hint is missing. Follow `details.upgradeUrl` (or see [Pricing](https://app.countrystatecity.in/pricing)) to compare the available plans. Handle it as `details.requiredTier ?? 'see pricing'` rather than printing `null` to your users.
</Note>

### Other 403s

Not every `403` is a plan gate. A `403` raised for any other reason returns the envelope with **no** `details` object:

```json 403 - No details theme={null}
{
  "status": "error",
  "message": "<human-readable message>"
}
```

<Warning>
  Check that `details` exists before you branch on it. `error.details?.feature` tells you it's a plan gate; a missing `details` means the message is all you get, and upgrading won't help.
</Warning>

## 404 — Not Found

Most `404` responses use the structured envelope. Five geographical handlers deliberately return the bare error object instead: country details, state details, region details, subregion details, and currency by country. They use it for two cases:

```json 404 - Record doesn't exist theme={null}
{
  "error": "Country not found."
}
```

```json 404 - No data for this record theme={null}
{
  "error": "No currency data available for this country."
}
```

The first means the code or ID you passed matches nothing. The second means the record exists, but has nothing on file for the attribute you asked for — a country with no currency on record, a city with no timezone. Both are `404`, and the wording varies per endpoint.

<Tip>
  Retrying won't change a `404`. Treat "no data on record" as a legitimate empty result in your integration rather than as a failure to retry.
</Tip>

## 429 — Rate limit exceeded

Every plan has a daily and a monthly request quota. Exceeding either returns `429 Too Many Requests`:

```json 429 - Daily limit exceeded theme={null}
{
  "status": "error",
  "message": "Daily usage limit exceeded. Please try again tomorrow or contact support for higher limits.",
  "details": {
    "limit": 100,
    "period": "daily",
    "resetAt": "2026-08-14T00:00:00.000Z",
    "upgradeUrl": "https://app.countrystatecity.in/pricing",
    "tier": "community"
  }
}
```

```json 429 - Monthly limit exceeded theme={null}
{
  "status": "error",
  "message": "Monthly usage limit exceeded. Please upgrade for higher limits.",
  "details": {
    "limit": 3000,
    "period": "monthly",
    "resetAt": "2026-09-01T00:00:00.000Z",
    "upgradeUrl": "https://app.countrystatecity.in/pricing",
    "tier": "community"
  }
}
```

<ResponseField name="details.limit" type="integer">
  The quota you exceeded, in requests.
</ResponseField>

<ResponseField name="details.period" type="string">
  `daily` or `monthly` — which quota was exceeded.
</ResponseField>

<ResponseField name="details.resetAt" type="string">
  ISO 8601 UTC timestamp for when the quota resets: next UTC midnight for `daily`, the 1st of next UTC month for `monthly`. Use this to schedule your retry instead of guessing a backoff window.
</ResponseField>

<ResponseField name="details.tier" type="string">
  Your plan's tier at the time of the request.
</ResponseField>

### Handling a 429 in your integration

```javascript theme={null}
const response = await fetch('https://api.countrystatecity.in/v1/countries/IN/cities', {
  headers: { 'X-CSCAPI-KEY': 'YOUR_API_KEY' }
});

if (response.status === 429) {
  // readError() is defined in "The two error shapes" above.
  const { message, details } = await readError(response);
  console.error(message);

  if (details?.resetAt) {
    const waitMinutes = Math.ceil((new Date(details.resetAt).getTime() - Date.now()) / 60000);
    console.log(`Limit resets in ${waitMinutes} minutes`);
  }
}
```

## Usage headers

When a request is authenticated and counted against your quota — the normal case on a successful `/v1` call — the response carries your plan and current usage as headers. Use them to watch your quota instead of waiting for a `429`:

| Header                | Example     | Meaning                                                                                                                   |
| --------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------- |
| `X-CSC-Plan`          | `community` | The tier your API key is on — the same value as `details.currentTier` on a plan-gate `403` and `details.tier` on a `429`. |
| `X-CSC-Daily-Used`    | `42`        | Requests counted against today's quota so far.                                                                            |
| `X-CSC-Daily-Limit`   | `100`       | Your daily quota — `unlimited` on the Custom tier.                                                                        |
| `X-CSC-Monthly-Used`  | `1203`      | Requests counted against this month's quota so far.                                                                       |
| `X-CSC-Monthly-Limit` | `3000`      | Your monthly quota — `unlimited` on the Custom tier.                                                                      |

<Warning>
  These headers are not on every `/v1` response. They're added when the request is authenticated and counted, so error responses may not carry them. In particular:

  * **`401`** — rejected before any usage is counted, so no usage headers.
  * **`429`** — the request was refused, so no usage headers. Read `details.limit` and `details.resetAt` from the body instead.

  Treat a missing header as *unknown*, never as `0`.
</Warning>

```javascript Reading the usage headers safely theme={null}
const plan = response.headers.get('X-CSC-Plan');
const used = response.headers.get('X-CSC-Daily-Used');
const limit = response.headers.get('X-CSC-Daily-Limit');

if (used === null || limit === null) {
  console.log('Usage headers not present on this response');
} else if (limit === 'unlimited') {
  console.log(`${used} requests today on the ${plan} plan (no daily limit)`);
} else {
  console.log(`${Number(limit) - Number(used)} requests left today on the ${plan} plan`);
}
```

## Usage limits by tier

| Tier         |     Daily |   Monthly |
| ------------ | --------: | --------: |
| Community    |       100 |     3,000 |
| Starter      |       300 |     9,000 |
| Supporter    |     1,000 |    30,000 |
| Professional |     3,300 |   100,000 |
| Business     |    25,000 |   750,000 |
| Legacy       |    50,000 | 1,500,000 |
| Custom       | Unlimited | Unlimited |

See [Pricing](https://app.countrystatecity.in/pricing) for what each tier unlocks beyond request volume — data fields, `?fields=`/`?sort=`, search, and other gated features.

## Related

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api/authentication">
    API key setup and the 401 Unauthorized response.
  </Card>

  <Card title="Field Filtering & Sorting" icon="filter" href="/api/field-filtering-and-sorting">
    `?fields=`/`?sort=` — the other common source of 400 and 403 responses.
  </Card>

  <Card title="FAQ" icon="circle-question" href="/api/faq">
    Common questions, including rate-limit troubleshooting.
  </Card>
</CardGroup>
