Developers

Investomation API

Build with the same data engine that powers the Investomation map. All endpoints are available at https://investomation.com/api/. This reference covers personal data (markers, formulas, shared views) and the demographics analytics engine that backs every heatmap and forecast on the platform.

Authentication

Overview

Most endpoints require authentication. There are two ways to authenticate:

  • API key — recommended for server-to-server integrations and scripts. Long-lived, sent on every request as a Bearer token.
  • Session cookie — used by the web client at investomation.com.

Public endpoints (anything under /api/analytics/.../us, plus the metrics catalog at /api/analytics/profile) work without authentication. Everything else returns 401 until you authenticate.

API keys

API keys are a paid feature available to Pro and Business subscribers. Generate them from your account settings. Each key is shown to you exactly once at creation time — store it in a password manager or secrets store immediately, because we only keep its hash and cannot recover the original.

Keys look like this:

ik_live_2dXk9mF7…43chars…vQ

The ik_live_ prefix is intentional: it makes keys recognizable to leak-detection bots scanning public code, and it lets our server distinguish them from JWTs at a glance.

Using a key

Send the key on the Authorization header:

GET /api/analytics/2022/rank/state/MA/zip?housing=1
Authorization: Bearer ik_live_2dXk9mF7…vQ

curl example:

curl -H 'Authorization: Bearer ik_live_…'   'https://investomation.com/api/analytics/2022/rank/state/MA/zip?housing=1'

That’s it — no token exchange, no expiration. The key inherits your account’s tier and feature flags, so it can call exactly the endpoints your account can call from the web app.

Revoking a key

Revoke from account settings at any time. Revocation is immediate — the next request using the key gets 401.

Account downgrade

If your account transitions back to Free, all of your API keys are revoked automatically. You’ll get an email with the list of keys that were revoked. To resume programmatic access, upgrade and create new keys.

JWT (username + password)

For interactive tools where API keys aren’t a fit, you can exchange username/password for a 7-day JWT:

POST /api/auth/token
Content-Type: application/json

{ "username": "[email protected]", "password": "…" }

Response:

{ "token": "eyJhbGc…", "token_type": "Bearer", "expires_in": 604800 }

Send the JWT on the same header as an API key:

Authorization: Bearer eyJhbGc…

JWTs are not recommended for production integrations — they expire, and rotating credentials means refreshing the token. Use an API key instead unless you specifically need short-lived auth.

Errors

  • 401 { error: "Authentication required" } — no credential supplied.
  • 401 { error: "Invalid API key" } — key not recognized, revoked, or owner deleted.
  • 401 { error: "Token expired" } — JWT past its 7-day lifetime; mint a new one.
  • 403 { error: "This feature requires an upgraded account" } — credential is valid but the endpoint is gated to a higher tier than your account holds.

Rate limits

Programmatic calls (API keys and JWTs) are subject to per-tier sliding-window caps. The web client (session cookies) is not subject to these limits — they only apply to scripted access.

Tier Per hour Per day
Free * 20 50
Pro 100 1,000
Business 600 10,000
* Free accounts can't mint API keys, but the JWT exchange at `/api/auth/token` is open to them — these caps apply to that path.

Caps are per user, not per key. A user with multiple keys shares one bucket; this prevents key-multiplication from being a quota-multiplication.

When you exceed a cap, the API returns 429 Too Many Requests:

{
  "error": "Rate limit exceeded",
  "error_description": "Daily cap (1000/day for professional) exceeded. Quota resets in 24h.",
  "retry_after": 86400,
  "tier": "professional",
  "limit": 1000,
  "window": "day"
}

retry_after is in seconds. Treat the response as advisory, not punitive — the limiter uses a sliding window, so quota frees up gradually rather than at a single reset moment.

These defaults are intentionally conservative for launch. If you have a legitimate use case that needs more headroom, reach out — we’d rather raise your cap than have you hit a wall. They’ll be relaxed over time as we observe real usage.

Burst protection

Independent of the per-tier caps, individual analytics endpoints (rank, timelapse, info) have a separate short-burst limiter (~5 hits per 2 seconds per user/IP) to prevent runaway loops. If you see 429s well below your daily quota, you’re hitting the burst limiter — add a small delay between calls.

Personal Data

Users

Users correspond to other investors using the platform. Use data typically contains a short bio, investor’s primary location of operation, their specialties/skills as well as other locations/markets they operate in.

These endpoints fetch data related to current user or other visible users.

GET /api/user
Fetch list of all visible users.

GET /api/user/:id
Fetch information for a specific user.

PUT /api/user/:id
Update user information (your changes will be rejected unless you are logged in as the same user).

GET /api/user/:id/comment
Fetch all blog post comments by this user.

GET /api/user/:id/marker
Fetch all markers that belong to this user.

Markers

Markers can be thought of as personal vaults of data tied to a location. Each marker is owned by a user and by default not visible to other users (this setting can be changed by the owner). Markers are typically used to group a collection of research data related to a property under one umbrella, including research data, important events (such as bill due date or an open-house showing), documents, notes, credit card data, and even analytics gathered by our platform.

Marker API is used to interact with markers (tagged geolocations) owned by the user. Markers can have descriptions, analysis, and events attached to them.

GET /api/marker
Fetch list of all markers that belong to the logged-in user.

GET /api/marker/:id
Fetch information for a specific marker.

PUT /api/marker
Create a new marker that belongs to the logged-in user.

PUT /api/marker/:id
Update marker information (your changes will be rejected unless you are logged in as the user who owns this marker).

DELETE /api/marker/:id
Delete the marker (your request will be rejected unless you are logged in as the user who owns this marker).

GET /api/marker/:id/event
Fetch all events that belong to a given marker.

Events

