Skip to main content

Rate limits

The Prop AI Deals API enforces three types of limits on every paid_* key:
  1. Per-second rate limit — Sliding-window counter, rejects with 429 when exceeded
  2. Monthly request quotaHard cap at the plan’s monthly_request_limit. No soft/hard split, no 2× tolerance. Once you hit the cap, requests are blocked until the next billing period.
  3. AI chat quota — Separate quota for AI endpoints (which also cost 5× normal requests against your monthly quota)
All limits are per API key (not per IP, not per user). If a user has multiple keys, each gets its own quota.

Plan comparison

PlanPrice (GBP/mo)Requests / month£/requestPer-secondAI chats / monthBatch limit
Starter£9920,000£0.0049510 req/s100100
Professional£299100,000£0.0029925 req/s1,000200
Business£999400,000£0.0025050 req/s4,000500
EnterpriseCustomCustomCustomCustomCustom
All tiers are paid — there is no free tier. Every plan grants read access to properties, market data, investment calculators, spatial search, planning data, lead generators, EPC certificates, UPRN, demographics, flood risk, and heritage data. Professional and Business also include the ai:chat and build-cost:read scopes.
Per-request value: at every tier we’re cheaper per request than the closest UK competitor AND give 20–25× higher per-second rate limits than equivalent tiers at PropertyData.
Enterprise plans are custom-provisioned (negotiated limits, dedicated infrastructure, SLAs). Email api@propaideals.co.uk to discuss. Live plan details: GET https://api.propaideals.co.uk/api/v1/api-billing/plans

How limits are enforced

Per-second rate limit

Sliding-window counter per key in Redis. If you exceed it, the request fails immediately with 429 rate_limit_exceeded:
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 1.0 seconds.",
    "retry_after": 1.0
  }
}
Sleep for retry_after seconds and retry. See retry policy.

Monthly request quota — HARD cap

The plan’s monthly_request_limit is a hard ceiling (not a soft target). Example for Professional at 100,000 requests/month:
  • Request #99,999 → ✅ 200 OK, X-Monthly-Usage: 99999
  • Request #100,000 → ✅ 200 OK, X-Monthly-Usage: 100000
  • Request #100,001 → ❌ 429 monthly_quota_exceeded
Once you’ve consumed the full quota, every subsequent request returns 429 until the calendar month rolls over or you upgrade your plan.
{
  "error": {
    "code": "monthly_quota_exceeded",
    "message": "Monthly request quota exceeded (100000/100000). Upgrade your plan or wait until the next billing period.",
    "used": 100000,
    "limit": 100000
  }
}
We recommend setting up alerts at 80% of your monthly quota so you have time to upgrade before hitting the cap. Monitor meta.usage.monthly_used on every response, or poll GET /api/v1/api-billing/usage periodically.

AI chat quota

