REST API — Server-Side Integration
Build custom dashboards, cron jobs, and backend automations against Investra data. Standard JSON-over-HTTPS endpoints with Bearer-token auth — not JSON-RPC, not MCP. Use this if you are writing code, not connecting Claude.
REST vs. MCP — which one do I want?
Investra exposes two parallel interfaces to the same underlying data:
| Use case | Use this |
|---|---|
| Connecting Claude (claude.ai, Claude Desktop, Claude Code) to Investra | /api/mcp — MCP JSON-RPC 2.0 protocol. See the Quickstart page. |
| Building a custom dashboard, cron job, mobile app, or other backend integration that calls Investra from your own code | REST endpoints below — standard HTTPS + JSON, easy to hit from any language. |
Common mistake: Writing Node.js or Python code that POSTs to /api/mcp with a REST-shaped body. That endpoint speaks MCP's JSON-RPC 2.0 protocol — if the envelope isn't exactly right, your request hangs waiting for a response that never comes. Use the REST endpoints below for server-side code.
Authentication
Every authenticated REST endpoint uses the same Authorization: Bearer inv_… header as the MCP endpoint. Generate a key at your profile page → Developer tab. Store it in an environment variable — never hardcode it, never commit it.
Authorization: Bearer inv_your_api_key_here
Two endpoints are unauthenticated and return public FRED / Census data: /api/interest-rate and /api/market-data.
Rate limits
Two independent rate-limit layers apply to every authenticated request:
- Per API key (authenticated endpoints): 60 requests / minute and 3,000 requests / hour. Keyed on your API key prefix — stays consistent across source IPs. 429 response includes
Retry-AfterandX-RateLimit-*headers. - Per IP (public endpoints):
/api/interest-rateand/api/market-dataare unauthenticated and limited by caller IP. - Per subscription tier (monthly credits): Off-market searches, off-market lookups, skip-trace leads, and analyses all have monthly caps that vary by plan. See Usage & Quota. 403
PaymentRequiredorSubscriptionLimitresponse when exhausted.
Use GET /api/usage to check your current credit balance before a batch run.
Endpoints
Every endpoint returns JSON. All URLs are relative to https://www.investraapp.com.
Search on-market properties
POST /api/search-properties — 150M+ active on-market listings nationwide, pre-enriched with cash flow, cap rate, cash-on-cash, A–F grade, and anomaly warnings. 1 analysis credit per call.
curl -X POST https://www.investraapp.com/api/search-properties \
-H "Authorization: Bearer inv_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"searchParams": {
"location": "Charlotte, NC",
"maxPrice": 300000,
"limit": 40,
"cashFlowOnly": true
}
}'
Get property details by address
POST /api/property-by-address — Full details for a single address (including photos, unique property ID, owner info when available). 1 analysis credit per call.
POST /api/property-by-address
{ "address": "4124 Vallhorn Ln, Charlotte, NC 28213" }
Underwrite / estimate investment costs
POST /api/cost-estimate — Full rental or fix-and-flip underwrite. Returns NOI, cash flow, cap rate, DSCR, cash-on-cash, A–F grade, and a full expense breakdown. Requires authentication. 1 analysis credit per call, regardless of which strategy type you request.
POST /api/cost-estimate
{
"property": { "address": "…", "price": 310000 },
"options": {
"type": "full",
"downPaymentPct": 0.20,
"interestRate": 6.5,
"loanTermYears": 30,
"rehabScope": "light"
}
}
Sales comps (ARV)
POST /api/sales-comps — Recent sold comparables. Returns ARV low/base/high/weighted with confidence. 1 analysis credit per call.
POST /api/sales-comps
{ "address": "…", "beds": 3, "baths": 2, "sqft": 1400 }
Rent estimate
POST /api/rent-comps — Comparable rentals plus a percentile-based rent sensitivity (worst / base / best). 1 analysis credit per call.
POST /api/rent-comps
{ "address": "…" }
Market analysis
POST /api/market-analysis — City-level pricing, rental, supply, demand, and economic indicators. 1 analysis credit per call.
POST /api/market-analysis
{ "city": "Cleveland", "state": "OH" }
Macro market data
GET /api/market-data — Live FRED mortgage rates + housing indicators. Public endpoint, no auth required.
GET /api/interest-rate — Current 30-year fixed. Public endpoint.
Off-market properties ⚠️
GET /api/off-market/search?city=…&state=…&limit=&offset= — Address-only search of Investra's 40M-parcel off-market index. ⚠️ Paid plan required. 1 search credit per query. Supports limit (default 60, max 200) and offset (0-based) for pagination — pagination is free, only the initial search query costs a credit.
curl "https://www.investraapp.com/api/off-market/search?city=Memphis&state=TN&limit=60&offset=0" \
-H "Authorization: Bearer inv_your_api_key_here"
POST /api/off-market/lookup — Enriches one off-market property with motivation score, equity, distress flags. ⚠️ Paid plan required. 1 enrichment credit per call.
POST /api/off-market/lookup
{ "street": "…", "city": "…", "state": "TN", "zip": "38115" }
POST /api/off-market/skip-trace — Owner contact info for an off-market property. ⚠️ Pro Plus required. 1 lead credit per call. Accepts flat or nested { address: {…} } body.
POST /api/off-market/skip-trace
{ "street": "…", "city": "…", "state": "TN", "zip": "38115" }
Community deals
GET /api/deals — Browse curated user-submitted deals. Paid plan required.
GET /api/deals/[id] — Full detail for one deal. Paid plan required.
Usage & quota
GET /api/usage — Your current tier, quota usage, remaining credits, and reset dates. Hit this before batch runs to avoid tripping subscription limits mid-job.
curl https://www.investraapp.com/api/usage \
-H "Authorization: Bearer inv_your_api_key_here"
Returns:
{
"user": { "id": "…", "email": "you@example.com" },
"subscription": {
"tier": "pro_plus_monthly",
"status": "active",
"is_active": true,
"is_trialing": false
},
"quota": {
"analyses": { "limit": null, "used": 124, "remaining": null, "resets_at": "…" },
"skip_trace_leads":{ "limit": 15, "used": 2, "remaining": 13, "resets_at": "…" },
"off_market_searches": { "limit": 10000, "used": 47, "remaining": 9953, "resets_at": "…" },
"off_market_lookups": { "limit": 75, "used": 3, "remaining": 72, "resets_at": "…" }
},
"rate_limits": { "per_minute": 60, "per_hour": 3000 }
}Complete Node.js example — Sacramento deal scanner
Pulls Sacramento listings under $500k in specific zips, filters by lot size, and emails the top picks. Runs on cron.
// scanner.js — runs at 7am PT daily
import 'dotenv/config';
const API_KEY = process.env.INVESTRA_API_KEY;
const TARGET_ZIPS = new Set(['95819', '95816', '95817']);
const MIN_LOT = 5000;
const MAX_PRICE = 500_000;
async function call(path, body) {
const res = await fetch(`https://www.investraapp.com${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(60_000),
});
if (!res.ok) throw new Error(`${path} → HTTP ${res.status}`);
return res.json();
}
async function searchSacramentoDeals() {
const all = [];
for (let page = 1; page <= 8; page++) {
const data = await call('/api/search-properties', {
searchParams: {
location: 'Sacramento, CA',
maxPrice: MAX_PRICE,
limit: 40,
page,
cashFlowOnly: false,
},
});
const props = data.properties || [];
if (!props.length) break;
all.push(
...props.filter((p) =>
TARGET_ZIPS.has(String(p.zipcode || p.zipCode || '').slice(0, 5)) &&
(p.lotSize || 0) >= MIN_LOT,
),
);
if (!data.analysis?.hasMorePages) break;
}
return all;
}
const deals = await searchSacramentoDeals();
const topPicks = deals
.filter((d) => d.grade === 'A' || d.grade === 'B')
.sort((a, b) => (b.investmentScore || 0) - (a.investmentScore || 0))
.slice(0, 10);
console.log(`${deals.length} matched, ${topPicks.length} A/B-grade picks:`);
topPicks.forEach((d) => {
console.log(` ${d.grade} ${d.address} — $${d.price.toLocaleString()}, CF $${d.potentialCashFlow}/mo, cap ${d.capRate}%, score ${d.investmentScore}`);
});
Schedule with node-cron, GitHub Actions, Vercel Cron, or whatever fits your stack. Every property in the response already carries price, beds, baths, sqft, lotSize, yearBuilt, estimatedRent, potentialCashFlow, capRate, cashOnCash, dscr, investmentScore, grade, verdict, anomaly — no follow-up enrichment needed unless you want deeper detail on a specific property.
Python example
import os, requests
API_KEY = os.environ["INVESTRA_API_KEY"]
BASE = "https://www.investraapp.com"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def search(location, max_price=None, limit=40):
res = requests.post(
f"{BASE}/api/search-properties",
headers=HEADERS,
json={"searchParams": {"location": location, "maxPrice": max_price, "limit": limit}},
timeout=60,
)
res.raise_for_status()
return res.json()["properties"]
def underwrite(address, price, down_pct=0.20, rate=6.5):
res = requests.post(
f"{BASE}/api/cost-estimate",
headers=HEADERS,
json={
"property": {"address": address, "price": price},
"options": {"type": "full", "downPaymentPct": down_pct, "interestRate": rate},
},
timeout=60,
)
res.raise_for_status()
return res.json()
props = search("Memphis, TN", max_price=200_000)
print(f"{len(props)} Memphis deals under $200k")
for prop in props[:5]:
print(prop["grade"], prop["address"],
"CF $" + str(prop["potentialCashFlow"]) + "/mo",
"cap " + str(prop["capRate"]) + "%")Error responses
All error responses are JSON with a consistent shape:
{
"error": "Monthly lead limit reached",
"errorType": "SubscriptionLimit",
"remaining": 0,
"limit": 15,
"used": 15,
"upgrade": true
}
| HTTP | errorType | Cause |
|---|---|---|
| 400 | ValidationError | Missing or malformed request body |
| 401 | AuthError | Missing or invalid API key |
| 403 | SubscriptionRequired / PaymentRequired | Your tier doesn't include this feature or credits are exhausted |
| 429 | RateLimitError | Per-key or per-IP rate limit tripped. Check Retry-After. |
| 500 | ServerError | Unexpected server failure. Report at info@investraapp.com. |
Security best practices
- Never commit your API key. Use environment variables (
process.env.INVESTRA_API_KEY). Add.envto.gitignore. - Rotate on suspicion. If your key leaks or you share it with a third-party tool, generate a new one at Profile → Developer. The old key is invalidated immediately.
- Don't expose the key to browsers. CORS on REST endpoints is locked to investraapp.com — browser code from other origins will fail anyway, but more importantly, a key in your frontend JS bundle is stolen within minutes. Always keep it server-side.
- Respect credit semantics.
skip_traceand off-market lookups cost real money on each call. Don't retry failed calls blindly. - Skip-trace compliance. Contacts returned from
/api/off-market/skip-tracemay be on the federal Do Not Call registry. Comply with TCPA / DNC regulations before any outreach.