Events are typically tied to a marker, and generate a text/email notification at user’s request.

Events can be accessed independently of the markers they were attached to. This is useful for accessing them via a calendar.

GET /api/event
Fetch list of all events attached to markers that belong to the logged-in user.

GET /api/event/:id
Fetch information for a specific event.

PUT /api/event
Create a new event that belongs to the logged-in user (you will need to specify the marker to attach the event to).

PUT /api/event/:id
Update event information (your changes will be rejected unless you are logged in as the user who owns this event).

DELETE /api/event/:id
Delete the event (your request will be rejected unless you are logged in as the user who owns this event).

Formulas

Formulas are saved query formulas that can be reused. Each formula is owned by a user and can be made public for sharing.

Formula API is used to interact with saved formulas owned by the user. Formulas store the formula expression, associated metrics, and a user-friendly name.

GET /api/formulas
Fetch list of all formulas that belong to the logged-in user. Supports search by name using query parameter: ?name=query.

GET /api/formulas/:id
Fetch information for a specific formula.

POST /api/formulas
Create a new formula that belongs to the logged-in user. Requires name, formula, and metrics fields in the request body.

PUT /api/formulas/:id
Update formula information (your changes will be rejected unless you are logged in as the user who owns this formula). Can update name, formula, metrics, and public fields.

DELETE /api/formulas/:id
Delete the formula (your request will be rejected unless you are logged in as the user who owns this formula).

API Keys

API keys are managed through the same endpoints they’re used to authenticate against. CRUD on your own keys requires either a session cookie or another valid key — keys cannot mint themselves. Creating keys requires a paid (Pro or Business) tier.

For a higher-level walkthrough of how to use a key once you have one, see the Authentication section.

GET /api/keys List all keys (active + revoked) belonging to the logged-in user. Plaintext is never returned — only the prefix (first 12 chars), name, lastUsedAt, revokedAt, and created timestamps. Free-tier accounts always receive an empty list.

POST /api/keys Create a new API key. Request body: { "name": "optional label" }. Response includes the full plaintext key field exactly once — store it on receipt, because we only persist its sha256 hash. Returns 401 if the caller is on a Free tier.

DELETE /api/keys/:id Revoke a key by id. Soft delete — the row stays so you keep an audit trail of issuance and last-use. The next request using the revoked key returns 401. Idempotent: revoking an already-revoked key is a no-op.

Shared Views

Shared views allow users to share their map view (query, region, optional markers/events) with others via shortened URLs. Visitors can view shared maps but cannot modify them without logging in.

GET /api/shared-views
Fetch list of all shared views that belong to the logged-in user.

GET /api/shared-views/:id
Fetch information for a specific shared view (owner only).

GET /api/shared-views/code/:shortCode
Fetch shared view by short code (public, no authentication required). Returns 404 if not found, 410 if expired.

POST /api/shared-views
Create a new shared view that belongs to the logged-in user. Requires formula field. Optional fields: focus (JSON object with state/city), granularity, year, markers (array of marker IDs), events (array of event IDs), trackVisits (boolean), expiresAt (ISO date string), public (boolean). Returns the created shared view with shareUrl field containing the full shareable URL.

PUT /api/shared-views/:id
Update shared view information (your changes will be rejected unless you are logged in as the user who owns this shared view). Can update any field except shortCode.

DELETE /api/shared-views/:id
Delete the shared view (your request will be rejected unless you are logged in as the user who owns this shared view).

POST /api/shared-views/code/:shortCode/visit
Track a visit to a shared view (public, no authentication required). Only tracks if trackVisits is enabled for the shared view. Extracts IP address, user agent, and referrer from request headers.

GET /api/shared-views/:id/visits
Get visit analytics for a shared view (owner only, requires trackVisits to be enabled). Returns list of visits with IP, user agent, visitedAt timestamp, and referrer.

Location Services

Address resolution and point-of-interest lookups for US addresses. Geocoding endpoints turn addresses into coordinates (and back) along with the layered region hierarchy that contains each location.

Geocoding

Address geocoding for the United States. Convert addresses to coordinates (forward), coordinates to the nearest address (reverse), and partial input to ranked suggestions (autocomplete). Every successful response also includes the full administrative and statistical hierarchy that contains the location, from country down to Census block group.

Coverage: roughly 120 million US address points, refreshed quarterly. All endpoints return JSON.

Authentication

/forward and /reverse require authentication, either a session cookie (web client) or an API key on the Authorization header. See the Authentication section for details. Anonymous requests to these endpoints return 401.

/autocomplete and /health are open and accept anonymous requests. Autocomplete suggestions are designed for the unauthenticated type-ahead path, where users may not yet be logged in.

Response shape

Forward and reverse return a unified envelope:

{
  "query": "100 Main St Dorchester MA",
  "normalized": "100 Main St, Dorchester, MA, 02124",
  "latLng": { "lat": 42.2901, "lng": -71.0601 },
  "address": {
    "street": "100 Main St",
    "quality": "street"
  },
  "places": [
    { "precision": 1,  "type": "country",      "id": "US",           "name": "United States" },
    { "precision": 2,  "type": "state",        "id": "MA",           "name": "Massachusetts" },
    { "precision": 3,  "type": "csa",          "id": "148",          "name": "Boston-Worcester-Providence" },
    { "precision": 4,  "type": "msa",          "id": "14460",        "name": "Boston-Cambridge-Newton" },
    { "precision": 5,  "type": "county",       "id": "25025",        "name": "Suffolk County" },
    { "precision": 6,  "type": "city",         "id": "MA-BOSTON",    "name": "Boston" },
    { "precision": 7,  "type": "zip",          "id": "02124",        "name": "02124" },
    { "precision": 8,  "type": "locality",     "id": "MA-DORCHESTER","name": "Dorchester" },
    { "precision": 9,  "type": "tract",        "id": "25025090100",  "name": "Census Tract 901" },
    { "precision": 10, "type": "neighborhood", "id": "275424",       "name": "South Dorchester" },
    { "precision": 11, "type": "blockgroup",   "id": "250250901001", "name": "Block Group 1" }
  ]
}

