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

# Rate limits

> Per-tier request + export quotas, the RFC 9457 problem+json 429 shape, and the X-RateLimit-* response headers.

PermitCore enforces **per-tier monthly request + export ceilings** on every
quota-gated endpoint. The current tier model is **Free / Pro / Enterprise** —
see <a href="https://permitcore.io/pricing">permitcore.io/pricing</a> for the
buy flow.

## Tier matrix

| Tier       | Price                      | Request quota                 | Export quota                  |
| ---------- | -------------------------- | ----------------------------- | ----------------------------- |
| Free       | Free                       | 1,000 / month (up to 100/day) | 5,000 / month (up to 500/day) |
| Pro        | $89 / month or $890 / year | 100,000 / month               | Unlimited                     |
| Enterprise | Contact for pricing        | Custom                        | Custom                        |

All tiers include all 18 cohorts and full history.

Webhook event delivery is on the roadmap and not yet available.

## Per-response rate-limit headers

Every 2xx response from `/v1/permits` (and other quota-gated endpoints)
carries the following headers so your client can self-throttle:

| Header                           | Example      | Meaning                                                                                                                                                                                        |
| -------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-RateLimit-Tier`               | `pro`        | Canonical tier serving the request: `free`, `pro`, or `enterprise`.                                                                                                                            |
| `X-RateLimit-Limit-Requests`     | `100000`     | Ceiling for the **currently-binding** quota window.                                                                                                                                            |
| `X-RateLimit-Remaining-Requests` | `99997`      | Requests remaining in the binding window before the next 429.                                                                                                                                  |
| `X-RateLimit-Quota-Kind`         | `monthly`    | Which dimension is binding right now: `daily` or `monthly`. Free is bound by either (1K/month or 100/day) — header reports whichever 429s next.                                                |
| `X-RateLimit-Reset`              | `2026-06-22` | When the binding quota next has capacity. Semantics depend on `X-RateLimit-Quota-Kind` — see below. Absent when the binding dimension is **unlimited** (Pro requests, Pro/Enterprise exports). |

### `X-RateLimit-Reset` semantics (kind-aware)

The reset surface is **interpreted via the `X-RateLimit-Quota-Kind` header**
(or the 429 body's `quota_kind` field — they always agree):

| `X-RateLimit-Quota-Kind` | `X-RateLimit-Reset` meaning                                                                                                                                                                                                            |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `daily`                  | Next UTC midnight — the full daily ceiling restores at once.                                                                                                                                                                           |
| `monthly`                | The earliest moment usage ages out of the trailing 30-day window. Monthly quotas restore *progressively* as old requests roll off, so the reset timestamp is when at least one slot becomes available, not when the full ceiling does. |
| (header absent)          | The binding dimension is unlimited — there is no reset to surface.                                                                                                                                                                     |

Branch on `X-RateLimit-Quota-Kind` (or `quota_kind` in the 429 body) if you
need to display the right narrative to a user. Otherwise the retry
guidance below works identically for both kinds.

## 429 response shape (RFC 9457 problem+json)

When a key exceeds its quota, the API returns `429 Too Many Requests`
with a `Content-Type: application/problem+json` body conforming to
[RFC 9457](https://www.rfc-editor.org/rfc/rfc9457):

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
Content-Type: application/problem+json

{
  "type": "https://api.permitcore.io/errors/monthly-requests-quota-exceeded",
  "title": "Monthly request quota exceeded",
  "detail": "You have used 100000 of 100000 requests for the current monthly window.",
  "retry_after_seconds": 3600,
  "reset_at": "2026-06-22",
  "limit": 100000,
  "quota_kind": "monthly",
  "tier_name": "pro",
  "tier_quota_daily": null,
  "tier_quota_monthly": 100000,
  "legacy_code": "rate_limited"
}
```

### Field map

