--- title: Prop AI Deals API description: REST API for UK property data, market intelligence, investment analysis, and AI-powered search. Access 4.9M+ live and sold UK properties with PostGIS spatial search, BMV scoring, rental yields, and HM Land Registry history. --- # Prop AI Deals API The Prop AI Deals API gives you programmatic access to the UK's largest live property dataset — **600,000+ on-market UK listings plus an exclusive off-market lead pool**, enriched with HM Land Registry sold history, EPC certificates, rental estimates, BMV (below-market-value) scoring, and AI-powered investment analysis. Built for property investors, fintech apps, lettings platforms, valuation tools, and research teams that need fresh, structured UK real estate data. ## Base URL ``` https://api.propaideals.co.uk ``` All endpoints are versioned under `/api/v1/`. ## What you can do | Capability | Endpoints | |---|---| | Search live for-sale and rental listings | `GET /api/v1/properties` | | Get full property details | `GET /api/v1/properties/{id}` | | Search by viewport, polygon, or radius | `POST /api/v1/spatial/search/*` | | Look up sold history (HM Land Registry + multi-portal aggregation) | `GET /api/v1/market-data/{id}/sold-history` | | Source **off-market** distressed / motivated-seller leads | `GET /api/v1/off-market-lead-generators/*` | | Find on-street comparable sales | `GET /api/v1/market-data/{id}/on-street` | | Pull rental estimates and yields | `GET /api/v1/rentals/*` | | Run BTL, BRRR, flip, HMO, SA calculators | `POST /api/v1/calculations/*` | | Get area market intelligence | `GET /api/v1/market-intelligence/*` | | Query off-market and motivated-seller leads | `GET /api/v1/off-market/*` | | Chat with the property AI | `POST /api/v1/ultimate-ai/chat` | ## Quick example ```bash curl https://api.propaideals.co.uk/api/v1/properties?area=London&min_price=300000&max_price=500000 \ -H "Authorization: Bearer paid_your_api_key_here" ``` ```javascript const res = await fetch( 'https://api.propaideals.co.uk/api/v1/properties?area=London&min_price=300000&max_price=500000', { headers: { Authorization: 'Bearer paid_your_api_key_here' } } ) const { data, meta } = await res.json() console.log(data.properties) ``` ```python import requests res = requests.get( "https://api.propaideals.co.uk/api/v1/properties", params={"area": "London", "min_price": 300000, "max_price": 500000}, headers={"Authorization": "Bearer paid_your_api_key_here"}, ) res.raise_for_status() print(res.json()["data"]["properties"]) ``` ## Get started 1. **[Quickstart](./quickstart)** — Make your first request in under 3 minutes 2. **[Authentication](./authentication)** — Create and use API keys 3. **[Rate limits](./rate-limits)** — Per-second and monthly quotas 4. **[Errors](./errors)** — Error codes and how to handle them 5. **[Response format](./response-format)** — Envelope, pagination, headers ## API reference - **[Properties](./endpoints/properties)** — Search, list, and retrieve UK property listings - **[Spatial search](./endpoints/spatial)** — Viewport, polygon, radius queries - **[Market data](./endpoints/market-data)** — Sold history, comparables, price trends - **[AI chat](./endpoints/ai-chat)** — Conversational property intelligence - **[API keys (dashboard)](./endpoints/api-keys)** — Create and manage your keys ## SDK & tooling - **[OpenAPI spec](https://api.propaideals.co.uk/openapi.json)** — machine-readable OpenAPI 3.1 definition - **[Status page](https://status.propaideals.co.uk)** — real-time API uptime and incident history - **[Changelog](./changelog)** — version history and breaking changes - **[llms.txt](https://docs.propaideals.co.uk/llms.txt)** — Markdown index for LLM-based agents (llmstxt.org spec) ## Support - Email: `api@propaideals.co.uk` - Office hours: Mon–Fri 09:00–17:00 GMT --- title: Quickstart description: Make your first authenticated request to the Prop AI Deals API in under 3 minutes. Includes cURL, Node.js, and Python examples. --- # Quickstart Get from zero to your first API response in three minutes. ## 1. Create an account Sign up at [propaideals.co.uk/sign-up](https://propaideals.co.uk/sign-up), then create a key at [propaideals.co.uk/dashboard/api](https://propaideals.co.uk/dashboard/api). The **Free tier (200 credits/month) needs no card** — a key is issued instantly. Paid plans: Starter **£29/mo**, Growth **£59/mo**, Advanced **£99/mo**, Professional **£299/mo**, Business **£999/mo**. ## 2. Generate an API key 1. Open the [API dashboard](https://propaideals.co.uk/dashboard/api) 2. Click **Create API Key** 3. Give it a name (e.g. `Production app`) 4. Copy the key shown — it starts with `paid_` and is only displayed once ``` paid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 ``` > **Important:** Treat your API key like a password. Store it in an environment variable, never commit it to source control, and rotate it if you suspect it's been exposed. ## 3. Make your first request ```bash curl https://api.propaideals.co.uk/api/v1/properties?area=Manchester&limit=5 \ -H "Authorization: Bearer paid_your_api_key_here" ``` ```javascript // Node.js 18+ (built-in fetch) const response = await fetch( 'https://api.propaideals.co.uk/api/v1/properties?area=Manchester&limit=5', { headers: { Authorization: `Bearer ${process.env.PROPAIDEALS_API_KEY}`, }, } ) if (!response.ok) { throw new Error(`API error: ${response.status}`) } const { data, meta } = await response.json() console.log(`Found ${data.total} Manchester properties`) console.log(`Request ID: ${meta.request_id}`) console.log(`Monthly usage: ${meta.usage.monthly_used}/${meta.usage.monthly_limit}`) ``` ```python import os import requests API_KEY = os.environ["PROPAIDEALS_API_KEY"] response = requests.get( "https://api.propaideals.co.uk/api/v1/properties", params={"area": "Manchester", "limit": 5}, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=15, ) response.raise_for_status() body = response.json() print(f"Found {body['data']['total']} Manchester properties") print(f"Request ID: {body['meta']['request_id']}") ``` ```ruby require "net/http" require "json" require "uri" uri = URI("https://api.propaideals.co.uk/api/v1/properties") uri.query = URI.encode_www_form(area: "Manchester", limit: 5) req = Net::HTTP::Get.new(uri) req["Authorization"] = "Bearer #{ENV['PROPAIDEALS_API_KEY']}" res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } body = JSON.parse(res.body) puts "Found #{body['data']['total']} Manchester properties" ``` ```go package main import ( "encoding/json" "fmt" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.propaideals.co.uk/api/v1/properties?area=Manchester&limit=5", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("PROPAIDEALS_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() var body map[string]any json.NewDecoder(res.Body).Decode(&body) fmt.Printf("Found %v Manchester properties\n", body["data"].(map[string]any)["total"]) } ``` ## 4. Understand the response Every successful response is wrapped in an envelope: ```json { "data": { "properties": [ { "id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde", "title": "3 bed terraced house for sale", "address": "12 Example Road, Manchester", "postcode": "M1 1AA", "price_numeric": 245000, "bedrooms": 3, "bathrooms": 1, "property_type": "terraced", "latitude": 53.4808, "longitude": -2.2426, "agent_name": "Example Estates", "agent_logo": "https://...", "estimated_rental_yield": 7.2, "bmv_discount_percentage": 8.5, "first_seen": "2026-04-01T09:30:00Z", "url": "https://www.rightmove.co.uk/properties/..." } ], "total": 1842, "page": 1, "limit": 5 }, "meta": { "request_id": "req_8c7f2a1b9d4e3f5a", "timestamp": 1713088200.123, "latency_ms": 184.5, "usage": { "monthly_used": 12, "monthly_limit": 10000, "request_cost": 1 } } } ``` - **`data`** — Endpoint payload (varies by endpoint) - **`meta.request_id`** — Include this in support tickets - **`meta.usage`** — Live usage counters; useful for budget alerts Every response also includes these headers: ``` X-RateLimit-Limit: 10 X-RateLimit-Remaining: 9 X-Request-Cost: 1 X-Monthly-Usage: 12 X-Monthly-Limit: 10000 ``` ## 5. Next steps - **[Authentication](./authentication)** — Manage and rotate keys - **[Rate limits](./rate-limits)** — Avoid hitting quotas - **[Errors](./errors)** — Build robust error handling - **[Properties endpoint](./endpoints/properties)** — Full filter reference --- title: Authentication description: Authenticate Prop AI Deals API requests with bearer API keys. Covers key creation, rotation, scopes, security best practices, and the difference between API keys and Clerk JWT tokens. --- # Authentication The Prop AI Deals API uses **bearer API keys** for all programmatic access. Keys are created from your dashboard and sent on every request via the `Authorization` header. ## API key format Keys look like this: ``` paid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 ``` - Prefix: `paid_` (37 characters total) - 32 hex characters of cryptographic randomness after the prefix - Hashed with HMAC-SHA256 in our database — we cannot recover a lost key - Cached in Redis for sub-millisecond validation - One key carries the full plan and scope set of the user who created it ## Creating an API key ### From the dashboard 1. Open [propaideals.co.uk/dashboard/api](https://propaideals.co.uk/dashboard/api) 2. Click **Create API Key** 3. Enter a descriptive name (e.g. `production-server`, `staging-app`) 4. Copy the key from the modal — **this is the only time it will be shown** 5. Store it in your secret manager (1Password, AWS Secrets Manager, Doppler, etc.) ### Limits - Maximum **5 active keys per user** - Revoke an old key before creating a 6th - Each key inherits its plan from your active subscription ## Sending the key Send the key in the `Authorization` header on every request: ```http GET /api/v1/properties HTTP/1.1 Host: api.propaideals.co.uk Authorization: Bearer paid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 Accept: application/json ``` ```bash curl https://api.propaideals.co.uk/api/v1/properties \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ```javascript fetch('https://api.propaideals.co.uk/api/v1/properties', { headers: { Authorization: `Bearer ${process.env.PROPAIDEALS_API_KEY}` }, }) ``` ```python import os, requests requests.get( "https://api.propaideals.co.uk/api/v1/properties", headers={"Authorization": f"Bearer {os.environ['PROPAIDEALS_API_KEY']}"}, ) ``` ## Scopes Each plan grants a set of scopes. The middleware checks the request path against the required scope: | Scope | Endpoint prefix | What it grants | |---|---|---| | `properties:read` | `/api/v1/properties`, `/api/v1/media` | Listing search, detail, and the property image proxy | | `market-data:read` | `/api/v1/market-data` | Sold history, comparables, valuations | | `investment:read` | `/api/v1/investment`, `/api/v1/calculations` | Investment calculators and strategy comparison | | `areas:read` | `/api/v1/area`, `/api/v1/area-analytics`, `/api/v1/area-pack`, `/api/v1/area-report-subscriptions`, `/api/v1/market-intelligence`, `/api/v1/yields`, `/api/v1/rents`, `/api/v1/demographics`, `/api/v1/points-of-interest`, `/api/v1/agents-list` | Area-level analytics, yields, rents, demographics, POIs, estate agents | | `spatial:read` | `/api/v1/spatial` | Viewport / polygon / radius search | | `planning:read` | `/api/v1/planning`, `/api/v1/flood-risk`, `/api/v1/heritage` | Planning applications, flood risk, heritage constraints | | `leads:read` | `/api/v1/lead-generators`, `/api/v1/off-market-lead-generators` | Lead generators and off-market signals | | `epc:read` | `/api/v1/epc`, `/api/v1/uprn` | EPC certificates and UPRN lookups | | `build-cost:read` | `/api/v1/build-cost` | Build cost estimates (Spon's 2026) | | `ai:chat` | `/api/v1/ultimate-ai` | Conversational AI (cost: 5 per request) | Requests to a path your key doesn't have scope for return: ```json { "error": { "code": "insufficient_scope", "message": "This key lacks the required scope: properties:read" } } ``` ## Rotating keys We recommend rotating production keys at least every 90 days, and immediately if you suspect exposure. 1. Create a new key in the dashboard 2. Deploy it to your application as a new env var 3. Verify traffic is flowing on the new key (check `last_used` in the dashboard) 4. Revoke the old key Revocation is instant — within 5 minutes of revoking, no requests using that key will succeed (the Redis cache TTL is 5 minutes; you can call the dashboard "Revoke" button to purge it immediately). ## Two authentication systems Prop AI Deals has two separate authentication paths: | | API keys (`paid_*`) | Clerk JWT (`eyJhbG...`) | |---|---|---| | **Who uses it** | Server-to-server, scripts, integrations | Logged-in users in the web app | | **How obtained** | Created in the dashboard | Issued by Clerk on browser sign-in | | **Lifetime** | Until revoked | Short-lived (rotated every 60 minutes) | | **Scopes** | Plan-based, fixed at creation | Full user permissions | | **Used for** | The public API (everything in this documentation) | The dashboard, billing, account settings | | **Header** | `Authorization: Bearer paid_...` | `Authorization: Bearer eyJ...` | | **Rate limited per** | Key | User | The same endpoint can usually accept either token type. The middleware auto-detects which one was sent based on the prefix. ## Security best practices - **Never** put API keys in client-side JavaScript, mobile apps, browser extensions, or public source control. Anyone with the key can use your full quota. - **Use environment variables.** `process.env.PROPAIDEALS_API_KEY` in Node, `os.environ["PROPAIDEALS_API_KEY"]` in Python. - **Use a secrets manager** in production (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Doppler, 1Password Secrets Automation). - **Restrict by network** where possible — IP allowlisting on your egress proxy adds defence in depth. - **Monitor `last_used`** in the dashboard to detect inactive keys. - **Rotate** regularly. If you ever leak a key, revoke it within minutes. - **One key per environment.** Use a separate key for production, staging, and local development. ## Verifying a key works Hit the cheapest endpoint with your key — it should return `200`: ```bash curl -i https://api.propaideals.co.uk/api/v1/properties?limit=1 \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` You should see `HTTP/1.1 200 OK` and headers including `X-RateLimit-Limit`, `X-Monthly-Usage`. If you get `401 invalid_api_key`, the key is wrong, expired, or revoked. See [Errors](./errors) for the full reference. --- title: Rate limits description: Per-second rate limits, monthly request quotas, AI chat quotas, and rate limit headers for the Prop AI Deals API. Covers the Free, Starter, Growth, Advanced, Professional, and Business plans with concrete numbers. --- # Rate limits The Prop AI Deals API enforces three types of limits on every `paid_*` request: 1. **Per-second rate limit** — Sliding-window counter, rejects with `429` when exceeded 2. **Monthly request quota** — **Hard 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 **account** (not per IP, not per key). Your subscription buys one shared pool: every API key you create draws from the **same** per-second rate limit, monthly request quota, and AI chat quota. Creating more keys does **not** increase your limits. ## Plan comparison | Plan | Price (GBP/mo) | Credits / month | £/credit | Per-second | AI chats / month | Batch limit | |---|---|---|---|---|---|---| | **Free** | £0 | 200 | — | 2 req/s | 0 | — | | **Starter** | £29 | 2,500 | £0.0116 | 5 req/s | 0 | — | | **Growth** | £59 | 7,500 | £0.0079 | 10 req/s | 25 | — | | **Advanced** | £99 | 20,000 | £0.00495 | 10 req/s | 100 | 100 | | **Professional** | £299 | 100,000 | £0.00299 | 25 req/s | 1,000 | 200 | | **Business** | £999 | 400,000 | £0.00250 | 50 req/s | 4,000 | 500 | | **Enterprise** | Custom | Custom | — | Custom | Custom | Custom | **The Free tier needs no card and issues a key instantly.** Free, Starter and Growth cover core reads: properties, market data, area aggregates and EPC/UPRN. **Advanced and above** add investment calculators, spatial search, planning / flood-risk / heritage data, and lead generators. The `ai:chat` scope starts at Growth (quota scales by tier — see the table). Professional and Business additionally include `build-cost:read`. > **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 account in Redis (shared across all your keys). If you exceed it, the request fails immediately with `429 rate_limit_exceeded`: ```json { "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](./errors#recommended-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. ```json { "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. ```json { "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 } } ``` **AI chat is included from the Growth plan up** — Growth 25 chats/month, Advanced 100, Professional 1,000, Business 4,000. Free and Starter do not include AI chat. When the AI quota is exhausted the AI endpoints return `429 ai_chat_quota_exceeded`; all other endpoints keep working until the main monthly request quota is also used up. ### 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. ```json { "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 your account (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: ```json { "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): ```bash curl https://api.propaideals.co.uk/api/v1/api-billing/usage \ -H "Authorization: Bearer eyJ..." ``` ```json { "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. ```javascript 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()) } ``` ```python 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. ```python 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](./errors#recommended-retry-policy) for a copy-paste retry helper. ### Run separate keys per environment Give production, staging, and CI each their own key so you can revoke or rotate one without disrupting the others, and tell traffic apart in `GET /api/v1/api-billing/logs` and `/usage/daily`. Note: all keys on your account share one quota pool — separate keys improve hygiene and attribution, they do not raise your limits, so a runaway CI script still draws down the same monthly quota. Add a client-side throttle to non-production keys if that's a concern. You can have up to 5 active keys per account. --- title: Errors description: Prop AI Deals API error format, error codes, HTTP status codes, and how to handle every possible failure mode in production code. --- # 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 ```json { "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 | Status | Meaning | |---|---| | `200` | Success | | `400` | Malformed request — fix and retry | | `401` | Authentication failed — check your API key | | `403` | Authenticated, but key lacks the required scope | | `404` | Resource not found | | `422` | Validation error — request body or query params invalid | | `429` | Rate limit or quota exceeded — back off and retry | | `500` | Internal server error — usually transient, retry with backoff | | `502` | Upstream bad gateway — retry | | `503` | Service temporarily unavailable — retry with backoff | ## Error codes ### `invalid_api_key` — 401 The `Authorization` header is missing, malformed, or the key has been revoked / expired. ```json { "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. ```json { "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](./authentication#scopes). ### `rate_limit_exceeded` — 429 You're sending requests faster than your plan's per-second limit allows. ```json { "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](./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. ```json { "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. ```json { "error": { "code": "ai_chat_quota_exceeded", "message": "AI chat quota exceeded (100/100). Upgrade your plan or wait until the next billing period.", "used": 100, "limit": 100 } } ``` AI chat is included from the Growth plan up: Growth 25/month, Advanced 100/month, Professional 1,000/month, Business 4,000/month. Free and Starter do not include AI chat. **Fix:** Upgrade for a higher AI chat quota, or wait until the first of next month when usage counters reset. 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](./endpoints/properties) — dashboard and billing endpoints require a Clerk session token, not an API key. ```json { "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](./endpoints/properties). 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. ```json { "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. ```json { "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. ```json { "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. ```json { "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`. ## Recommended retry policy 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. ```javascript 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) } ``` ```python 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` --- title: Response format description: How Prop AI Deals API responses are structured. Covers the JSON envelope, pagination, headers, request IDs, and timestamps. --- # Response format Every JSON response from the Prop AI Deals API is wrapped in a consistent envelope. Errors share the same shape across all endpoints, and metadata about the request (latency, usage, request ID) lives in `meta`. ## Envelope ```json { "data": { /* endpoint-specific payload */ }, "meta": { "request_id": "req_8c7f2a1b9d4e3f5a", "timestamp": 1713088200.123, "latency_ms": 184.5, "usage": { "monthly_used": 4321, "monthly_limit": 100000, "request_cost": 1 } } } ``` | Field | Type | Description | |---|---|---| | `data` | object | Endpoint-specific payload — varies per endpoint. See the endpoint reference. | | `meta.request_id` | string | Unique ID for this request. Include in support tickets. | | `meta.timestamp` | number | Unix epoch (seconds, with millisecond precision) when the response was finalised. | | `meta.latency_ms` | number | Server-side processing time in milliseconds. | | `meta.usage.monthly_used` | integer | Requests consumed this billing period after this call. | | `meta.usage.monthly_limit` | integer | Hard cap (monthly quota ceiling). | | `meta.usage.request_cost` | integer | What this single request consumed (1 for most, 5 for AI). | > The `data` field always contains the actual response. Always read `body.data`, never `body` directly. ## Errors share the same envelope Errors omit `data` and `meta`, returning only an `error` object: ```json { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded. Retry after 1.0 seconds.", "retry_after": 1.0 } } ``` See [Errors](./errors) for the full reference. ## Headers Every response includes: | Header | Description | |---|---| | `X-Request-ID` | Same value as `meta.request_id` | | `X-RateLimit-Limit` | Per-second cap for your account | | `X-RateLimit-Remaining` | Per-second budget left in the current window | | `X-Request-Cost` | Cost of this request (1 or 5) | | `X-Monthly-Usage` | Requests used this billing period | | `X-Monthly-Limit` | Hard cap (monthly quota ceiling) | ## Pagination List endpoints (`GET /api/v1/properties`, `GET /api/v1/spatial/search/*`, etc.) return paginated results. ### Offset pagination (default) Use `page` and `limit` query parameters: ```bash curl "https://api.propaideals.co.uk/api/v1/properties?area=London&page=2&limit=20" \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ```json { "data": { "properties": [ /* 20 properties */ ], "total": 4823, "page": 2, "limit": 20, "pages": 242 } } ``` | Param | Type | Default | Max | Description | |---|---|---|---|---| | `page` | integer | `1` | — | 1-indexed page number | | `limit` | integer | `20` | `100` | Items per page | ### Cursor pagination (large result sets) For result sets above ~10,000 items, offset pagination becomes slow. Use cursor pagination instead: ```bash curl "https://api.propaideals.co.uk/api/v1/properties?area=London&limit=100" \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ```json { "data": { "properties": [ /* 100 properties */ ], "next_cursor": "eyJpZCI6IjVmYTFiMmMzIiwic2VlbiI6IjIwMjYtMDQtMDFUMDk6MzA6MDBaIn0=", "has_more": true } } ``` Pass the cursor on the next request: ```bash curl "https://api.propaideals.co.uk/api/v1/properties?area=London&limit=100&cursor=eyJpZCI6..." \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` When `has_more` is `false`, you've reached the end. ## Timestamps All timestamps are returned as **ISO 8601** strings in UTC: ``` 2026-04-14T09:30:00Z ``` Parse with: ```javascript const date = new Date('2026-04-14T09:30:00Z') ``` ```python from datetime import datetime date = datetime.fromisoformat('2026-04-14T09:30:00Z'.replace('Z', '+00:00')) ``` ## Money values Prices are returned as integers (pounds, no pence) in the `*_numeric` fields. The string `price` field is a formatted display string ("£450,000") and should never be parsed for calculations. ```json { "price": "£450,000", "price_numeric": 450000, "currency": "GBP" } ``` ## Coordinates Latitude and longitude are returned as floats in WGS84 (EPSG:4326) — the same projection used by Google Maps, Mapbox, and Leaflet: ```json { "latitude": 53.4808, "longitude": -2.2426 } ``` ## Field availability Not every property has every field populated. Optional fields can be `null`. Always check before accessing nested properties: ```javascript // Good const yield = property.estimated_rental_yield ?? 0 // Bad const yield = property.estimated_rental_yield.toFixed(1) // crashes on null ``` ```python # Good yield_pct = property.get("estimated_rental_yield") or 0 # Bad yield_pct = property["estimated_rental_yield"] # KeyError on missing ``` --- title: Connect with AI agents (MCP) description: Connect Claude Code, Cursor, Claude Desktop, Continue, and any other MCP-aware AI client to the Prop AI Deals API documentation. Three integration paths — MCP server, llms.txt, and llms-full.txt — covering every major LLM agent ecosystem. --- # Connect with AI agents Prop AI Deals is **AI-native by design**. Every major AI agent ecosystem can discover and use our API documentation without a single line of glue code on your side. Three integration layers, in order of richness: | Layer | Best for | URL | |---|---|---| | **MCP Server** | Claude Code, Cursor, Claude Desktop, Continue, Cline, any MCP client | `https://docs.propaideals.co.uk/mcp` | | **`llms.txt`** | ChatGPT browsing, Perplexity, Claude search, You.com | `https://docs.propaideals.co.uk/llms.txt` | | **`llms-full.txt`** | One-shot context loading for any LLM | `https://docs.propaideals.co.uk/llms-full.txt` | ## MCP Server (recommended) Our docs are exposed as a **Model Context Protocol** server with two tools: - `search_prop_ai_deals` — semantic + keyword search across all our API docs - `query_docs_filesystem_prop_ai_deals` — read individual doc pages directly When connected, your AI agent can answer questions about our API, generate working code samples for any endpoint, and look up plan details, rate limits, and error codes — all without the user pasting docs into the chat. ### Claude Code Add the MCP server with one CLI command: ```bash claude mcp add propaideals https://docs.propaideals.co.uk/mcp ``` Then in any Claude Code session you can ask: ``` > How do I search for properties in London with a yield above 8%? > What error code do I get when I exceed the monthly cap? > Generate a Python script that fetches sold history for a property ``` Claude Code will use the MCP server to look up real answers from our docs. ### Cursor Add to your `~/.cursor/mcp.json`: ```json { "mcpServers": { "propaideals": { "url": "https://docs.propaideals.co.uk/mcp" } } } ``` Restart Cursor. The Cursor chat will now have a "propaideals" tool available — it'll auto-invoke when you ask property API questions. ### Claude Desktop Edit your `claude_desktop_config.json` (located in `~/Library/Application Support/Claude/` on macOS, `%APPDATA%\Claude\` on Windows): ```json { "mcpServers": { "propaideals": { "type": "http", "url": "https://docs.propaideals.co.uk/mcp" } } } ``` Restart Claude Desktop. You'll see a hammer icon (🔨) in the chat input — click it to confirm the `propaideals` server is connected. ### Continue (VS Code / JetBrains) Edit `~/.continue/config.json`: ```json { "experimental": { "modelContextProtocolServers": [ { "transport": { "type": "http", "url": "https://docs.propaideals.co.uk/mcp" } } ] } } ``` ### Cline (VS Code) In Cline settings → MCP Servers → Add Server → enter `https://docs.propaideals.co.uk/mcp`. ### Other MCP clients Any MCP-compatible client should work. The transport is **HTTP streaming** (not stdio), and authentication is **public read-only** — no API key needed to read the docs. ## `llms.txt` (for non-MCP agents) If your agent doesn't speak MCP yet, it almost certainly knows about [llmstxt.org](https://llmstxt.org). Our `llms.txt` is a structured Markdown index of all the API docs: ``` https://docs.propaideals.co.uk/llms.txt https://www.propaideals.co.uk/llms.txt ``` Both URLs serve the same file. ChatGPT browsing, Perplexity, Claude search, You.com, and most "research mode" agents will discover and use it automatically when answering questions about our API. You can also paste it into any chat: ``` Here's the Prop AI Deals API documentation index: https://docs.propaideals.co.uk/llms.txt Help me build a Python script that searches for high-yield properties. ``` ## `llms-full.txt` (one-shot context) For agents that prefer to load all docs into context at once (instead of fetching pages incrementally), we provide a **2,400+ line concatenated file**: ``` https://docs.propaideals.co.uk/llms-full.txt https://www.propaideals.co.uk/llms-full.txt ``` This is the entire public API documentation — quickstart, authentication, rate limits, errors, response format, every endpoint reference, and the changelog — in one Markdown file. Useful for: - Long-context models that can ingest the whole thing in a single message - Pre-loading the docs into a system prompt - Building a custom RAG index of our docs offline - Agents that don't want to make N HTTP calls during a conversation ## What this means for your apps If you're building a property tool and want users to be able to ask natural-language questions like: > "Find me 3-bed houses under £400k with at least 7% yield in Manchester" ...you have three options: 1. **Use our [AI chat endpoint](./endpoints/ai-chat)** directly — we run the LLM, you get back property cards. Costs 5× a normal request. 2. **Connect your AI agent to our MCP server** so it can look up the right endpoint to call, then call it itself with your `paid_*` key. The agent does the orchestration; you pay our standard rate. 3. **Embed our `llms.txt` in your system prompt** so any LLM you use already knows our API surface. Option 1 is fastest to ship. Option 2 gives you the most control. Option 3 is the cheapest. ## Verifying the MCP server You can test the MCP server is reachable without an MCP client by sending a JSON-RPC call directly: ```bash curl -X POST https://docs.propaideals.co.uk/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` You should get back a list of two tools: `search_prop_ai_deals` and `query_docs_filesystem_prop_ai_deals`. ## Status & support The MCP server is hosted by [Mintlify](https://mintlify.com) and inherits their uptime. We monitor it as part of our [status page](https://status.propaideals.co.uk). If you have an issue connecting an AI agent or want a feature, email `api@propaideals.co.uk`. --- title: Properties endpoint description: Search, list, and retrieve on-market UK property listings aggregated from every major UK property portal. 140+ filter parameters, BMV scoring, rental yields, agent details. Required scope properties:read. --- # Properties The `/api/v1/properties` endpoint is the workhorse of the Prop AI Deals API. It serves **600,000+ on-market UK property listings** aggregated from every major UK property portal and refreshed continuously, with deep filtering, BMV scoring, rental yield estimates, and agent details. For **off-market** leads (distressed sales, motivated sellers, repossessions, probate stock) see the [off-market lead generators](./api-keys). **Required scope:** `properties:read` **Cost:** 1 request per call ## Search properties ```http GET /api/v1/properties ``` Returns a paginated list of properties matching the filter criteria. ### Request ```bash curl "https://api.propaideals.co.uk/api/v1/properties?area=London&min_price=300000&max_price=500000&min_bedrooms=2&listing_type=sale&limit=20" \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ```javascript const params = new URLSearchParams({ area: 'London', min_price: '300000', max_price: '500000', min_bedrooms: '2', listing_type: 'sale', limit: '20', }) const res = await fetch( `https://api.propaideals.co.uk/api/v1/properties?${params}`, { headers: { Authorization: `Bearer ${process.env.PROPAIDEALS_API_KEY}` } } ) const { data } = await res.json() ``` ```python import requests res = requests.get( "https://api.propaideals.co.uk/api/v1/properties", params={ "area": "London", "min_price": 300_000, "max_price": 500_000, "min_bedrooms": 2, "listing_type": "sale", "limit": 20, }, headers={"Authorization": f"Bearer {API_KEY}"}, ) properties = res.json()["data"]["properties"] ``` ### Query parameters #### Location | Param | Type | Description | |---|---|---| | `area` | string | City, town, or area name (e.g. `London`, `Manchester`, `Edinburgh`) | | `postcode` | string | Full postcode or outcode (e.g. `M1 1AA` or `M1`) | | `outcode` | string | Postcode prefix only (e.g. `SW1`) | | `region` | string | UK region (e.g. `North West`, `Greater London`) | | `radius_miles` | number | Search radius around `lat` + `lon` (default `5`, max `50`) | | `lat` | number | Latitude for radius search | | `lon` | number | Longitude for radius search | #### Price | Param | Type | Description | |---|---|---| | `min_price` | integer | Minimum price (GBP) | | `max_price` | integer | Maximum price (GBP) | | `price_qualifier` | string | `asking`, `oiro` (offers in region of), `oieo`, `guide`, `from`, `auction` | #### Property attributes | Param | Type | Description | |---|---|---| | `min_bedrooms` | integer | Minimum bedrooms | | `max_bedrooms` | integer | Maximum bedrooms | | `bedrooms` | integer | Exact bedroom count | | `min_bathrooms` | integer | Minimum bathrooms | | `property_type` | string\|array | `house`, `flat`, `bungalow`, `detached`, `semi-detached`, `terraced`, `studio`, `maisonette`, `land`, `commercial` | | `tenure` | string | `freehold`, `leasehold`, `share-of-freehold`, `commonhold` | | `min_floor_area_sqft` | integer | Minimum internal area | | `max_floor_area_sqft` | integer | Maximum internal area | | `epc_rating_min` | string | Minimum EPC rating: `A`, `B`, `C`, `D`, `E`, `F`, `G` | | `council_tax_band` | string | `A`–`H` (England, Scotland) or `A`–`I` (Wales) | #### Listing type & source | Param | Type | Description | |---|---|---| | `listing_type` | string | `sale` (default) or `rent` | | `source` | string\|array | `rightmove`, `zoopla`, `onthemarket` (default: all) | | `status` | string | `active` (default), `under_offer`, `sold_stc`, `let_agreed` | #### Features (boolean filters) | Param | Type | Description | |---|---|---| | `has_garden` | boolean | Property has a garden | | `has_parking` | boolean | Off-street parking available | | `has_garage` | boolean | Garage included | | `chain_free` | boolean | Chain-free transaction | | `new_build` | boolean | New build property | | `auction` | boolean | Property going to auction | | `retirement` | boolean | Retirement property | | `shared_ownership` | boolean | Shared ownership scheme | #### Investment filters | Param | Type | Description | |---|---|---| | `min_yield` | number | Minimum gross rental yield (%) | | `max_yield` | number | Maximum gross rental yield (%) | | `min_bmv_discount` | number | Minimum below-market-value discount (%) | | `is_btl` | boolean | Suitable for buy-to-let | | `is_hmo` | boolean | HMO licensable | | `is_serviced_accommodation` | boolean | Suitable for short-term let | #### Listing age | Param | Type | Description | |---|---|---| | `max_days_on_market` | integer | Listed within the last N days | | `min_days_on_market` | integer | Stale listings older than N days | | `first_seen_after` | ISO date | Only properties first scraped after this date | #### Agent | Param | Type | Description | |---|---|---| | `agent_name` | string | Filter by estate agent name (substring match) | #### Pagination & sorting | Param | Type | Default | Description | |---|---|---|---| | `page` | integer | `1` | Page number (1-indexed) | | `limit` | integer | `20` | Results per page (max 100) | | `sort_by` | string | `newest` | `newest`, `price_asc`, `price_desc`, `highest_yield`, `best_bmv`, `best_investment` | ### Response ```json { "data": { "properties": [ { "id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde", "external_id": "rm_148273629", "source": "rightmove", "title": "3 bedroom terraced house for sale", "description": "A beautifully presented Victorian terraced house...", "url": "https://www.rightmove.co.uk/properties/148273629", "price": "£395,000", "price_numeric": 395000, "price_qualifier": "asking", "currency": "GBP", "address": "12 Example Road, Manchester", "postcode": "M1 1AA", "outcode": "M1", "area": "Manchester", "region": "North West", "country": "England", "latitude": 53.4808, "longitude": -2.2426, "bedrooms": 3, "bathrooms": 1, "reception_rooms": 2, "property_type": "terraced", "tenure": "freehold", "floor_area_sqft": 1240, "epc_rating": "C", "council_tax_band": "C", "has_garden": true, "has_parking": false, "chain_free": false, "new_build": false, "estimated_rental_pcm": 1850, "estimated_rental_yield": 5.62, "bmv_discount_percentage": 8.5, "deal_score": 78, "investment_score": 82, "agent_id": "ag_98273", "agent_name": "Example Estates", "agent_phone": "0161 123 4567", "agent_logo": "https://media.propaideals.co.uk/agents/example-estates.png", "listing_type": "sale", "status": "active", "first_seen": "2026-04-01T09:30:00Z", "last_updated": "2026-04-13T14:22:00Z", "days_on_market": 13, "main_image_url": "https://media.rightmove.co.uk/dir/propertyimg/.../IMG_00.jpg", "image_count": 24 } ], "total": 4823, "page": 1, "limit": 20, "pages": 242 }, "meta": { /* request metadata */ } } ``` ## Get property by ID ```http GET /api/v1/properties/{property_id} ``` Returns full details for a single property, including all images, the full description, agent details, transport links, nearby schools, price history, and enrichment data. ### Request ```bash curl "https://api.propaideals.co.uk/api/v1/properties/5fa1b2c3-d4e5-6f78-9012-3456789abcde" \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ```python property_id = "5fa1b2c3-d4e5-6f78-9012-3456789abcde" res = requests.get( f"https://api.propaideals.co.uk/api/v1/properties/{property_id}", headers={"Authorization": f"Bearer {API_KEY}"}, ) property = res.json()["data"] ``` ### Response Returns the same property object as the search endpoint, plus these additional fields: ```json { "data": { /* ... all search fields ... */ "images": [ { "url": "https://...", "caption": "Living room", "order": 1 }, { "url": "https://...", "caption": "Kitchen", "order": 2 } ], "floor_plans": [ { "url": "https://...", "floor_level": "ground", "sequence_order": 1 } ], "transport_data": { "nearest_station": "Manchester Piccadilly", "station_distance_miles": 0.4, "all_stations": [ { "name": "Manchester Piccadilly", "distance_miles": 0.4, "type": "national_rail" }, { "name": "Piccadilly Gardens", "distance_miles": 0.6, "type": "tram" } ] }, "nearest_schools": [ { "name": "Example Primary", "distance_miles": 0.2, "ofsted_rating": "Good", "type": "primary" }, { "name": "Example High School", "distance_miles": 0.8, "ofsted_rating": "Outstanding", "type": "secondary" } ], "price_history": [ { "date": "2026-04-01", "price": 395000, "change_type": "listed" }, { "date": "2026-04-13", "price": 379000, "change_type": "reduced" } ], "key_features": [ "Victorian terraced house", "South-facing garden", "Recently refurbished", "Walk to city centre" ] } } ``` ## Common patterns ### Find high-yield BTL properties under £200k in the Midlands ```bash curl "https://api.propaideals.co.uk/api/v1/properties?\ region=West%20Midlands&\ max_price=200000&\ min_yield=8&\ is_btl=true&\ sort_by=highest_yield&\ limit=50" \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ### Find chain-free flats in Manchester listed in the last week ```bash curl "https://api.propaideals.co.uk/api/v1/properties?\ area=Manchester&\ property_type=flat&\ chain_free=true&\ max_days_on_market=7&\ sort_by=newest" \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ### Find BMV opportunities in London (>10% discount) ```bash curl "https://api.propaideals.co.uk/api/v1/properties?\ area=London&\ min_bmv_discount=10&\ sort_by=best_bmv&\ limit=100" \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ### Page through all properties in a postcode ```python properties = [] page = 1 while True: res = requests.get( "https://api.propaideals.co.uk/api/v1/properties", params={"postcode": "SW1", "limit": 100, "page": page}, headers={"Authorization": f"Bearer {API_KEY}"}, ) body = res.json()["data"] properties.extend(body["properties"]) if page >= body["pages"]: break page += 1 ``` ## Related endpoints - **[Spatial search](./spatial)** — Viewport, polygon, and radius queries - **[Market data](./market-data)** — Sold history and comparables for a property - **[AI chat](./ai-chat)** — Natural language property search --- title: Spatial search endpoint description: PostGIS-powered spatial search for UK properties. Search by map viewport bounding box, drawn polygon, or radius around a point. Required scope spatial:read. --- # Spatial search The spatial search endpoints use PostGIS GIST indexes to return properties matching a geographic shape. Use these instead of `area=` filters when you have explicit map coordinates — they're an order of magnitude faster on large queries. **Required scope:** `spatial:read` **Cost:** 1 request per call ## Search within a viewport (bounding box) ```http POST /api/v1/spatial/search/viewport ``` Returns properties inside a rectangular bounding box. Designed for map-based UIs where users pan and zoom. ### Request ```bash curl -X POST https://api.propaideals.co.uk/api/v1/spatial/search/viewport \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "north": 51.5174, "south": 51.5074, "east": -0.1178, "west": -0.1378, "zoom": 14, "limit": 100, "filters": { "min_price": 300000, "max_price": 600000, "min_bedrooms": 2 } }' ``` ```javascript const res = await fetch('https://api.propaideals.co.uk/api/v1/spatial/search/viewport', { method: 'POST', headers: { Authorization: `Bearer ${process.env.PROPAIDEALS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ north: 51.5174, south: 51.5074, east: -0.1178, west: -0.1378, zoom: 14, limit: 100, filters: { min_price: 300000, max_price: 600000, min_bedrooms: 2, }, }), }) const { data } = await res.json() ``` ### Body parameters | Field | Type | Required | Description | |---|---|---|---| | `north` | number | yes | Maximum latitude (top edge) | | `south` | number | yes | Minimum latitude (bottom edge) | | `east` | number | yes | Maximum longitude (right edge) | | `west` | number | yes | Minimum longitude (left edge) | | `zoom` | integer | no | Map zoom level (1–20). Triggers clustering when `zoom < 14` and result count > 100. | | `limit` | integer | no | Max properties to return (default `100`, max `500`) | | `filters` | object | no | Same filter object as the [properties endpoint](./properties) | ### Response ```json { "data": { "properties": [ { "id": "5fa1b2c3-...", "title": "2 bedroom apartment for sale", "price_numeric": 425000, "latitude": 51.5124, "longitude": -0.1278, "bedrooms": 2, /* ... full property fields ... */ } ], "total": 187, "viewport": { "north": 51.5174, "south": 51.5074, "east": -0.1178, "west": -0.1378 }, "clustered": false } } ``` When `clustered` is `true`, the response includes cluster centroids instead of individual properties — zoom in to see them. ## Search within a drawn polygon ```http POST /api/v1/spatial/search/polygon/draw ``` Returns properties inside an arbitrary polygon. Use this for "draw on map" UIs. ### Request ```bash curl -X POST https://api.propaideals.co.uk/api/v1/spatial/search/polygon/draw \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "polygon_coordinates": [ [-0.138, 51.517], [-0.118, 51.517], [-0.118, 51.507], [-0.138, 51.507], [-0.138, 51.517] ], "limit": 100, "filters": { "max_price": 500000 } }' ``` ### Body parameters | Field | Type | Required | Description | |---|---|---|---| | `polygon_coordinates` | array | yes* | Array of `[longitude, latitude]` pairs. Min 3, max 20 vertices. First and last point should match (closed ring). | | `polygon_wkt` | string | yes* | Alternative: WKT-encoded polygon string | | `limit` | integer | no | Max properties (default `100`, max `500`) | | `filters` | object | no | Same filter object as the properties endpoint | \* Provide either `polygon_coordinates` or `polygon_wkt`, not both. > Coordinates are `[longitude, latitude]` (GeoJSON order), **not** `[lat, lon]`. This trips up most first-time users. ## Search within a radius ```http POST /api/v1/spatial/search/radius ``` Returns properties within `radius_miles` of a centre point. Equivalent to `GET /api/v1/properties?lat=...&lon=...&radius_miles=...` but slightly faster for large radii. ### Request ```bash curl -X POST https://api.propaideals.co.uk/api/v1/spatial/search/radius \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lat": 51.5074, "lon": -0.1278, "radius_miles": 2, "limit": 100, "filters": { "min_bedrooms": 3, "max_price": 800000 } }' ``` ### Body parameters | Field | Type | Required | Description | |---|---|---|---| | `lat` | number | yes | Centre latitude | | `lon` | number | yes | Centre longitude | | `radius_miles` | number | yes | Search radius (max `25`) | | `limit` | integer | no | Max properties (default `100`, max `500`) | | `filters` | object | no | Same filter object as the properties endpoint | ### Response Same shape as viewport search, plus a `distance_meters` field on each property: ```json { "data": { "properties": [ { "id": "5fa1b2c3-...", "title": "3 bedroom house for sale", "latitude": 51.5095, "longitude": -0.1290, "distance_meters": 230, /* ... */ } ], "total": 412 } } ``` Properties are sorted by ascending distance from the centre point. ## Common patterns ### Find all properties around a London tube station ```python # Bond Street station res = requests.post( "https://api.propaideals.co.uk/api/v1/spatial/search/radius", json={ "lat": 51.5142, "lon": -0.1494, "radius_miles": 0.5, "filters": {"listing_type": "rent", "min_bedrooms": 1}, "limit": 200, }, headers={"Authorization": f"Bearer {API_KEY}"}, ) ``` ### Search a custom catchment area ```python # A school catchment polygon (5 vertices) catchment = [ [-2.245, 53.485], [-2.235, 53.486], [-2.234, 53.479], [-2.247, 53.478], [-2.245, 53.485], # close the ring ] res = requests.post( "https://api.propaideals.co.uk/api/v1/spatial/search/polygon/draw", json={"polygon_coordinates": catchment, "limit": 500}, headers={"Authorization": f"Bearer {API_KEY}"}, ) ``` --- title: Market data endpoint description: Sold history, on-street comparables, and price trends for UK properties. Combines HM Land Registry full price-paid data with multi-portal sold listings via PostGIS spatial matching. Required scope market-data:read. --- # Market data The market data endpoints return historical sale prices, comparables, and price trends for any UK property. Data is sourced from **HM Land Registry Price Paid Data** (31M+ transactions, 1995–present) and aggregated multi-portal sold listings, joined via PostGIS spatial matching. **Required scope:** `market-data:read` **Cost:** 1 request per call ## Get sold history for a property ```http GET /api/v1/market-data/{property_id}/sold-history ``` Returns historical sale records for a specific property, matched via three priorities: exact UPRN match, PostGIS spatial match, and Land Registry building-number matching. ### Request ```bash curl https://api.propaideals.co.uk/api/v1/market-data/5fa1b2c3-d4e5-6f78-9012-3456789abcde/sold-history \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ```python property_id = "5fa1b2c3-d4e5-6f78-9012-3456789abcde" res = requests.get( f"https://api.propaideals.co.uk/api/v1/market-data/{property_id}/sold-history", headers={"Authorization": f"Bearer {API_KEY}"}, ) history = res.json()["data"] ``` ### Response ```json { "data": { "property_id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde", "current_price": 395000, "sold_records": [ { "sale_date": "2018-06-15", "sale_price": 285000, "source": "land_registry", "match_confidence": "exact_uprn", "property_type": "T", "tenure": "F", "new_build": false }, { "sale_date": "2010-09-22", "sale_price": 192000, "source": "land_registry", "match_confidence": "spatial_paon", "property_type": "T", "tenure": "F", "new_build": false }, { "sale_date": "2003-04-11", "sale_price": 145000, "source": "land_registry", "match_confidence": "spatial_paon", "property_type": "T", "tenure": "F", "new_build": false } ], "summary": { "first_sale_date": "2003-04-11", "first_sale_price": 145000, "last_sale_date": "2018-06-15", "last_sale_price": 285000, "total_appreciation_pct": 96.55, "annualised_appreciation_pct": 4.5 } } } ``` ### Match confidence values | Value | Meaning | |---|---| | `exact_uprn` | UPRN exact match (highest confidence) | | `spatial_uprn` | PostGIS match within 25m + UPRN agreement | | `spatial_paon` | PostGIS match within 25m + building number agreement | | `spatial_only` | PostGIS match within 25m, no building number | | `address_match` | Address string match | ## Get on-street comparables ```http GET /api/v1/market-data/{property_id}/on-street ``` Returns comparable sales on the **same street** as the target property. Useful for valuation work where you want to anchor against truly comparable houses. ### Request ```bash curl "https://api.propaideals.co.uk/api/v1/market-data/5fa1b2c3-d4e5-6f78-9012-3456789abcde/on-street?max_age_years=5&limit=20" \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ### Query parameters | Param | Type | Default | Description | |---|---|---|---| | `max_age_years` | integer | `5` | Only return sales within the last N years | | `limit` | integer | `20` | Max comparables (max `100`) | | `min_bedrooms` | integer | — | Only matching bedroom count | | `max_bedrooms` | integer | — | | | `property_type` | string | — | Filter by `D`/`S`/`T`/`F` (Detached/Semi/Terrace/Flat) | | `radius_meters` | integer | `100` | Search radius from target | ### Response ```json { "data": { "property_id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde", "street_name": "Example Road", "comparables": [ { "address": "14 Example Road, Manchester", "sale_date": "2025-11-22", "sale_price": 380000, "property_type": "T", "bedrooms": 3, "distance_meters": 18, "source": "land_registry", "price_per_sqft": 306 }, { "address": "8 Example Road, Manchester", "sale_date": "2025-08-04", "sale_price": 365000, "property_type": "T", "bedrooms": 3, "distance_meters": 32, "source": "land_registry", "price_per_sqft": 294 } ], "summary": { "comparables_count": 12, "median_price": 372500, "average_price": 374100, "median_price_per_sqft": 301, "lowest_sale": 320000, "highest_sale": 425000 } } } ``` ## Get area price trends ```http GET /api/v1/market-data/area/{outcode}/trends ``` Returns rolling 1yr / 3yr / 5yr price trends for a postcode outcode. Useful for area-level dashboards. ### Request ```bash curl https://api.propaideals.co.uk/api/v1/market-data/area/M1/trends \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" ``` ### Response ```json { "data": { "outcode": "M1", "city": "Manchester", "median_price_current": 245000, "1yr": { "median_price": 232000, "change_pct": 5.6, "transactions": 487 }, "3yr": { "median_price": 198000, "change_pct": 23.7, "transactions": 1632 }, "5yr": { "median_price": 174000, "change_pct": 40.8, "transactions": 2841 }, "by_property_type": { "F": { "median_price": 185000, "1yr_change_pct": 4.2 }, "T": { "median_price": 295000, "1yr_change_pct": 6.8 }, "S": { "median_price": 320000, "1yr_change_pct": 7.1 }, "D": { "median_price": 485000, "1yr_change_pct": 5.3 } } } } ``` ## Common patterns ### Estimate appreciation since purchase ```python res = requests.get( f"https://api.propaideals.co.uk/api/v1/market-data/{property_id}/sold-history", headers={"Authorization": f"Bearer {API_KEY}"}, ).json()["data"] if res["sold_records"]: last_sale = res["sold_records"][0] current = res["current_price"] appreciation = (current - last_sale["sale_price"]) / last_sale["sale_price"] * 100 print(f"Appreciated {appreciation:.1f}% since {last_sale['sale_date']}") ``` ### Build a quick comparables-based valuation ```python res = requests.get( f"https://api.propaideals.co.uk/api/v1/market-data/{property_id}/on-street", params={"max_age_years": 2, "min_bedrooms": 3, "max_bedrooms": 3}, headers={"Authorization": f"Bearer {API_KEY}"}, ).json()["data"] property_sqft = 1240 # from the property detail endpoint estimated_value = res["summary"]["median_price_per_sqft"] * property_sqft ``` --- title: EPC certificates endpoint description: Look up Energy Performance Certificate (EPC) data for UK properties by postcode, UPRN, or Prop AI Deals property ID. Joins HMG EPC register data with our property table for instant address resolution. --- # EPC certificates The EPC endpoints return Energy Performance Certificate data for UK properties. Data is sourced from the [HMG EPC register](https://www.gov.uk/government/collections/energy-performance-certificates-epcs) and matched to live listings via UPRN and PostGIS spatial joins. **Required scope:** `epc:read` **Cost:** 1 request per call **Available on:** Starter, Professional, Business ## Get EPCs for a postcode ```http GET /api/v1/epc/postcode/{postcode} ``` Returns up to 100 EPC records for properties in the given postcode, ordered by EPC date desc. ### Request ```bash curl https://api.propaideals.co.uk/api/v1/epc/postcode/M1%201AA?limit=10 \ -H "Authorization: Bearer paid_..." ``` ```python import requests res = requests.get( "https://api.propaideals.co.uk/api/v1/epc/postcode/M1 1AA", params={"limit": 10}, headers={"Authorization": f"Bearer {API_KEY}"}, ) records = res.json()["data"]["records"] ``` ### Query parameters | Param | Type | Default | Max | Description | |---|---|---|---|---| | `limit` | integer | 20 | 100 | Max records to return | ### Response ```json { "data": { "count": 10, "records": [ { "property_id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde", "energy_rating": "C", "epc_score": 71, "epc_url": "https://find-energy-certificate.service.gov.uk/...", "epc_lmk_key": "abc123def456...", "epc_date": "2024-08-15", "construction_age_band": "1976-1982", "sqft_estimated": false, "uprn": 100012345678, "address": "12 Example Road, Manchester", "postcode": "M1 1AA" } ] } } ``` ## Get EPC by UPRN ```http GET /api/v1/epc/uprn/{uprn} ``` Direct lookup by Unique Property Reference Number — the most reliable identifier for a UK property. ### Request ```bash curl https://api.propaideals.co.uk/api/v1/epc/uprn/100012345678 \ -H "Authorization: Bearer paid_..." ``` Returns the same `EpcRecord` shape as above, or `404` if no EPC matches that UPRN. ## Get EPC by property ID ```http GET /api/v1/epc/property/{property_id} ``` Look up the EPC linked to a specific Prop AI Deals property UUID (returned from the [properties endpoint](./properties)). ```bash curl https://api.propaideals.co.uk/api/v1/epc/property/5fa1b2c3-d4e5-6f78-9012-3456789abcde \ -H "Authorization: Bearer paid_..." ``` ## Field reference | Field | Type | Description | |---|---|---| | `property_id` | UUID | Internal Prop AI Deals property identifier | | `energy_rating` | string | EPC band: `A` (best) – `G` (worst) | | `epc_score` | integer | EPC numerical score 1–100 | | `epc_url` | string | Direct link to the official HMG certificate PDF | | `epc_lmk_key` | string | LMK certificate key (use to query MHCLG datasets directly) | | `epc_date` | ISO date | Date the certificate was issued | | `construction_age_band` | string | Era the property was built (e.g. `1976-1982`) | | `sqft_estimated` | boolean | `true` if `floor_area_sqft` was derived from the EPC certificate | | `uprn` | integer | Unique Property Reference Number (12-digit) | | `address` | string | Full property address | | `postcode` | string | Postcode | ## Use cases - **Mortgage origination:** Fetch EPC for a property before underwriting (lenders increasingly require minimum EPC C from 2030) - **Refurb planning:** Identify F/G rated properties in a postcode for retrofit business - **Compliance:** Landlords need EPC C+ to let from 2028 — bulk-check a portfolio - **Investment scoring:** EPC band correlates with future MEES compliance cost ## Related endpoints - **[UPRN lookup](./uprn-lookup)** — Resolve a UPRN to property metadata - **[Properties](./properties)** — Search live listings (returns the property UUIDs you'd pass here) --- title: UPRN lookup endpoint description: Resolve a Unique Property Reference Number (UPRN) to property metadata. Returns address, postcode, energy rating, construction era, and the linked Prop AI Deals property UUID if a match exists. --- # UPRN lookup A **UPRN** (Unique Property Reference Number) is a 12-digit identifier maintained by Ordnance Survey that uniquely identifies every land and property record in Great Britain. UPRNs are stable across the lifetime of a property and are the canonical way to reference a specific address. This endpoint resolves a UPRN to whatever metadata Prop AI Deals has on file — typically EPC certificate data plus a linked live listing if the property is currently on market. **Required scope:** `epc:read` **Cost:** 1 request per call **Available on:** Starter, Professional, Business ## Look up a UPRN ```http GET /api/v1/uprn/{uprn} ``` ### Request ```bash curl https://api.propaideals.co.uk/api/v1/uprn/100012345678 \ -H "Authorization: Bearer paid_..." ``` ```python import requests uprn = 100012345678 res = requests.get( f"https://api.propaideals.co.uk/api/v1/uprn/{uprn}", headers={"Authorization": f"Bearer {API_KEY}"}, ) result = res.json()["data"] if result["matched"]: print(f"{result['address']}, {result['postcode']}") print(f"EPC: {result['energy_rating']}") ``` ### Response (matched) ```json { "data": { "uprn": 100012345678, "matched": true, "property_id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde", "address": "12 Example Road, Manchester", "postcode": "M1 1AA", "energy_rating": "C", "construction_age_band": "1976-1982", "source": "epc_register" } } ``` ### Response (no match) ```json { "data": { "uprn": 999999999999, "matched": false, "property_id": null, "address": null, "postcode": null, "energy_rating": null, "construction_age_band": null, "source": null } } ``` The endpoint returns `200` even when no match exists — `matched: false` is the signal. This makes batch UPRN validation easier than handling 404s. ## Field reference | Field | Type | Description | |---|---|---| | `uprn` | integer | The UPRN you queried (echoed back) | | `matched` | boolean | Whether we found a record for this UPRN | | `property_id` | UUID \| null | Internal Prop AI Deals property UUID (use with [properties endpoint](./properties)) | | `address` | string \| null | Best available address — from live listing if linked, else from EPC register | | `postcode` | string \| null | Postcode | | `energy_rating` | string \| null | EPC band `A`–`G` | | `construction_age_band` | string \| null | Era the property was built | | `source` | string \| null | Where the metadata came from (`epc_register`, `os_addressbase`, etc.) | ## Use cases - **Conveyancing checks** — Validate a UPRN supplied by a buyer's solicitor against actual property metadata - **Mortgage origination** — Resolve a property reference to EPC for compliance checks - **Bulk address normalisation** — Run a list of UPRNs through and get clean addresses + postcodes - **Cross-referencing** — Match HM Land Registry sale records (which include UPRN since April 2025) to live listings ## Match confidence UPRN matches in our database come from multiple sources with varying confidence: | Source | Confidence | Notes | |---|---|---| | `epc_register` | High | Direct match in HMG EPC register | | `os_addressbase` | High | Ordnance Survey AddressBase Premium | | `spatial_paon` | Medium | Spatial join + house number match | | `address_match` | Medium | Address string fuzzy match | Use `source` to decide how much to trust the match in critical workflows. ## Related endpoints - **[EPC certificates](./epc)** — Get the full EPC record once you have the UPRN - **[Properties](./properties)** — Get the live listing details using `property_id` --- title: Area aggregates (yields, rents, agents) description: Aggregate rental yields, rental prices, and active estate agent listings across UK postcode outcodes. For investor area-screening and competitive analysis. --- # Area aggregates Three endpoints for area-level analytics. All scoped per postcode outcode (e.g. `M1`, `SW1`, `EH4`). **Available on:** Starter, Professional, Business **Cost:** 1 request per call ## Yields by area ```http GET /api/v1/yields/area/{outcode} ``` **Required scope:** `areas:read` Aggregates gross rental yield across all sale listings in an outcode. Returns mean, median, p25, and p75 alongside the underlying sample size. ### Request ```bash curl https://api.propaideals.co.uk/api/v1/yields/area/M1 \ -H "Authorization: Bearer paid_..." ``` ### Response ```json { "data": { "area": "M1", "sample_size": 423, "average_gross_yield_pct": 6.82, "median_gross_yield_pct": 6.40, "p25_gross_yield_pct": 5.10, "p75_gross_yield_pct": 8.20, "average_price": 215000, "average_monthly_rent": 1185 } } ``` Yields above 25% are excluded as data quality outliers (typically a missing decimal point in the source rent estimate). If `sample_size` is `0`, the area has no live sale listings with usable rent estimates — try a wider outcode or use the [search endpoint](./properties) directly. ## Rents by area ```http GET /api/v1/rents/area/{outcode} ``` **Required scope:** `areas:read` Aggregates monthly rents across all active rental listings in an outcode. Returns overall stats plus a per-bedroom breakdown. ### Request ```bash curl https://api.propaideals.co.uk/api/v1/rents/area/M1 \ -H "Authorization: Bearer paid_..." ``` ### Response ```json { "data": { "area": "M1", "sample_size": 1247, "average_monthly_rent": 1450, "median_monthly_rent": 1325, "p25_monthly_rent": 1100, "p75_monthly_rent": 1700, "by_bedrooms": { "1": {"sample_size": 312, "average_monthly_rent": 1050, "median_monthly_rent": 1000}, "2": {"sample_size": 542, "average_monthly_rent": 1380, "median_monthly_rent": 1325}, "3": {"sample_size": 287, "average_monthly_rent": 1825, "median_monthly_rent": 1750}, "4": {"sample_size": 89, "average_monthly_rent": 2400, "median_monthly_rent": 2300}, "5": {"sample_size": 17, "average_monthly_rent": 3250, "median_monthly_rent": 3100} } } } ``` ## Active estate agents ```http GET /api/v1/agents-list/area/{outcode} ``` **Required scope:** `areas:read` Returns the most active estate agents in an outcode, ranked by active listing count. Useful for competitive intelligence and agent outreach. ### Request ```bash curl "https://api.propaideals.co.uk/api/v1/agents-list/area/M1?limit=20" \ -H "Authorization: Bearer paid_..." ``` ### Query parameters | Param | Type | Default | Max | |---|---|---|---| | `limit` | integer | 20 | 100 | ### Response ```json { "data": { "area": "M1", "count": 20, "agents": [ { "name": "Savills - Manchester", "branch_id": "rm_BRANCH_12345", "company_name": "Savills", "phone": "0161 123 4567", "listing_count": 187 } ] } } ``` ## Use cases - **Investor area screening** — Filter outcodes by minimum yield or rent before drilling into individual properties - **Market intelligence dashboards** — Show clients the rent/yield landscape for an area at a glance - **Outreach lists** — Pull the top 20 active agents in a target area for off-market BD - **Comparable rent assessments** — Per-bedroom medians for valuation appeals or tenant-side disputes ## Related endpoints - **[Properties](./properties)** — Drill down into individual listings filtered by outcode - **[Market data](./market-data)** — Sold prices and historical trends - **[Spatial search](./spatial)** — When you want shapes, not postcodes --- title: Demographics endpoint description: Population, household income, employment rate, crime rating, and ONS deprivation index for any UK postcode district. Sourced from ONS census, ONS deprivation index, and police.uk. --- # Demographics Returns area-level demographic data for any UK postcode district (e.g. `M1`, `SW1`, `EH4`). Combines ONS census, ONS Index of Multiple Deprivation, and police.uk crime statistics into a single record per district. Refreshed quarterly. **Required scope:** `areas:read` **Cost:** 1 request per call **Available on:** Starter, Professional, Business ## Get demographics for a district ```http GET /api/v1/demographics/{postcode_district} ``` ### Request ```bash curl https://api.propaideals.co.uk/api/v1/demographics/M1 \ -H "Authorization: Bearer paid_..." ``` ```python import requests res = requests.get( "https://api.propaideals.co.uk/api/v1/demographics/M1", headers={"Authorization": f"Bearer {API_KEY}"}, ) demo = res.json()["data"] print(f"Population: {demo['population']:,}") print(f"Median income: £{demo['median_income']:,.0f}") print(f"Crime rating: {demo['crime_rating']}") print(f"Deprivation decile: {demo['deprivation_decile']}/10 (1 = most deprived)") ``` ### Response ```json { "data": { "postcode_district": "M1", "population": 28412, "households": 14256, "median_age": 28.4, "median_income": 31250.50, "employment_rate_pct": 76.20, "crime_rate": 89.45, "crime_rating": "High", "deprivation_score": 42, "deprivation_decile": 3, "data_source": "ons_census_2021", "data_date": "2024-12-15" } } ``` ### Field reference | Field | Type | Description | |---|---|---| | `postcode_district` | string | The district queried (echoed back) | | `population` | integer | Total population (ONS census) | | `households` | integer | Total households | | `median_age` | float | Median resident age | | `median_income` | float | Median household income (£) | | `employment_rate_pct` | float | % of working-age residents in employment | | `crime_rate` | float | Crimes per 1,000 population per year | | `crime_rating` | string | `Very Low` / `Low` / `Average` / `High` / `Very High` | | `deprivation_score` | integer | ONS IMD score (higher = more deprived) | | `deprivation_decile` | integer | 1 (most deprived 10%) – 10 (least deprived 10%) | | `data_source` | string | Source dataset identifier | | `data_date` | ISO date | When the data was last refreshed | ## Use cases - **Investor area screening** — Combine median income + deprivation decile to find improving areas - **Target tenant matching** — Match the demographic profile to your rental stock (median age, income) - **Risk underwriting** — Crime rating and deprivation decile feed into BTL underwriting models - **Marketing segmentation** — Build campaigns around districts with specific demographics ## Coverage 742 UK postcode districts have demographic records loaded today. We're adding more from the ONS Census 2021 release as data becomes available. If a district returns `404`, email `api@propaideals.co.uk` and we'll prioritise loading it. ## Related endpoints - **[Area aggregates (yields/rents/agents)](./area-aggregates)** — Investment data for the same areas - **[Heritage status](./heritage)** — Listed building & conservation area data - **[Flood risk](./flood-risk)** — Flood zone overlap by point or property --- title: Flood risk endpoint description: Check whether a UK property or coordinate falls inside a flood risk zone. PostGIS spatial check against 144,000+ flood zone polygons sourced from EA flood map and SEPA. --- # Flood risk Check whether a UK location is inside a flood risk zone. Backed by **144,000+ flood zone polygons** stored as PostGIS geometries, sourced from the Environment Agency flood map (England & Wales) and SEPA (Scotland). **Required scope:** `planning:read` **Cost:** 1 request per call **Available on:** Starter, Professional, Business ## Check by coordinates ```http GET /api/v1/flood-risk/coordinates?lat={lat}&lon={lon} ``` PostGIS `ST_Contains` check against the `flood_zones` table. If the point falls inside multiple overlapping zones, returns the highest-risk match. ### Request ```bash curl "https://api.propaideals.co.uk/api/v1/flood-risk/coordinates?lat=53.4808&lon=-2.2426" \ -H "Authorization: Bearer paid_..." ``` ```python import requests res = requests.get( "https://api.propaideals.co.uk/api/v1/flood-risk/coordinates", params={"lat": 53.4808, "lon": -2.2426}, headers={"Authorization": f"Bearer {API_KEY}"}, ) risk = res.json()["data"] if risk["in_flood_zone"]: print(f"At risk: {risk['risk_level']} ({risk['zone_category']})") else: print("Outside any flood zone") ``` ### Response (in zone) ```json { "data": { "lat": 53.4808, "lon": -2.2426, "in_flood_zone": true, "risk_level": "high", "zone_category": "3a", "zone_type": "rivers", "source": "ea_flood_map" } } ``` ### Response (no risk) ```json { "data": { "lat": 53.4808, "lon": -2.2426, "in_flood_zone": false, "risk_level": null, "zone_category": null, "zone_type": null, "source": null } } ``` The endpoint returns `200` even when the point isn't in any zone — `in_flood_zone: false` is the signal. ## Check by property ID ```http GET /api/v1/flood-risk/property/{property_id} ``` Looks up the property's stored coordinates and runs the same check. Honours the precomputed `is_flood_zone` flag if it's already set on the property row, falling back to the spatial query otherwise. ```bash curl "https://api.propaideals.co.uk/api/v1/flood-risk/property/5fa1b2c3-d4e5-6f78-9012-3456789abcde" \ -H "Authorization: Bearer paid_..." ``` Returns the same `FloodRiskResponse` shape. ## Risk levels | Level | Zone category | Annual probability | What it means | |---|---|---|---| | `high` | 3a, 3b | > 1.0% (rivers/sea) | Significant flood risk — insurance premiums materially higher, lender restrictions possible | | `medium` | 2 | 0.1–1.0% | Moderate risk — disclose on conveyancing | | `low` | 1 | < 0.1% | Low risk | | _(none)_ | — | — | Not in any mapped zone | ## Use cases - **Conveyancing checks** — Pre-flag flood risk before instructing a search - **Mortgage underwriting** — Lenders can decline or surcharge high-risk properties - **Insurance scoring** — Quote adjustments based on flood zone overlap - **Investor due diligence** — Filter portfolio acquisitions by flood risk - **Refurb planning** — Avoid putting flood-vulnerable improvements in zone 3 properties ## Coverage | Region | Coverage | Source | |---|---|---| | England | All flood zones 2 + 3 | EA flood map | | Wales | All flood zones | NRW flood map | | Scotland | All categories | SEPA flood maps | | Northern Ireland | Limited | DfI Rivers | Total: **144,237 polygon records** across all UK home nations. ## Related endpoints - **[Heritage status](./heritage)** — Listed building & conservation area data (often correlated with flood risk) - **[Properties](./properties)** — Use the `is_flood_zone=false` filter to exclude flood-risk listings entirely - **[Spatial search](./spatial)** — Combine viewport queries with flood-risk filtering --- title: Heritage endpoint description: Listed building and conservation area status for any UK property. Plus area-level aggregates so you can see how heritage-restricted a postcode outcode is at a glance. --- # Heritage status Check whether a UK property is a **listed building** or sits inside a **conservation area** — both of which materially restrict what alterations are allowed without planning permission. Data is sourced from Historic England and local authority planning records. **Required scope:** `planning:read` **Cost:** 1 request per call **Available on:** Starter, Professional, Business ## Get heritage status for a property ```http GET /api/v1/heritage/property/{property_id} ``` ### Request ```bash curl https://api.propaideals.co.uk/api/v1/heritage/property/5fa1b2c3-d4e5-6f78-9012-3456789abcde \ -H "Authorization: Bearer paid_..." ``` ### Response ```json { "data": { "property_id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde", "is_listed_building": false, "in_conservation_area": true, "address": "12 Example Road, Manchester", "postcode": "M1 1AA" } } ``` | Field | Type | Description | |---|---|---| | `is_listed_building` | boolean | Property is on Historic England's National Heritage List (Grade I, II*, or II) | | `in_conservation_area` | boolean | Property sits within a designated conservation area | | `address` | string | Property address | | `postcode` | string | Postcode | ## Heritage aggregates by area ```http GET /api/v1/heritage/area/{outcode} ``` Returns counts of listed buildings and conservation properties in an outcode plus a small sample of each. Useful for **due diligence on heritage-heavy areas** where extension permissions are tightly restricted. ### Request ```bash curl https://api.propaideals.co.uk/api/v1/heritage/area/SW1 \ -H "Authorization: Bearer paid_..." ``` ### Response ```json { "data": { "outcode": "SW1", "listed_buildings_count": 287, "conservation_count": 1842, "sample_listed": [ { "id": "5fa1b2c3-...", "address": "12 Heritage Square, London", "postcode": "SW1A 1AA", "price_numeric": 4200000 } ], "sample_conservation": [ { "id": "9c8d7e6f-...", "address": "5 Conservation Way, London", "postcode": "SW1A 2BB", "price_numeric": 1850000 } ] } } ``` ## Why this matters | Property status | Restrictions | |---|---| | **Grade I listed** | Almost no alterations without listed building consent. Repair like-for-like. Highest restriction. | | **Grade II* listed** | Major alterations need consent. Internal works often restricted too. | | **Grade II listed** | Most listed UK properties. Exterior changes need consent; some internal protected. | | **Conservation area** | Permitted development rights restricted. Article 4 directions may apply. Tree works need notice. | | **Both** | Worst case — listed AND in a conservation area means even repairs need approvals. | For investors, this affects: - **Refurb scope** — UPVC windows, render, extensions can be blocked - **Build cost** — Specialist contractors + consents add 30–60% to refurb budgets - **Resale value** — Premium for "tasteful restoration", penalty for unsympathetic works - **HMO conversion** — Often blocked or heavily restricted ## Coverage We have heritage flags pre-computed on **5,193 listed building** records and **14,770 conservation area** records across our 600,000+ live property dataset. Coverage is best in England (where Historic England data is most complete) and growing in Scotland (Historic Environment Scotland) and Wales (Cadw). ## Use cases - **Pre-offer due diligence** — Don't waste solicitor time on listed properties you can't afford to refurb to spec - **HMO sourcing** — Filter out conservation areas where HMO conversion is blocked - **Refurb cost modelling** — Add a 30–60% premium to estimates when listed - **Insurance underwriting** — Listed buildings need specialist policies - **Auction prep** — Spot heritage restrictions before bidding ## Related endpoints - **[Planning](./properties)** — `/api/v1/planning/{property_id}/heritage-status`, `/api/v1/planning/{property_id}/article4` for deeper checks - **[Demographics](./demographics)** — Area context for heritage-rich districts - **[Properties](./properties)** — Filter live listings by `is_listed_building` / `in_conservation_area` --- title: Build cost endpoint description: Search Spon's Architects' & Builders' Price Book 2026 for UK construction and refurb cost data. Hybrid vector + full-text search across 68 chunks of the canonical industry reference. --- # Build cost search Search the canonical UK construction price reference — **Spon's Architects' and Builders' Price Book 2026** — for ballpark build, refurb, and material costs. Powered by hybrid vector + full-text search over 68 chunks of the book in our `propai_rag` pgvector database. **Required scope:** `build-cost:read` **Cost:** 1 request per call **Available on:** Professional, Business ## Search build costs ```http GET /api/v1/build-cost/search?q={query} ``` ### Request ```bash curl "https://api.propaideals.co.uk/api/v1/build-cost/search?q=loft+conversion+per+sqm" \ -H "Authorization: Bearer paid_..." ``` ```python import requests res = requests.get( "https://api.propaideals.co.uk/api/v1/build-cost/search", params={"q": "kitchen extension cost per sqm", "limit": 5}, headers={"Authorization": f"Bearer {API_KEY}"}, ) matches = res.json()["data"]["matches"] for m in matches: print(f"{m['title']}\n{m['snippet']}\n") ``` ### Query parameters | Param | Type | Default | Max | Description | |---|---|---|---|---| | `q` | string | _(required)_ | — | Search query — try industry terms like "rebuild cost", "rcc beam", "thermal insulation" | | `limit` | integer | 5 | 20 | Max matches to return | ### Response ```json { "data": { "query": "loft conversion per sqm", "count": 3, "matches": [ { "title": "Loft conversions — Section 4.2", "snippet": "Loft conversion with new dormer window, joists, insulation, and stair to comply with Building Regulations Part B. Average cost per square metre of usable floor area: £1,800 – £2,400 in Greater London, £1,400 – £1,900 elsewhere in the UK...", "relevance": 0.0892, "source": "Spon's Architects' and Builders' Price Book 2026", "metadata": { "page": 412, "section": "4.2" } } ], "disclaimer": "Build cost data is sourced from Spon's Architects' and Builders' Price Book 2026. Use as a directional guide only — always commission a quantity surveyor for actual project pricing." } } ``` ## What's covered The full Spon's 2026 reference includes pricing for: - **New build construction** (per sqm by build type and region) - **Refurbishment** (kitchens, bathrooms, extensions, conversions) - **Materials** (concrete, steel, timber, brick, glazing, insulation) - **Trades** (bricklaying, electrical, plumbing, decoration) - **External works** (drives, paving, fencing, landscaping) - **Mechanical & electrical** (boilers, heat pumps, solar PV, wiring) - **Demolition and groundworks** - **Professional fees** (architect, QS, structural engineer) - **Regional adjustments** (London/SE/SW/Midlands/North) ## Important caveats > ⚠️ **This is a directional pricing tool, NOT a substitute for a quantity surveyor.** > > Spon's prices are list-prices for typical projects — actual build costs vary by site conditions, contractor relationships, market timing, and specification. For any project where you're committing real capital, get at least three quotes from local builders and engage a chartered QS. The disclaimer is included on every response so downstream apps can show it to end users. ## Use cases - **Refurbishment ROI modeling** — Estimate the cost side of a flip/BRRR before submitting an offer - **Development feasibility** — Quick GDV vs build cost check before paying for a full appraisal - **Insurance valuations** — Rebuild cost lookups for buildings insurance (use the dedicated `/rebuild-cost` query) - **Investor reports** — Pull cost ranges into automated investment summaries ## Related endpoints - **[Investment calculators](./investment)** — Strategy modeling (BRRR/Flip) that consumes build cost as an input - **[Properties](./properties)** — The property listings you'd be costing up --- title: AI chat endpoint description: Conversational property intelligence powered by GPT-4.1. Natural language search, multi-turn conversations, sub-3-second responses with property cards and investment analysis. Required scope ai:chat. --- # AI chat The AI chat endpoint exposes the same Ultimate AI System that powers the propaideals.co.uk web app — a multi-agent system built on GPT-4.1 that can search 600,000+ live UK properties, run investment analysis, retrieve sold history, and answer property questions in natural language. **Required scope:** `ai:chat` **Cost:** 5 requests per call (against your monthly request quota), plus 1 AI chat call (against your AI chat quota) ## Send a chat message ```http POST /api/v1/ultimate-ai/chat ``` ### Request ```bash curl -X POST https://api.propaideals.co.uk/api/v1/ultimate-ai/chat \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Show me 3-bed houses under £400k in Manchester with at least 7% rental yield", "session_id": "sess_my_user_123" }' ``` ```javascript const res = await fetch('https://api.propaideals.co.uk/api/v1/ultimate-ai/chat', { method: 'POST', headers: { Authorization: `Bearer ${process.env.PROPAIDEALS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ message: 'Show me 3-bed houses under £400k in Manchester with at least 7% rental yield', session_id: 'sess_my_user_123', }), }) const { data } = await res.json() console.log(data.response_text) console.log(`${data.properties.length} properties found`) ``` ```python res = requests.post( "https://api.propaideals.co.uk/api/v1/ultimate-ai/chat", json={ "message": "Show me 3-bed houses under £400k in Manchester with at least 7% rental yield", "session_id": "sess_my_user_123", }, headers={"Authorization": f"Bearer {API_KEY}"}, ) body = res.json()["data"] print(body["response_text"]) for prop in body["properties"]: print(f" - {prop['address']}: £{prop['price_numeric']:,} ({prop['estimated_rental_yield']}% yield)") ``` ### Body parameters | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | The user's natural-language message (max 4,000 chars) | | `session_id` | string | no | Session identifier for multi-turn conversations. Reuse to maintain context. Auto-generated if omitted. | | `context` | object | no | Optional structured context (e.g. user's investor profile) | | `top_k` | integer | no | Max properties to return (default `5`, max `20`) | ### Response ```json { "data": { "session_id": "sess_my_user_123", "message_id": "msg_a1b2c3d4", "response_text": "I found 12 three-bedroom houses in Manchester under £400,000 with rental yields above 7%. The strongest deals are in M9 and M40, where you can find Victorian terraces yielding 8–9% gross. Here are the top picks:", "intent": "property_search", "properties": [ { "id": "5fa1b2c3-...", "title": "3 bedroom terraced house for sale", "address": "12 Example Road, Manchester M9", "price_numeric": 285000, "bedrooms": 3, "estimated_rental_yield": 8.4, "bmv_discount_percentage": 6.2, "deal_score": 81, /* ... full property fields ... */ "ai_insight": "Strong yield, BMV discount of 6%, and on a street with rising values. Worth a viewing." } /* ... more properties ... */ ], "follow_up_suggestions": [ "Show me more in M40", "What about HMO-suitable properties?", "Compare the top 3 deals side by side" ], "agents_used": ["intent_classifier", "search_agent", "analysis_agent", "response_agent"], "latency_ms": 2840 } } ``` ## Multi-turn conversations Pass the same `session_id` on subsequent requests to maintain context. The AI remembers the previous turn and can interpret references like "show me more", "what about cheaper ones", or "tell me about the first property". ```python session_id = "sess_user_123_2026_04_14" # Turn 1 requests.post( "https://api.propaideals.co.uk/api/v1/ultimate-ai/chat", json={"message": "Find me high-yield BTL flats in Liverpool", "session_id": session_id}, headers={"Authorization": f"Bearer {API_KEY}"}, ) # Turn 2 — refers back to the first turn requests.post( "https://api.propaideals.co.uk/api/v1/ultimate-ai/chat", json={"message": "Show me only the ones under £150k", "session_id": session_id}, headers={"Authorization": f"Bearer {API_KEY}"}, ) # Turn 3 — refers to a specific result requests.post( "https://api.propaideals.co.uk/api/v1/ultimate-ai/chat", json={"message": "Tell me more about the first one", "session_id": session_id}, headers={"Authorization": f"Bearer {API_KEY}"}, ) ``` Sessions persist for 24 hours of inactivity in Redis. ## Streaming responses For lower perceived latency, use the streaming endpoint. It returns Server-Sent Events (SSE) as the AI generates its response. ```http POST /api/v1/ultimate-ai/chat-stream ``` ```javascript const res = await fetch('https://api.propaideals.co.uk/api/v1/ultimate-ai/chat-stream', { method: 'POST', headers: { Authorization: `Bearer ${process.env.PROPAIDEALS_API_KEY}`, 'Content-Type': 'application/json', Accept: 'text/event-stream', }, body: JSON.stringify({ message: 'Find me HMO opportunities in Birmingham', session_id }), }) const reader = res.body.getReader() const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value) for (const line of chunk.split('\n').filter(l => l.startsWith('data: '))) { const event = JSON.parse(line.slice(6)) if (event.type === 'token') process.stdout.write(event.text) if (event.type === 'property') console.log('\nProperty:', event.property.address) if (event.type === 'done') console.log('\nDone') } } ``` ### Event types | Event | Payload | Description | |---|---|---| | `token` | `{ text: string }` | Next text token of the response | | `property` | `{ property: PropertyCard }` | A property card the AI found | | `progress` | `{ stage: string, message: string }` | Status update (e.g. "Searching properties...") | | `done` | `{ session_id, message_id }` | Stream complete | | `error` | `{ code, message }` | Stream-level error | ## Show more properties ```http POST /api/v1/ultimate-ai/show-more ``` Returns the next page of properties for an existing session — without re-running the AI search. ### Request ```bash curl -X POST https://api.propaideals.co.uk/api/v1/ultimate-ai/show-more \ -H "Authorization: Bearer $PROPAIDEALS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_user_123_2026_04_14", "offset": 5, "limit": 5 }' ``` This endpoint costs 1 request (not 5), since no AI inference is performed. ## Cost notes A single `/ultimate-ai/chat` call consumes: - **5 requests** from your monthly request quota (vs 1 for non-AI endpoints) - **1 AI chat call** from your AI chat quota Use the AI chat endpoint when you need natural language understanding. For deterministic structured queries, the [properties endpoint](./properties) is 5× cheaper and faster. ## Best practices - **Reuse `session_id`** across turns from the same user. Don't start a new session per message. - **Cache responses** on the client side keyed by `(session_id, message)` — users often re-issue the same query. - **Use streaming** for any user-facing UI; it dramatically improves perceived latency. - **Set a sensible `top_k`**. The default is 5; raise to 10–20 only if you need the wider set. - **Include `context`** when you have a user profile — the AI can personalise results without you having to explain the user every turn. --- title: API keys (dashboard endpoints) description: Programmatically manage your Prop AI Deals API keys. Create, list, and revoke keys via the dashboard endpoints. Authenticated with Clerk JWT (not API keys). --- # API keys The dashboard endpoints under `/api/v1/api-keys` let you manage your API keys programmatically. These endpoints are intended for the web dashboard and CLI tools — they're authenticated with a Clerk JWT, not an API key. > **Authentication:** These endpoints require a Clerk session token (`Bearer eyJ...`), not a `paid_*` API key. ## Create an API key ```http POST /api/v1/api-keys ``` Generates a new API key. **The `key` field is shown only once and never returned again** — store it immediately. ### Request ```bash curl -X POST https://api.propaideals.co.uk/api/v1/api-keys \ -H "Authorization: Bearer $CLERK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Production server" }' ``` ```javascript const res = await fetch('https://api.propaideals.co.uk/api/v1/api-keys', { method: 'POST', headers: { Authorization: `Bearer ${clerkToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'Production server' }), }) const created = await res.json() console.log('Save this:', created.key) ``` ```python res = requests.post( "https://api.propaideals.co.uk/api/v1/api-keys", json={"name": "Production server"}, headers={"Authorization": f"Bearer {clerk_token}"}, ) created = res.json() print("Save this:", created["key"]) ``` ### Body parameters | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | Human-readable name (1–100 chars) | | `expires_at` | ISO datetime | no | Optional expiry timestamp (UTC) | ### Response — `201 Created` ```json { "id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde", "name": "Production server", "prefix": "paid_a1b2c3", "key": "paid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "scopes": [ "properties:read", "market-data:read", "rentals:read", "investment:read", "areas:read", "spatial:read" ], "plan": "developer", "status": "active", "last_used": null, "expires_at": null, "created_at": "2026-04-14T10:30:00Z" } ``` > **The `key` field is only returned in this response.** All other endpoints (and listings) only return the `prefix`. If you lose the key, you must revoke and re-create it. ### Limits - Maximum **5 active keys per user**. Creating a 6th returns `400 Bad Request` with the message "Maximum of 5 active API keys per user reached". ## List your API keys ```http GET /api/v1/api-keys ``` Returns all of your API keys (active and revoked). ### Request ```bash curl https://api.propaideals.co.uk/api/v1/api-keys \ -H "Authorization: Bearer $CLERK_TOKEN" ``` ### Response ```json [ { "id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde", "name": "Production server", "prefix": "paid_a1b2c3", "scopes": ["properties:read", "market-data:read"], "plan": "developer", "status": "active", "last_used": "2026-04-14T10:25:00Z", "expires_at": null, "created_at": "2026-04-14T10:30:00Z" }, { "id": "9c8d7e6f-...", "name": "Staging server", "prefix": "paid_x9y8z7", "scopes": ["properties:read"], "plan": "developer", "status": "revoked", "last_used": "2026-04-10T08:00:00Z", "expires_at": null, "created_at": "2026-03-01T09:00:00Z" } ] ``` ## Revoke an API key ```http DELETE /api/v1/api-keys/{key_id} ``` Immediately revokes a key. The Redis cache is purged within 5 minutes of revocation, after which all requests using the key fail with `401 invalid_api_key`. ### Request ```bash curl -X DELETE https://api.propaideals.co.uk/api/v1/api-keys/5fa1b2c3-d4e5-6f78-9012-3456789abcde \ -H "Authorization: Bearer $CLERK_TOKEN" ``` ### Response — `200 OK` ```json { "status": "revoked", "key_id": "5fa1b2c3-d4e5-6f78-9012-3456789abcde" } ``` Returns `404` if the key doesn't exist or doesn't belong to the authenticated user. ## Get current usage ```http GET /api/v1/api-billing/usage ``` Returns the current billing period's usage across all your active keys. ### Request ```bash curl https://api.propaideals.co.uk/api/v1/api-billing/usage \ -H "Authorization: Bearer $CLERK_TOKEN" ``` ### Response ```json { "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" } ``` `null` for `requests_limit` or `ai_chat_limit` means **unlimited** (Enterprise plans). ## List available plans ```http GET /api/v1/api-billing/plans ``` Public endpoint (no auth required) — returns the list of API plans for your pricing page. ### Response ```json [ { "id": "starter", "name": "Starter", "monthly_price_pence": 9900, "monthly_request_limit": 20000, "rate_limit_per_second": 10, "ai_chat_limit": 100, "batch_limit": 100, "scopes": [ "properties:read", "market-data:read", "investment:read", "areas:read", "spatial:read", "planning:read", "leads:read", "epc:read" ] }, { "id": "professional", "name": "Professional", "monthly_price_pence": 29900, "monthly_request_limit": 100000, "rate_limit_per_second": 25, "ai_chat_limit": 1000, "batch_limit": 200, "scopes": [ "properties:read", "market-data:read", "investment:read", "areas:read", "spatial:read", "planning:read", "leads:read", "epc:read", "ai:chat", "build-cost:read" ] }, { "id": "business", "name": "Business", "monthly_price_pence": 99900, "monthly_request_limit": 400000, "rate_limit_per_second": 50, "ai_chat_limit": 4000, "batch_limit": 500, "scopes": [ "properties:read", "market-data:read", "investment:read", "areas:read", "spatial:read", "planning:read", "leads:read", "epc:read", "ai:chat", "build-cost:read" ] } ] ``` **A free tier exists** — 200 credits/month, no card, issued instantly from the dashboard. Paid plans run £29 (Starter), £59 (Growth), £99 (Advanced), £299 (Professional) and £999 (Business) per month. Enterprise plans are provisioned manually (contact `api@propaideals.co.uk`). --- title: Changelog description: Prop AI Deals API release notes, breaking changes, and deprecation timeline. --- # Changelog This page lists user-visible changes to the Prop AI Deals API. We follow [SemVer](https://semver.org/) at the API level — breaking changes are versioned (`/api/v2/`) and announced at least 90 days in advance. ## 2026-04-14 — Public API beta launch **New:** - Public API key authentication (`paid_*` bearer tokens) - `POST/GET/DELETE /api/v1/api-keys` — Manage your API keys from the dashboard - `GET /api/v1/api-billing/usage` — Live usage counters per billing period - `GET /api/v1/api-billing/plans` — List available API plans - Standard response envelope `{ data, meta }` on all `paid_*` requests - Rate-limit headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-Request-Cost`, `X-Monthly-Usage`, `X-Monthly-Limit` - Comprehensive [docs](./index), [llms.txt](./llms.txt) for AI agents **Changed:** - API key dashboard responses now use `key`, `plan`, `status`, `last_used` (was `raw_key`, `plan_id`, `is_active`, `last_used_at`) for consistency with frontend types - `UsageResponse` fields renamed: `requests_used`, `requests_limit`, `plan_tier`, `plan_name` (was `monthly_used`, `monthly_limit`, `plan_id`) **Fixed:** - API key creation now correctly returns the raw key field to the dashboard so it can be displayed once and copied ## How to subscribe to changes - Watch the [GitHub changelog repo](https://github.com/propaideals/api-changelog) for RSS-style updates - Subscribe to the API mailing list at [propaideals.co.uk/api](https://propaideals.co.uk/api) - Follow [@propaideals_dev](https://twitter.com/propaideals_dev) on Twitter