Fields

Field Description
query The text or coordinates you sent.
normalized A canonical, comma-joined form of the matched address.
latLng WGS84 (EPSG:4326) decimal degrees, the standard for web maps.
address.street Title-cased street line, for example “100 Main St”.
address.quality Match precision discriminator: street, zipcode, city, state, or none. Filter on this if you need property-level results only.
places Ordered hierarchy of geographies containing the point, coarsest to finest. See below.

The places array

Every containing geography is one entry. Each entry has the same shape:

{ "precision": <int>, "type": <string>, "id": <string>, "name": <string> }
  • precision is an integer; higher values are more specific. Use it to sort entries, compare specificity between responses, or pick the most-specific place containing the point with places[places.length - 1].
  • type is a discriminator from a closed enum (see table). Filter by type to find a specific level, for example places.find(p => p.type === 'tract').id to get a Census tract GEOID for joining against tract-level datasets.
  • id is the canonical identifier for the type: FIPS, GEOID, OMB code, ZCTA, and so on. Compare regions by id, never by name, because names are not unique across regions and can change between vintages.
  • name is a human-readable label for display.

Place types, by precision:

precision type id format example name
1 country ISO 2-letter “United States”
2 state USPS 2-letter “Massachusetts”
3 csa OMB CSA code “Boston-Worcester-Providence”
4 msa OMB CBSA code “Boston-Cambridge-Newton”
5 county 5-digit county FIPS “Suffolk County”
6 city <state>-<name> slug “Boston”
7 zip 5-digit ZCTA “02124”
8 locality <state>-<USPS name> slug “Dorchester” (USPS-recognized sub-city name, only included when it differs from the incorporated city)
9 tract 11-digit GEOID “Census Tract 901”
10 neighborhood Zillow region id “South Dorchester”
11 blockgroup 12-digit GEOID “Block Group 1”

Gaps are allowed. A rural address may skip CSA, MSA, locality, and neighborhood entries; the remaining entries stay in order with their precision numbers preserved.

Endpoints

GET /api/geocode/forward?q=<text> Forward geocode. Resolves an address string to coordinates and the layered place hierarchy. Required parameter: q. Returns the unified envelope above.

Example:

GET /api/geocode/forward?q=1600+Pennsylvania+Ave+NW+Washington+DC
{
  "query": "1600 Pennsylvania Ave NW Washington DC",
  "normalized": "1600 Pennsylvania Ave NW, Washington, DC, 20500",
  "latLng": { "lat": 38.8977, "lng": -77.0365 },
  "address": { "street": "1600 Pennsylvania Ave NW", "quality": "street" },
  "places": [
    { "precision": 1,  "type": "country", "id": "US", "name": "United States" },
    { "precision": 2,  "type": "state",   "id": "DC", "name": "District of Columbia" },
    { "precision": 4,  "type": "msa",     "id": "47900", "name": "Washington-Arlington-Alexandria" },
    { "precision": 5,  "type": "county",  "id": "11001", "name": "District of Columbia" },
    { "precision": 6,  "type": "city",    "id": "DC-WASHINGTON", "name": "Washington" },
    { "precision": 7,  "type": "zip",     "id": "20500", "name": "20500" }
  ]
}

GET /api/geocode/reverse?lat=<n>&lng=<n> Reverse geocode. Resolves coordinates to the nearest known address plus its containing places. Required parameters: lat and lng, in decimal degrees (WGS84). Returns the same shape as forward.

Example:

GET /api/geocode/reverse?lat=35.2271&lng=-80.8431

GET /api/geocode/autocomplete?q=<prefix>&limit=<n>&state=<2-letter> Autocomplete. Returns up to limit (default 10, max 50) suggestions matching the partial address, designed for type-ahead search UIs. Debounce client-side to roughly 150ms. The optional state parameter is a 2-letter focus hint that ranks matches in that state above others without filtering anything out. Suggestions are returned in an items envelope.

Each suggestion carries only the address envelope and a lightweight places array (state and zip), because full layered enrichment per result would be too expensive for type-ahead. Once the user picks a suggestion, call /forward with the selected label to get the full layered response.

Example:

GET /api/geocode/autocomplete?q=100+main+st+charlotte&limit=2&state=NC
{
  "items": [
    {
      "label": "100 Main St, Charlotte, NC 28202",
      "address_full": "100 MAIN ST CHARLOTTE NC 28202",
      "latLng": { "lat": 35.227, "lng": -80.843 },
      "address": { "street": "100 Main St", "quality": "street" },
      "places": [
        { "precision": 2, "type": "state", "id": "NC",    "name": "North Carolina" },
        { "precision": 7, "type": "zip",   "id": "28202", "name": "28202" }
      ]
    }
  ]
}

GET /api/geocode/health Service health check. Returns { "available": true|false } indicating whether the geocoder is currently serving requests. Useful for monitoring and uptime checks.

Errors

  • 400 missing 'q' parameter: required query parameter is absent or empty.
  • 400 missing or invalid 'lat'/'lng': reverse-lookup coordinates are missing or non-numeric.
  • 401 Authentication required: /forward or /reverse was called without a valid session or API key.
  • 404 no match: the address or coordinates could not be resolved.
  • 429 Rate limit exceeded: per-tier hourly or daily cap reached (see the Authentication page for tier limits).
  • 503 autocomplete unavailable: the autocomplete index is temporarily unreachable; retry shortly.