AI endpoints (/api/v1/ultimate-ai/*) consume two budgets simultaneously:
  1. 5 requests from the monthly request quota (AI requests cost 5×)
  2. 1 AI chat call from the separate AI chat quota
When the AI quota is exhausted: 429 ai_chat_quota_exceeded. Non-AI endpoints continue to work normally until the main monthly quota is also exhausted.
{
  "error": {
    "code": "ai_chat_quota_exceeded",
    "message": "AI chat quota exceeded (1000/1000). Upgrade your plan or wait until the next billing period.",
    "used": 1000,
    "limit": 1000
  }
}
Starter plans do not include AI chat — the ai:chat scope is only granted to Professional and Business. Sending a paid_* Starter key to /api/v1/ultimate-ai/chat returns 403 insufficient_scope.

Service-level backpressure (503)

During high concurrent traffic, the API server may return 503 Service temporarily busy when its database connection pool is >85% utilized. This is a protective fast-fail to prevent cascade failures — not an authentication or quota error. The response includes a Retry-After: 2 header.
{
  "error": "Service temporarily busy",
  "detail": "Database connection pool at capacity. Please retry.",
  "retry_after": 2
}
Recommended handling: retry after retry_after seconds with exponential backoff. 503s don’t count against your monthly quota — we only increment usage on successful handler execution.

Rate-limit headers

Every successful response includes these headers so you can budget without polling separately:
X-RateLimit-Limit: 25           # Per-second cap for this key (matches your plan)
X-RateLimit-Remaining: 23       # Per-second budget left in this 1-second window
X-Request-Cost: 1               # How much this request consumed (1 for most, 5 for AI)
X-Monthly-Usage: 4321           # Requests used this billing period (after this call)
X-Monthly-Limit: 100000         # Your plan's monthly hard cap
The same data appears in the response envelope:
{
  "meta": {
    "usage": {
      "monthly_used": 4321,
      "monthly_limit": 100000,
      "request_cost": 1
    }
  }
}

Live usage endpoint

Get the current state of all quotas in one call (uses Clerk session auth, not API key):
curl https://api.propaideals.co.uk/api/v1/api-billing/usage \
  -H "Authorization: Bearer eyJ..."
{
  "requests_used": 4321,
  "requests_limit": 100000,
  "ai_chat_used": 87,
  "ai_chat_limit": 1000,
  "plan_tier": "professional",
  "plan_name": "Professional",
  "billing_period_start": "2026-04-01T00:00:00+00:00",
  "billing_period_end": "2026-05-01T00:00:00+00:00"
}

Best practices

Throttle client-side

Most production traffic is bursty. Add a token-bucket or leaky-bucket throttle so you never hit the per-second limit in the first place.
import { RateLimiter } from 'limiter'

// Professional plan: 25 req/s — leave 10-20% headroom
const limiter = new RateLimiter({ tokensPerInterval: 22, interval: 'second' })

async function fetchProperty(id) {
  await limiter.removeTokens(1)
  return fetch(`https://api.propaideals.co.uk/api/v1/properties/${id}`, {
    headers: { Authorization: `Bearer ${process.env.PROPAIDEALS_API_KEY}` },
  }).then(r => r.json())
}
from ratelimit import limits, sleep_and_retry

# Professional plan: 25 req/s — leave 10-20% headroom
@sleep_and_retry
@limits(calls=22, period=1)
def fetch_property(property_id):
    return requests.get(
        f"https://api.propaideals.co.uk/api/v1/properties/{property_id}",
        headers={"Authorization": f"Bearer {API_KEY}"},
    ).json()
Leave 10–20% headroom below your nominal limit to absorb clock skew and transient bursts.

Cache aggressively

Properties don’t change second-to-second. A simple in-memory cache with a 5-minute TTL can cut your bill by 80%+ for read-heavy workloads.
from cachetools import TTLCache, cached

cache = TTLCache(maxsize=10_000, ttl=300)  # 5 minutes

@cached(cache)
def get_property(property_id: str) -> dict:
    return fetch_property(property_id)

Batch where possible

Use the search endpoints (GET /api/v1/properties?area=...&limit=100) instead of N parallel GET /api/v1/properties/{id} calls. One paginated request returns up to 100 properties for the cost of one request — saves 99 quota units and improves latency.

Monitor meta.usage

Log meta.usage.monthly_used on every response. Page yourself when usage crosses 80% of your monthly limit so you can upgrade before hitting the hard cap. For Professional (100,000 requests/month), 80% = 80,000 requests. At 25 req/s sustained, that’s ~53 minutes. At 5 req/s sustained, that’s ~4.4 hours. Set your alerting accordingly.

Handle 429 and 503 with exponential backoff

Both 429 rate_limit_exceeded (per-second) and 503 Service temporarily busy (server pressure) are transient and should be retried with backoff. 429 monthly_quota_exceeded is NOT transient — don’t retry, upgrade instead. See Errors › Recommended retry policy for a copy-paste retry helper.

Run separate keys per environment

Production, staging, and CI should each have their own key. That way a runaway script in CI can’t burn through your production quota. You can have up to 5 active keys per user account.