| Field                 | Type                      | Contractual?                              | Meaning                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------- | ------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                | URI string                | **YES**                                   | Stable identifier. Pattern: `https://api.permitcore.io/errors/{daily\|monthly}-{requests\|exports}-quota-exceeded`. Branch your error-handling on this URI, not on `title`/`detail`.                                                                                                                                                                                                          |
| `title`               | string                    | NO — illustrative only                    | Short human-readable summary. **Not contractual** — prose may change without notice.                                                                                                                                                                                                                                                                                                          |
| `detail`              | string                    | NO — illustrative only                    | Longer human-readable explanation, often with the binding numbers inlined. **Not contractual.**                                                                                                                                                                                                                                                                                               |
| `retry_after_seconds` | integer                   | **YES**                                   | Seconds to wait before the next request is likely to succeed. Equals the `Retry-After` header. Matches `X-RateLimit-Reset` (kind-aware).                                                                                                                                                                                                                                                      |
| `reset_at`            | ISO 8601 date or datetime | Conditional / kind-aware                  | When the binding quota next has capacity. For `quota_kind: "daily"` the next UTC midnight; for `quota_kind: "monthly"` the earliest moment usage ages out of the trailing 30-day window (monthly quotas restore progressively). **Absent** when the binding dimension is unlimited (Pro requests, Pro/Enterprise exports). The field name is `reset_at` (singular) — there is no `resets_at`. |
| `limit`               | integer                   | **YES**                                   | The ceiling for the currently-binding quota window — matches `X-RateLimit-Limit-Requests`.                                                                                                                                                                                                                                                                                                    |
| `quota_kind`          | `"daily"` \| `"monthly"`  | **YES**                                   | Which dimension was binding when the 429 fired.                                                                                                                                                                                                                                                                                                                                               |
| `tier_name`           | string                    | **YES**                                   | Canonical tier name (`free`, `pro`, `enterprise`).                                                                                                                                                                                                                                                                                                                                            |
| `tier_quota_daily`    | integer \| null           | **YES**                                   | The tier's daily ceiling. `null` means **unlimited** on that dimension (Pro + Enterprise have no daily ceiling).                                                                                                                                                                                                                                                                              |
| `tier_quota_monthly`  | integer \| null           | **YES**                                   | The tier's monthly ceiling. `null` means **unlimited**.                                                                                                                                                                                                                                                                                                                                       |
| `legacy_code`         | string                    | **DEPRECATED** — removed after 2026-06-27 | Legacy `error.code` value (`rate_limited`) emitted for clients written against the pre-RFC-9457 envelope. **Migrate to branching on `type`.** Field will be removed entirely after 2026-06-27.                                                                                                                                                                                                |
| `grandfather_until`   | ISO 8601 timestamp        | Conditional / rare                        | Present only when an account is mid-migration between tier configurations. Most responses omit this field.                                                                                                                                                                                                                                                                                    |

Title and detail are **illustrative** — the example body shows representative
prose, but title/detail may change without notice. Always branch on `type`
or `legacy_code` (until removed); never on the prose fields.

## Recommended retry behavior

**Sleep on `retry_after_seconds` (or the `Retry-After` header, or
`X-RateLimit-Reset`) and retry — the timer is kind-aware on both
daily and monthly quotas.** All three surfaces agree.

For monthly quotas the timer points at *the next slot* in the trailing
30-day window (not the full ceiling restoring at once), so a successful
retry consumes that newly-available slot — back-to-back retries against
a fully-exhausted monthly key will still 429.

```js Node.js theme={null}
async function permitcoreWithRetry(path, attempt = 1) {
  const res = await fetch(`https://api.permitcore.io${path}`, {
    headers: { Authorization: `Bearer ${process.env.PERMITCORE_API_KEY}` },
  });
  if (res.status === 429) {
    const body = await res.json();
    const retryAfter = Number(
      res.headers.get("retry-after") ?? body.retry_after_seconds ?? 0,
    );

    // Sleep the indicated duration and retry. Same logic for daily +
    // monthly — quota_kind only matters for the user-facing narrative.
    if (attempt < 3 && retryAfter > 0) {
      await new Promise((r) => setTimeout(r, retryAfter * 1000));
      return permitcoreWithRetry(path, attempt + 1);
    }
    throw new Error(
      `Rate limited (quota_kind=${body.quota_kind}, tier=${body.tier_name}). ` +
      `Limit ${body.limit}; check usage at /account/api-keys.`,
    );
  }
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
```

If you want to surface a user-facing message that distinguishes "daily
quota — comes back at midnight UTC" from "monthly quota — slots restore
progressively over the next 30 days," branch on `quota_kind` for the
narrative. The retry mechanics are the same either way.

### Other guidance

1. **Cache responses** when you can — most endpoints declare
   `Cache-Control` headers (e.g., cohort distribution caches 1 hour).
2. **Bulk over loop.** Where the API supports bulk parameters, prefer them.
3. **Watch `X-RateLimit-Remaining-Requests`** on 2xx responses — back off
   *before* the 429 fires.

## Migration: `legacy_code` removal 2026-06-27

The `legacy_code` field (`"rate_limited"`) was added at launch for clients
written against a pre-RFC-9457 error envelope shape. **It will be removed
on 2026-06-27.** Before that date:

* Migrate any client logic branching on `error.code === "rate_limited"`
  to branching on `type` (URI) instead.
* The `type` URI pattern is stable: `…/errors/{daily|monthly}-{requests|exports}-quota-exceeded`.
* If you can't migrate by 2026-06-27, fall back to branching on HTTP
  status `429` plus the `X-RateLimit-*` headers.

Watch the [changelog](/changelog) for the removal notice.