Notes

  • /autocomplete and /health are open and CORS-enabled, so they can be called directly from a browser or a server with no credentials.
  • Coordinates use WGS84 (EPSG:4326), the standard for web maps.
  • address.quality: "street" results are property-level precision. "zipcode" and "city" are coarser. Filter by quality if you need exact addresses.
  • The places array is the source of truth for state, zip, city, and region data. Read those fields from places.find(p => p.type === '...') rather than expecting top-level flat copies; there are none.
  • The older /api/analytics/geocode?lat=&lng= endpoint returns the same regional data in a different (legacy) shape and is deprecated in favor of /api/geocode/reverse. It continues to work but emits a Sunset HTTP header. Migrate new code to the unified endpoints above.

Points of Interest

Find businesses, schools, parks, transit stops, and other points of interest inside a US city. Results include both individual items (within a viewport) and zip-level aggregates for map clustering at zoom-out. Every successful response uses the same latLng envelope as the Geocoding endpoints.

Coverage: roughly 60 million POIs across 21 categories, sourced from public and openly-licensed datasets and refreshed quarterly.

Authentication

All POI endpoints require authentication, either a session cookie (web client) or an API key on the Authorization header. See the Authentication section. Anonymous requests return 401.

Endpoints

GET /api/locations/poi/search?state=<2-letter>&city=<name>&category=<name>&[subcategory=<name>]&[mode=items|clusters]&[bounds=<s,w,n,e>]&[size=county|msa|csa] Search for POIs inside a city scope. state, city, and category are required. mode defaults to items. size defaults to county (just the city’s home county); set to msa or csa to widen to the metro or combined area. When mode=items, supply bounds (south,west,north,east in decimal degrees, WGS84) to filter to a viewport; otherwise all matching items in scope are returned (capped at 500). When mode=clusters, you get one entry per ZIP with a count.

Items response:

{
  "mode": "items",
  "count": 47,
  "items": [
    {
      "id": "08f2ea3b66c1d0001234567890abcdef",
      "name": "Joe's Diner",
      "latLng": { "lat": 35.227, "lng": -80.843 },
      "category": "Dining",
      "subcategory": "Restaurants",
      "zip": "28202",
      "operatingStatus": "open"
    }
  ]
}

Clusters response:

{
  "mode": "clusters",
  "count": 12,
  "items": [
    { "zip": "28202", "count": 142, "latLng": { "lat": 35.227, "lng": -80.843 } },
    { "zip": "28203", "count":  88, "latLng": { "lat": 35.205, "lng": -80.860 } }
  ]
}

The id is a stable string the server uses to identify a POI. Its format varies by data source (numeric for some sources, a 32-char hex string for others); treat it as opaque — do not parse, slice, or pattern-match against it.

GET /api/locations/poi/{category}/{subcategory}/{id}?[include=property] Fetch full detail for one POI. Reconstruct this URL from the category, subcategory, and id fields on the search-response item.

Optional include=property folds in the parcel and building the POI sits on, useful for gauging the size of a business or who owns the building it operates from. It is best-effort: the property key is null when no parcel covers the point, and property.building is null for states whose building catalog has not been built yet. Omit include to skip the lookup entirely (the property key is then absent).

Response (with ?include=property):

{
  "item": {
    "id": "08f2ea3b66c1d0001234567890abcdef",
    "name": "Joe's Diner",
    "latLng": { "lat": 35.227, "lng": -80.843 },
    "category": "Dining",
    "subcategory": "Restaurants",
    "details": {
      "address": "100 Main St",
      "phone": "+1-555-0100",
      "website": "https://example.com",
      "cuisines": ["american", "diner"]
    },
    "property": {
      "parcel": {
        "apn": "12345-678",
        "address": "100 Main St, Charlotte, NC 28202",
        "owner": { "name": "MAIN ST HOLDINGS LLC", "mailingAddress": "PO BOX 9, Charlotte, NC 28202" },
        "landUse": "Commercial",
        "zoning": "B-1",
        "lotSqft": 18000,
        "acreage": 0.41,
        "buildingSqft": 12000,
        "yearBuilt": 1998,
        "stories": 2,
        "units": 1,
        "assessedValue": 1250000,
        "marketValue": 1400000,
        "lastSale": { "date": "2019-06-01", "price": 1100000 }
      },
      "building": {
        "count": 1,
        "totalFootprintSqft": 11800,
        "maxHeightMeters": 9.4,
        "maxFloors": 2,
        "types": ["commercial"]
      }
    }
  }
}

The shape under details is per-category and reflects what is known about that subcategory. Common categories like Dining carry contact info; Education (universities) carries enrollment and earnings; Transportation traffic stations carry the trafficProfile block from FHWA. Treat details as a permissive object: read the fields you care about, ignore the rest. Fields inside property.parcel are similarly best-effort and vary by county assessor coverage; absent fields are omitted rather than returned as null.

Categories

The 21 categories under 6 groups:

Group Categories
Food & Shopping Dining, Grocery & Markets, Retail
Transportation Automotive, Transportation
Services Financial, Professional Services, Home Services, Personal Care, Storage & Shipping
Community Healthcare, Government & Safety, Education, Social Services, Faith, Accommodation
Recreation Recreation & Entertainment, Nature
Other Agriculture, Infrastructure, Other

Pass the category name exactly as shown (case-sensitive, including spaces and ampersands). Each category has a closed set of subcategories — for example, Dining has Restaurants, Fast Food, Coffee & Cafes, Bars & Nightlife, Specialty Food. Send the search without a subcategory to retrieve everything in the category.

