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 caseUse 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-After and X-RateLimit-* headers.
  • Per IP (public endpoints): /api/interest-rate and /api/market-data are 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 PaymentRequired or SubscriptionLimit response 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
}
HTTPerrorTypeCause
400ValidationErrorMissing or malformed request body
401AuthErrorMissing or invalid API key
403SubscriptionRequired / PaymentRequiredYour tier doesn't include this feature or credits are exhausted
429RateLimitErrorPer-key or per-IP rate limit tripped. Check Retry-After.
500ServerErrorUnexpected server failure. Report at info@investraapp.com.

Security best practices

  • Never commit your API key. Use environment variables (process.env.INVESTRA_API_KEY). Add .env to .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_trace and 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-trace may be on the federal Do Not Call registry. Comply with TCPA / DNC regulations before any outreach.