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 | 890 / year | 100,000 / month | Unlimited |
| Enterprise | Contact for pricing | Custom | Custom |
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. |
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 returns429 Too Many Requests
with a Content-Type: application/problem+json body conforming to
RFC 9457:
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. |
type
or legacy_code (until removed); never on the prose fields.
Recommended retry behavior
Sleep onretry_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.
Node.js
quota_kind for the
narrative. The retry mechanics are the same either way.
Other guidance
- Cache responses when you can — most endpoints declare
Cache-Controlheaders (e.g., cohort distribution caches 1 hour). - Bulk over loop. Where the API supports bulk parameters, prefer them.
- Watch
X-RateLimit-Remaining-Requestson 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 ontype(URI) instead. - The
typeURI 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
429plus theX-RateLimit-*headers.