Errors

  • 400 missing 'category' parameter: search needs a category to scope the query.
  • 400 missing 'state' or 'city' parameter: both are required for the search endpoint.
  • 400 invalid 'bounds' parameter: when present, bounds must be four comma-separated numbers (south,west,north,east).
  • 400 unknown category or subcategory: the {category}/{subcategory} path segments do not name a real POI category/subcategory pair.
  • 401 Authentication required: every POI endpoint needs valid auth.
  • 404 city not found: the state + city combination does not match a known incorporated place.
  • 404 POI not found: the decoded id points at a record that no longer exists.

Notes

  • Item responses are capped at 500 entries; combine bounds with progressive zoom to fetch smaller slices.
  • For map-driven discovery, call the search endpoint at low zoom with mode=clusters to render ZIP-level density, then switch to mode=items&bounds=... once the user zooms in.
  • POI data is fully first-party. There is no paid third-party fallback on any code path.
  • IDs are stable strings the server uses to identify a POI. Their format varies by source; treat them as opaque (do not parse, slice, or match patterns against them).
  • IDs are stable across requests but may change if a POI migrates between source datasets between quarterly refreshes. Treat them as ephemeral within a session and refetch via the search endpoint for long-lived references.

Demographics Analysis

Format

All metric-related queries are handled by our prediction engine, and have /api/predict prefix in the query. You can not modify the metrics information, so all of the endpoints in this section use the GET method.

Basic query format for informational queries (info and timeline) is:
/api/analytics/:year/:query/:region/:delta?:metric=:weight

Colons represent variables that should be substituted to form the query. Subsequent sections will explain how each part of the query is formed.

These queries return approximate value for a given metric at a certain time/location (i.e. population of Chelsea, MA in 2016).

The other type of query is a heatmap query (rank and timelapse), the format for these queries is: /api/analytics/:year/:query/:region/:granule/:delta?:metric=:weight

Note the presence of granule in the query.

Year

A regular query will start with a year (yyyy) or a year range (yyyy-yyyy).

Examples:
investomation.com/api/analytics/2022/rank/state/CA/zip (year)
investomation.com/api/analytics/2010-2022/timelapse/state/CA/zip (year range)

We track 1-4 decades of data for each metric, depending on the source (government-provided data such as CENSUS and tax records goes back further than sales data provided by Redfin, for example). If your query falls within the available dataset, you will see actual data for the region. If your query falls outside, you will see a prediction (along with a score to identify its accuracy) based on the predictive model for the query. Each metric uses a separate model of extrapolation, and we constanstly tweak our models to be more accurate.

Query

The query controls what kind of result is returned. The query must be compatible with both, the format of the year and region that was passed in. For example, rank is only compatible with year, while timelapse is only compatible with year range, as you probably already noticed from the example above.

Valid queries include:

  • info: Obtain information on a specific region (currently only works with specific zipcode, will be updated in the future to also aggregate info for larger regions).
  • timeline: Similar to info, but returns info for a range of years (good for plotting data).
  • rank: Rank zip codes in a region for a specific year into percentile groups (good for generating maps).
  • timelapse: Similar to rank, but returns ranks for a range of years (good for tracking map changes over time).

Examples:
investomation.com/api/analytics/2022/info/zip/90210?housing=1
investomation.com/api/analytics/2010-2022/timeline/zip/90210?housing=1
investomation.com/api/analytics/2022/rank/state/CA/zip?housing=1
investomation.com/api/analytics/2010-2022/timelapse/state/CA/zip?housing=1

timeline queries can be thought of as a multi-year versions of info queries, just like timelapse queries are multi-year versions of rank queries.

Region

Region is an area of land you want to apply your query to. Your query format will differ depending on the region it’s being applied to. The following region formats are supported:

/us
Applies query to all of United States.
Example Usage: /api/analytics/2022/rank/us/county?housing=1

/state/:id
Takes 2-letter abbreviation to identify a specific US state.
Example Usage: /api/analytics/2022/rank/state/CA/zip?housing=1

/msa/:id
Takes the ID of a given Metropolitan Statistical Area to identify a specific city. For example, Boston is one such MSA, which includes Cambridge, Newton, and many other Boston suburbs, but does not include Providence or Worcester. The ID we use is specific to Investomation, but you can obtain it by executing /api/analytics/related/msa/:zipOrCity query to find the MSA your 5-digit zip code or city (i.e. Boston,MA) belongs to.
Example Usage: /api/analytics/2022/rank/msa/e838947088b279fa2d360ca5b7ea6321/zip?housing=1

/csa/:id
Takes the ID of a given Combined Statistical Area to identify a specific set of closely-connected cities. For example, Greater Boston CSA includes the entirety of Boston MSA, as ell as Providence, Worcester, and portions of New Hampshire. The ID we use is specific to Investomation, but you can obtain it by executing /api/analytics/related/csa/:zipOrCity query to find the CSA your 5-digit zip code or city (i.e. Boston,MA) belongs to.
Example Usage: /api/analytics/2022/rank/csa/61a809ba35e19767af67bb35f8b91d5f/county?housing=1

/city/:id Takes the ID of a city to identify a specific city. A city is a subset of an MSA, and can be used to identify a specific city within an MSA. In rural areas, cities can be standalone entities, without belonging to an MSA.

/county/:id
Takes county FIPS to identify a specific county. Example Usage: /api/analytics/2022/info/county/25001?housing=1

/zip/:id
Takes the 5-digit zip code to identify a specific postal area. Example Usage: /api/analytics/2022/info/zip/02465?housing=1

/tract/:id
Takes tract GEOID to identify a specific CENSUS tract. Currently this is the smallest region we support, but we’re exploring Uber’s H3 indexing system to identify more granular regions.
Example Usage: /api/analytics/2022/info/tract/250010101?housing=1

Note: regions other than /us require user registration.

Region-Seeking Endpoints

