Skip to main content

Errors

The Prop AI Deals API uses standard HTTP status codes. Every error response has the same JSON shape so your error handler only needs to be written once.

Error response format

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 1.0 seconds.",
    "retry_after": 1.0
  }
}
  • error.code — Stable machine-readable string. Switch on this in code.
  • error.message — Human-readable explanation. Show in UI / log to console.
  • Additional fields — Some errors include extra context (e.g. retry_after, required_scope).

HTTP status codes

StatusMeaning
200Success
400Malformed request — fix and retry
401Authentication failed — check your API key
403Authenticated, but key lacks the required scope
404Resource not found
422Validation error — request body or query params invalid
429Rate limit or quota exceeded — back off and retry
500Internal server error — usually transient, retry with backoff
502Upstream bad gateway — retry
503Service temporarily unavailable — retry with backoff

Error codes

invalid_api_key — 401

The Authorization header is missing, malformed, or the key has been revoked / expired.
{
  "error": {
    "code": "invalid_api_key",
    "message": "Invalid or expired API key"
  }
}
Fix: Verify the header is exactly Authorization: Bearer paid_.... Check the key hasn’t been revoked in the dashboard.

insufficient_scope — 403

Your key is valid, but its plan doesn’t grant access to this endpoint.
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key lacks the required scope: ai:chat"
  }
}
Fix: Upgrade your plan, or use a different endpoint. See Authentication › Scopes.

rate_limit_exceeded — 429

You’re sending requests faster than your plan’s per-second limit allows.
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 1.0 seconds.",
    "retry_after": 1.0
  }
}
Fix: Sleep for retry_after seconds and retry. Implement client-side throttling. See Rate limits.

monthly_quota_exceeded — 429

You’ve reached your plan’s hard monthly request limit. There is no 2× soft/hard split — once you hit the number printed in X-Monthly-Limit, every subsequent request is rejected until the next calendar month rolls over.
{
  "error": {
    "code": "monthly_quota_exceeded",
    "message": "Monthly request quota exceeded (250000/250000). Upgrade your plan or wait until the next billing period.",
    "used": 250000,
    "limit": 250000
  }
}
This error is NOT transient. Do not retry with backoff — retries will fail with the same error until the billing period resets. Fix: Upgrade your plan at /dashboard/api (takes effect immediately after Stripe charges succeed), or wait until the first of next month when usage counters reset.

ai_chat_quota_exceeded — 429

AI chat endpoints (/api/v1/ultimate-ai/*) have a separate monthly quota that’s been used up.
{
  "error": {
    "code": "ai_chat_quota_exceeded",
    "message": "AI chat quota exceeded (2500/2500). Upgrade your plan or wait until the next billing period.",
    "used": 2500,
    "limit": 2500
  }
}
Starter plans do not include AI chat at all — they receive 403 insufficient_scope on AI endpoints instead. Fix: Upgrade to Professional (2,500 AI chats/month) or Business (15,000 AI chats/month). Non-AI endpoints continue to work normally until your main monthly quota is also exhausted.

endpoint_not_in_public_api — 403

You tried to call an internal endpoint (like /api/v1/api-keys or /api/v1/dashboard/*) with a paid_* API key. The public API exposes only the routes listed in the API reference — dashboard and billing endpoints require a Clerk session token, not an API key.
{
  "error": {
    "code": "endpoint_not_in_public_api",
    "message": "The endpoint /api/v1/dashboard/stats is not exposed via API key authentication."
  }
}
Fix: Check the supported endpoint list. If you need the requested endpoint exposed publicly, email api@propaideals.co.uk.

Service temporarily busy — 503

The API server rejected your request because its database connection pool was at >85% capacity. This is a protective fast-fail under high concurrent load, not a quota or auth error.
{
  "error": "Service temporarily busy",
  "detail": "Database connection pool at capacity. Please retry.",
  "retry_after": 2
}
The response includes a Retry-After: 2 header. 503s do not count against your monthly quota — we only charge successful handler executions. Fix: Retry after retry_after seconds with exponential backoff. If you see sustained 503s, you’re pushing concurrent load higher than your plan’s capacity — throttle client-side.

validation_error — 422

The request body or query string failed validation.
{
  "error": {
    "code": "validation_error",
    "message": "Invalid value for 'min_price': must be a positive integer",
    "details": {
      "field": "min_price",
      "value": "-100"
    }
  }
}
Fix: Check the endpoint reference for valid field types and ranges.

not_found — 404

The resource ID doesn’t exist or has been deleted.
{
  "error": {
    "code": "not_found",
    "message": "Property not found: 5fa1b2c3-d4e5-6f78-9012-3456789abcde"
  }
}
Fix: Check the ID. Property IDs are UUIDs.

internal_error — 500

Something went wrong on our side. We’re already alerted.
{
  "error": {
    "code": "internal_error",
    "message": "An unexpected error occurred. Please try again."
  }
}
Fix: Retry with exponential backoff. If it persists, contact support with your meta.request_id. Implement exponential backoff with jitter for 429, 500, 502, and 503. Don’t retry 400, 401, 403, 404, or 422 — they will fail the same way every time.
async function callApi(url, options, attempt = 0) {
  const res = await fetch(url, options)
  if (res.ok) return res.json()

  const body = await res.json().catch(() => ({}))
  const code = body.error?.code

  // Don't retry client errors
  if (res.status >= 400 && res.status < 500 && res.status !== 429) {
    throw new Error(`${code}: ${body.error?.message}`)
  }

  // Retry with backoff
  if (attempt >= 5) {
    throw new Error(`Max retries exceeded: ${code}`)
  }

  const retryAfter = body.error?.retry_after ?? Math.pow(2, attempt)
  const jitter = Math.random() * 0.3
  await new Promise(r => setTimeout(r, (retryAfter + jitter) * 1000))
  return callApi(url, options, attempt + 1)
}
import time, random, requests

def call_api(url, headers, attempt=0):
    res = requests.get(url, headers=headers, timeout=15)
    if res.ok:
        return res.json()

    body = res.json() if res.headers.get("content-type", "").startswith("application/json") else {}
    code = body.get("error", {}).get("code")

    # Don't retry client errors
    if 400 <= res.status_code < 500 and res.status_code != 429:
        raise RuntimeError(f"{code}: {body.get('error', {}).get('message')}")

    if attempt >= 5:
        raise RuntimeError(f"Max retries exceeded: {code}")

    retry_after = body.get("error", {}).get("retry_after", 2 ** attempt)
    time.sleep(retry_after + random.uniform(0, 0.3))
    return call_api(url, headers, attempt + 1)

Getting help

Every response includes a unique meta.request_id. Include it when you contact support:
Subject: API request failing
Request ID: req_8c7f2a1b9d4e3f5a
Error code: internal_error
Endpoint: GET /api/v1/properties?area=London
Time: 2026-04-14T10:23:00Z
Email: api@propaideals.co.uk