Some queries require the user to know internal Investomation ID of a given region (like MSA and CSA). This ID can be determined via the following query:

/api/analytics/related/:regionType/:zipOrCity
This query identifies a greater region that a certain 5-digit zip code or city belongs to. City should include state in its name (i.e. Miami,FL). Region type can be either msa or csa. This endpoint will also return all other zip codes that belong to this statistical area.

Examples:
investomation.com/api/analytics/related/msa/90210
investomation.com/api/analytics/related/csa/Boston,MA

Proximity Analysis

Proximity analysis estimates data within a certain radius of a given location. This could be useful to estimate demand near your target location. For example, in self-storage industry investors often conduct a “demand study” to understand whether the location can reach full occupancy or if there is room to raise rents. This study consists of measuring population and self-storage capacity within a certain radius of the property (typically 3-5 miles). Investomation makes such analysis a breeze. The endpoint for this analysis is:

/api/analytics/:year/info/proximity?radius=:radius&lat=:lat&lng=:lng&metric=:weight The result will be in the following format:

{
  "zipcodeOverlap": { zipcode: overlapPercentage },
  "estimates": { ... metrics in the same format as regular info query ... }
}

Similarity Analysis

Similarity analysis finds regions that are statistically similar to a reference region based on selected metrics. This is useful for finding comparable markets, identifying peer regions, or discovering alternative investment opportunities with similar characteristics.

/api/analytics/similar/:year/:referenceGranuleType/:referenceGranuleId?scope=:scope&scopeId=:scopeId&limit=:limit&exclude=:excludedMetrics&metric=:weight

Parameters:

  • year: The year to analyze (yyyy format, e.g., 2022)
  • referenceGranuleType: Type of reference region (zip, county, city, tract)
  • referenceGranuleId: ID of the reference region
  • scope: Search scope - country, state, or msa (optional, defaults to country)
  • scopeId: Required when scope is state or msa - the state code (e.g., “MA”) or MSA ID
  • limit: Number of similar regions to return (optional, defaults to 10)
  • exclude: Comma-separated list of metrics to exclude from similarity calculation (optional)
  • metric=weight: One or more metrics to compare (same format as other endpoints)

Algorithm: The similarity algorithm uses Euclidean distance based on relative differences (coefficient of variation) across all selected metrics. For each metric, it finds the closest 100 candidates using database indexes, then calculates a combined similarity score for candidates that appear in at least 50% of the metric lists. Lower scores indicate better matches.

Response Format:

{
  "reference": {
    "type": "zip",
    "id": "02465",
    "name": "Newton Centre",
    "state": "MA",
    "values": {
      "population:total": 12345,
      "income:median": 98765,
      ...
    },
    "formulaValue": 75.2  // If formula is used
  },
  "similar": [
    {
      "type": "zip",
      "id": "10583",
      "name": "Scarsdale",
      "state": "NY",
      "score": 94.5,  // 0-100, higher is more similar
      "values": {
        "population:total": 12789,
        "income:median": 95432,
        ...
      },
      "formulaValue": 73.8
    },
    ...
  ],
  "scoringMethod": "distance",
  "metricsUsed": ["population:total", "income:median", ...],
  "hasFormula": false
}

Examples: Find zip codes similar to Newton, MA (02465) anywhere in the country:
/api/analytics/similar/2022/zip/02465?population:total=1&income:median=1&housing:home_price=1

Find zip codes similar to Newton, MA within Massachusetts only:
/api/analytics/similar/2022/zip/02465?scope=state&scopeId=MA&population:total=1&income:median=1

Find top 20 similar zip codes, excluding density from comparison:
/api/analytics/similar/2022/zip/02465?limit=20&exclude=population:density&population:total=1&income:median=1

Note: The similarity endpoint requires user registration and works best with 2-5 metrics. Including too many metrics may result in fewer matches.

Granule

In addition to region, some endpoints take a :granule parameter. Granule represents the size of the chunk we’ll be dividing our region into. For example, if our region is the city of Denver, and our granule size is a county, the rank query will return all counties that are part of the Denver MSA, ranked by the metric we requested. Think of a region like a loaf of bread and granule as the size of each slice.

Valid granule sizes are: tract, zip, county

Example Usage: /api/analytics/2022/rank/us/county?housing=1

Granule Context Endpoints

These endpoints are used to find granules of a certain size that are part of a larger region. They are useful when you have a region and want to find all the smaller regions that are part of it.

/api/analytics/context/granules/:granularity/of/:regionClass/:regionId:
Return a list of granule IDs of requested granularity for a given regionClass and regionId.

Example Usage:
/api/analytics/context/granules/zip/of/msa/e838947088b279fa2d360ca5b7ea6321
/api/analytics/context/granules/county/of/state/MA

/api/analytics/context/granules/:granularity/of/city/:cityId
Return a list of granule IDs of requested granularity for a given cityId. This endpoint works very similar to the endpoint above, it’s a convenience method to avoid having to make a separate query converting city to MSA or CSA.

Example Usage:
/api/analytics/context/granules/zip/city/Denver,CO

/api/analytics/context/:regionSize/of/:granularity/:granuleId
Return a list of sibling granule IDs of type granularity in a given regionSize that represents the parent region of granuleId. This endpoint is meant for cases when you have granule and want to find other nearby granules but don’t know the parent region.

Example Usage:
/api/analytics/context/msa/of/zip/90210

Delta

Delta parameter is optional, and changes the query to return the proportional difference in change instead of the actual value. The value of delta corresponds to the number of years back to go to in order to compute the difference. Delta is useful to see which areas are changing faster than others, either for better (higher percentile) or for worse (lower percentile).

Examples:

/2018/rank/state/FL/zip/delta/4?housing:home_price=1
Rank all Florida zip codes by percent growth in home prices between 2014 and 2018 (delta of 4 years).

/2010-2018/timelapse/state/FL/zip/delta/4?housing:home_price=1
Similar to the first query, but perform such ranking for every year between 2010 and 2018. 2010 ranks correspond to percent growth between 2006-2010, 2011 correspond to 2007-2011, etc.

Result

Result format depends on the query, as well as the metrics being requested. See examples below.

Single Granule

Queries of type info and timeline return actual data for a specific metric at a specific location/region. Regular rank-type metrics will return information as a single data point.

Example return value for info query (for obtaining data for specific year):

{
  "city": "Newbury",
  "county": "Essex",
  "housing:home_price": {
    "score": 0.7,
    "value": 555309
  }
}

Example return value for timeline query (for obtaining data change over time):

{
  "housing:home_price": [
    {
      "score": 0.95,
      "value": 543388
    },
    {
      "score": 0.7833333333333333,
      "value": 549751
    },
    {
      "score": 0.7,
      "value": 555309
    }
  ]
}

Category-based metrics are more complex and aggregate multiple rank-type metrics into one. Both info and timeline queries for these will return a list of data for all rank-type subqueries that form the aggregated categorical-type query.

Example return value for a categorical-type info query:

{
  "city": "Salisbury",
  "county": "Essex",
  "housing:by_type": {
    "single_family": {
      "score": 0.7,
      "value": 0.5584928949303785
    },
    "condo": {
      "score": 0.7,
      "value": 0.06890226386044895
    },
    "duplex": {
      "score": 0.7,
      "value": 0.05908248442100673
    },
    "triplex": {
      "score": 0.7,
      "value": 0.06792608034936377
    },
    "small_apartment_building": {
      "score": 0.7,
      "value": 0.0711739756497468
    },
    "medium_apartment_building": {
      "score": 0.7,
      "value": 0.020938567929689123
    },
    "large_apartment_building": {
      "score": 0.7,
      "value": 0.15179544836954317
    },
    "mobile_home": {
      "score": 0.7,
      "value": 0.1197719972322517
    },
    "boat": {
      "score": 0.7,
      "value": null
    }
  }
}

The Metrics Catalog section lists the type of each metric you can retrieve.

Granule Ranks

Queries of type rank (not to be confused with metrics of type rank) will break down all zip codes of a specific region into buckets. These buckets correspond to percentile scores for regular rank-type metrics, and categories for categorical-type metrics. The winning category for each zip code is also based on percentile scores. This means that a zip code may be marked with a winning category that is still considered a minority for that zip code relative to other categories if this zip code corresponds to the most frequent occurrence of that category.

For example, rank version of the above categorical-type query for housing:by_type metric may mark a zip code as a mobile_home hotspot even if the proportion of mobile homes in that zip code is lower than that of single-family homes, simply because it represents the highest proportion of mobile homes in the area compared to all other zip codes. This is intentional, to prevent minority metrics from being drowned by the dominant metric, and allows one to quickly find hotspots even for rare categories.

Example format of a response to the rank-type rank query:

{
    "type": "ranks",
    "data": {
        "1": [ // 0th percentile
            42023,
            42083,
            42021,
            42053,
            42121,
            42047,
            42107
        ],
        "2": [ // 10th percentile
            42123,
            42033,
            42105,
            42087,
            42065,
            42111,
            42051
        ],
        "3": [ // 20th percentile
            42097,
            42073,
            42085,
            42039,
            42079,
            42005,
            42049
        ],
        "4": [ // 30th percentile
            42013,
            42031,
            42063,
            42025,
            42059,
            42061,
            42069
        ],
        "5": [ // 40th percentile
            42007,
            42009,
            42035,
            42067,
            42015,
            42117,
            42129
        ],
        "6": [ // 50th percentile
            42037,
            42113,
            42043,
            42057,
            42089,
            42081,
            42133
        ],
        "7": [ // 60th percentile
            42011,
            42075,
            42131,
            42099,
            42103,
            42003,
            42109
        ],
        "8": [ // 70th percentile
            42115,
            42093,
            42055,
            42125,
            42127,
            42001,
            42077
        ],
        "9": [ // 80th percentile
            42119,
            42095,
            42041,
            42071,
            42101,
            42019,
            42045
        ],
        "10": [ // 90th percentile
            42027,
            42091,
            42017,
            42029
        ]
    }
}

Example format of a response to the categorical-type rank query:

{
    "type": "categories",
    "data": {
        "no_data": [
            1063,
            1086,
            1199,
            1937,
            2366,
            2584,
            2643
        ],
        "gas_heat": [
            1001,
            1003,
            1022,
            ...
            2780,
            2790,
            2791
        ],
        "gastank_heat": [
            1026,
            1029,
            1032,
            ...
            2568,
            2575,
            2652
        ],
        "electric_heat": [
            1002,
            1013,
            1020,
            ...
            2760,
            2762,
            2763
        ],
        "oil_heat": [
            1005,
            1007,
            1008,
            ...
            2770,
            2771,
            2779
        ],
        "coal_heat": [
            1068,
            1081,
            1256
        ],
        "wood_heat": [
            1070,
            1083,
            1098,
            ...
            1378,
            1379,
            1436
        ],
        "solar_heat": [
            1257,
            1338
        ],
        "other_heat": [
            1079,
            1080,
            1224,
            ...
            1467,
            1516,
            1521
        ],
        "no_heat": [
            2203,
            2457
        ]
    },
    "categories": [
        "gas_heat",
        "gastank_heat",
        "electric_heat",
        "oil_heat",
        "coal_heat",
        "wood_heat",
        "solar_heat",
        "other_heat",
        "no_heat"
    ]
}

Versus Mode

When number of selected categories is two (either because there are only two categories in a given metric - i.e. Democrat vs Republican) or because you narrowed the selection down to two, the result will report more granular differences between the two categories in each zipcode rather than simply color-coding the zipcode according to the winning category. This is called versus mode and more information on it can be found on our blog. To learn how to activate versus mode on demand, see Metric Filters section.

Example format of a response to the versus-type rank query:

{
    "type": "versus",
    "data": {
        "0": [ // neutral
            15054,
            15779,
            16121,
            ...
            18248,
            19523,
            19550
        ],
        "1": [ // low lean towards 1st category
            15640,
            16415,
            16444,
            ...
            18045,
            18063,
            18091
        ],
        "2": [
            15316,
            15611,
            16401,
            ...
            18088,
            18343,
            18351
        ],
        "3": [
            15006,
            15015,
            15017,
            ...
            19457,
            19460,
            19520
        ],
        "4": [
            15018,
            15024,
            15025,
            ...
            19475,
            19477,
            19492
        ],
        "5": [
            15030,
            15045,
            15075,
            ...
            19473,
            19474,
            19525
        ],
        "6": [
            15014,
            15031,
            15034,
            ...
            19090,
            19310,
            19401
        ],
        "7": [
            15213,
            15612,
            16427,
            ...
            19113,
            19367,
            19405
        ],
        "8": [
            16677,
            19103,
            19106,
            ...
            19147,
            19150,
            19154
        ],
        "9": [
            15007,
            19102,
            19104,
            ...
            19151,
            19152,
            19153
        ],
        "10": [], // high lean towards 1st category
        "no_data": [],
        "-10": [], // high lean towards 2nd category
        "-9": [],
        "-8": [
            15325,
            15334,
            15357,
            ...
            17229,
            17238,
            17267
        ],
        "-7": [
            15320,
            15322,
            15337,
            ...
            18850,
            18851,
            18854
        ],
        "-6": [
            15310,
            15327,
            15380,
            ...
            18832,
            18848,
            18853
        ],
        "-5": [
            15004,
            15021,
            15038,
            ...
            18840,
            18844,
            19549
        ],
        "-4": [
            15012,
            15019,
            15060,
            ...
            19545,
            19554,
            19564
        ],
        "-3": [
            15001,
            15003,
            15005,
            ...
            19608,
            19609,
            19610
        ],
        "-2": [
            15033,
            15061,
            15066,
            ...
            19503,
            19559,
            19560
        ],
        "-1": [ // low lean towards 2nd category
            15410,
            15420,
            15434,
            ...
            19602,
            19604,
            19611
        ]
    },
    "categories": [
        "democrat", // category 1
        "republican" // category 2
    ]
}

Metrics

Metrics are the bread and butter of Investomation. They’re used to rank regions, estimate trends, or obtain specific information about a given region. You can find a list of all available metrics and their format by executing the following query: /api/analytics/profile

You can combine multiple metrics into a single query via & operator, effectively ranking zip codes in a certain region based on how well each one fits all of the specified criteria, the same way Investomation Heat Map combines these metrics.

For example, the following query ranks all zipcodes in Massachusetts by their rent-to-price ratio and crime, preferring areas with highest rent-to-price ratio and lowest crime:
/api/analytics/2022/rank/state/MA?housing:rent_to_price=2&crime:crime_index=-1

Note the weights, negative values correspond to metric being undesirable, and higher weights signify the importance of the metric. The above query puts twice as much emphasis on rent-to-price ratio as it does on crime score.

Metric Types

There are currently 4 types of metrics:

  • rank: regular metrics that rank a region based on its percentile score, these types of metrics can be stacked in your analysis.
  • proximity: proximity metrics rank a location based on its proximity to a given resource and quality of said resource. For example, how far away is the area from nearest school or police station, and what is the score of that police station. Like rank-type metrics, these metrics can be stacked as additional overlays.
  • categorical: categorical metrics compare several qualitative metrics based on proportion, due to how this comparison is done, categrical metrics can’t stack with other metrics
  • categorical rank: these work like categorical metrics (and can’t be stacked), with the exception that categories can be objectively ranked (i.e. number of bedrooms).

Metric Filters

The concept of metric filters is explained on our blog. You can control these filters via API. For example, for rank-type metrics, you can filter our a portion of results that don’t match certain criteria. You can apply a rank-type filter by prefixing a rank-type metric with the keyword filter.

For example, the following query will rank all of US zipcodes by highest population, but exclude regions where median home price is below $50,000:
/api/analytics/2023/rank/us/zip?population:total=1&filter:housing:home_price=below:50000

For categorical-type metrics, the filter works differently (see above blog article for more detail). Instead of using the filter keyword in the query, the weight (which typically doesn’t matter for categorical metrics since they can’t be combined with other metrics) can be replaced with a comma-separated list of categories you want to compare:
/api/analytics/2023/rank/state/ma/zip?housing:by_heat=0,3

Category numbers correspond to the order they are listed in when executing /api/analytics/profile query.

NOTE: When the number of requested categories is two, as in the above example, the result will be reported in Versus Mode format

Delta Metrics

Delta metrics are special metrics with an individual delta parameter applied to it, independent of other metrics. Delta metrics use delta:# suffix. For example, delta version of the population:total metric going 5 years back would be called population:total:delta:5 and represents population growth over the last 5 years: /api/analytics/2023/rank/us/zip?housing:units=1&population:total:delta:5=1

NOTE: Delta metrics don’t stack with delta queries, you can’t pass delta version of a metric as a parameter to delta version of a query.

Metrics Catalog

The full catalog of metrics exposed by the demographics engine, grouped by category. Use these names as metric=weight query parameters in any of the analytics endpoints described above.