Public API Documentation

Reference for callers using a generated X-API-Key. Choose a tab below to read each API's full reference.

Overview

The Research API returns enriched property research for a single street address, on demand. Each request combines listing details from Redfin (with a Zillow fallback) and public-records data from qPublic (Georgia counties) or Bridge Interactive (Los Angeles County), and returns a single merged JSON payload.

Responses are served from cache when the underlying data is fresh (≤ 90 days). Cold or stale requests are queued for live scraping and you'll receive a job_id to poll, or a webhook callback when results land.

Authentication

Every request must include an X-API-Key header with a key generated on the API Keys page. The raw key is shown once at creation — store it in your secrets manager immediately.

curl https://dashboard.rek-partners.com/api/public/research \
  -H "X-API-Key: rek_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"address":"996 Greenwood Ave NE","city":"Atlanta","state":"GA","zip_code":"30306"}'
Missing or invalid keys return 401 Unauthorized. Revoked keys are rejected immediately on the next request.

Base URL

EnvironmentURL
Productionhttps://dashboard.rek-partners.com/api/public

All endpoints below are paths relative to the base URL.

Quick start

Three calls cover the full happy path:

1. Submit a research request

POST /research
Content-Type: application/json
X-API-Key: rek_live_…

{
  "address":  "2817 S Norton Ave",
  "city":     "Los Angeles",
  "state":    "CA",
  "zip_code": "90018"
}

Cache hit → returns the full research payload immediately with "status": "fresh_cache".

Cache miss / stale → returns "status": "queued" + a job_id.

2. Poll for results

GET /jobs/{job_id}
X-API-Key: rek_live_…

Polling returns "status": "running" until the worker finishes; final response carries "status": "completed" and the full research payload.

3. (Optional) Subscribe to a webhook

If a callback_url is provided in step 1, we POST the completed payload to that URL — see the Webhook callback section for HMAC verification.

POST /research

The primary endpoint. Submit one address per call.

Request body

FieldTypeRequiredNotes
addressstringyesStreet address (no city/state suffix).
citystringyese.g. "Atlanta", "Los Angeles".
statestringyesTwo-letter USPS code ("GA", "CA", …).
zip_codestringyes5-digit ZIP. Improves match accuracy when sources disagree on address normalization.
callback_urlstringnoHTTPS URL where the completed payload will be POSTed when ready.
force_refreshbooleannotrue bypasses the 90-day freshness cache and re-scrapes.

Response — cache hit (sync, 200 OK)

{
  "status":       "fresh_cache",
  "property":     { /* canonical property columns */ },
  "redfin":       { /* listing data */ },
  "zillow":       null,
  "public_records":         { /* full provider payload */ },
  "public_records_summary": { /* promoted fields — see below */ },
  "sources_used": ["redfin", "public_records"]
}

Response — queued (202 Accepted)

{
  "status":     "queued",
  "job_id":     "a3f9e0c2-…",
  "poll_url":   "/api/public/jobs/a3f9e0c2-…",
  "eta_seconds": 60
}

GET /jobs/{job_id}

Poll for an in-flight research job. Returns the same merged payload as a cache hit once status === "completed".

Status values

StatusMeaning
queuedJob accepted, not yet started.
runningA worker is currently scraping / merging.
completedDone. Payload present.
failedUnrecoverable error. Inspect error field.
Polling cadence: 5-10 second intervals. Typical end-to-end time is 30-90 seconds; prefer a webhook for jobs you submit at scale.

Webhook callback

Pass callback_url in the original /research request and we'll POST the completed payload to that URL when the job finishes.

Headers we send

HeaderValue
Content-Typeapplication/json
X-REK-SignatureHMAC-SHA256 hex digest of the raw request body
X-REK-Job-IdThe original job id

Verifying the signature

The HMAC secret is sha256(your_raw_api_key) — i.e. the same value we store internally as the key hash. Compute it once and store the result alongside your secret.

# Python
import hmac, hashlib

SECRET = hashlib.sha256(raw_api_key.encode()).digest()

def verify(raw_body: bytes, header_sig: str) -> bool:
    expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_sig)
Verify on the raw request body — any JSON re-serialization by your framework will change byte order and break the signature.

Status codes

CodeWhenBody
200Synchronous success — cache hit on /research or a completed /jobs/{id}.Full research payload.
202Queued — /research miss.{job_id, poll_url, eta_seconds}
401Missing / invalid / revoked X-API-Key.{"detail":"Invalid or revoked API key"}
404Unknown job_id on a polling call.{"detail":"Job not found"}
429Rate limit exceeded.{"detail":"Rate limit exceeded","retry_after_seconds":N}
500Unexpected server-side error.{"detail":"Internal error","request_id":"…"}

Response field reference

The completed payload has four top-level blocks plus a list of sources used. Most callers consume property + public_records_summary and ignore the raw public_records blob.

Top-level keys

KeyTypeDescription
propertyobjectCanonical property columns (address, beds, baths, sqft, year_built, price, image_url, latitude/longitude, etc.).
redfinobject \| nullListing details from Redfin: price, price_per_sqft, listing_type, listing_date, listing_agent, beds, baths, sqft, days_on_redfin, last_updated, last_checked.
zillowobject \| nullSame shape as redfin — populated only when Redfin has no listing for this address.
public_recordsobject \| nullFull provider blob (Bridge or qPublic shape). Large. Inspect when you need a field that isn't promoted.
public_records_summaryobject \| nullPromoted fields lifted out of public_records — see table below.
sources_usedstring[]Which providers contributed to this payload, e.g. ["redfin","public_records"].
listing_sourcestring \| null"redfin" or "zillow" — which listing block carried the data.
listing_urlstring \| nullCanonical URL on the listing source.

public_records_summary — promoted fields

KeyTypeNotes
parcel_idstringCounty APN / parcel number.
owner_namestring \| nullCurrent owner(s) of record.
owner_mailing_addressstring \| nullTax-mailing address on file with the assessor (qPublic counties).
assessed_value_totalint \| nullTaxable assessed value, USD.
market_value_totalint \| nullEstimated market value, USD.
tax_amount_currentint \| nullMost recent annual tax bill, USD.
annual_tax_amountint \| nullSame as tax_amount_current; derived from tax_history when the canonical column is null.
tax_yearint \| nullCalendar year of the tax amount.
effective_year_builtint \| nullPer the assessor; may differ from property.year_built.
zoning_codestring \| nullLocal zoning code (e.g. "LAR1", "RG2").
last_sale_datestring \| nullMost recent transfer date (ISO YYYY-MM-DD).
last_sale_amountint \| nullSale price recorded with that transfer.
tax_districtstring \| nullCounty tax-district code. qPublic counties only.
homesteadstring \| null"Y" or "N" — homestead-exemption flag. qPublic only.
neighborhoodstring \| nullCounty-internal neighborhood code (e.g. "CA02"). qPublic only.
Coverage: qPublic-only fields (last 3 rows) return null for LA / CA addresses since Bridge doesn't expose them.

Overview

The Connector API is a pull-based batch-sync surface. Designed for downstream apps (e.g. Datahouse, internal ETLs) that mirror REK's property data into their own store. REK serves the data; the caller owns the schedule.

Each property is returned in the same full-detail shape as the Research API's POST /research response, so consumer code can ingest payloads from either surface interchangeably.

Looking up a single address on demand? Use the Research API tab instead.

Authentication

Same X-API-Key system as the Research API. Generate a dedicated key (e.g. named datahouse-sync) on the API Keys page and store the raw value in your secrets manager — it's shown once at creation.

curl https://dashboard.rek-partners.com/api/connector/properties \
  -H "X-API-Key: rek_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Base URL

EnvironmentURL
Productionhttps://dashboard.rek-partners.com/api/connector

All endpoint paths below are relative to this base URL.

Quick start

Three common call patterns:

1. Full sync (first run)

GET /properties?limit=50
X-API-Key: rek_live_…

No since param → returns every dashboard-visible property, paginated. Follow next_cursor until null.

2. Delta sync (subsequent runs)

GET /properties?since=2026-06-05T03:00:00Z&limit=50
X-API-Key: rek_live_…

Returns only properties where updated_at > since. Save the response's sync_timestamp as the next call's since value.

3. Refetch one property

GET /properties/{listing_id}
X-API-Key: rek_live_…

Refreshes a single record by its stable listing_id — no need to run a full sync.

GET /properties

Paginated full-detail list. Each item has the same shape as a Research API POST /research response (see Response fields below for the complete key reference).

Query parameters

ParamTypeDefaultNotes
sinceISO-8601 datetimeIf set, return only rows where updated_at > since. Omit for full sync.
cursorstring (opaque)Pagination token from the previous page's next_cursor.
limitint (1–100)50Page size. Use 25–50 for fast pages; 100 for catch-up runs.

Response (200 OK)

{
  "results": [
    { /* same shape as POST /research — see Response fields below */ }
  ],
  "next_cursor": "eyJ1IjoiMjAyNi0wNi0wNlQwMTowMDowMFoiLCJpIjozMjF9",
  "sync_timestamp": "2026-06-06T01:24:00.123456+00:00",
  "count": 50
}

Behavior

  • next_cursor is null on the last page.
  • sync_timestamp is REK's server clock at request time — save it and pass as the next call's since.
  • Rows excluded automatically: external-API research stubs and inactive rows.
  • Stable ordering on (updated_at ASC, id ASC); cursor encodes that tuple so resuming a sync never skips or duplicates a row.

GET /properties/{listing_id}

Refetch one property by its stable listing_id (NOT the internal numeric id). Returns the same full-detail shape as one entry from /properties.

Response (404 Not Found)

{ "detail": "Property not found" }

Returned when the listing_id is unknown, the row is inactive, or the row is an external-API research stub (those are hidden from this endpoint).

Recommended sync loop

import httpx, os

REK_BASE = "https://dashboard.rek-partners.com/api/connector"
HEADERS = {"X-API-Key": os.environ["REK_API_KEY"]}

def sync_once(last_sync_timestamp: str | None) -> str:
    """Pull every property updated since last_sync_timestamp, upsert
    locally, return the new sync timestamp to persist."""
    params = {"limit": 50}
    if last_sync_timestamp:
        params["since"] = last_sync_timestamp

    new_timestamp = None
    with httpx.Client(timeout=60.0, headers=HEADERS) as c:
        while True:
            r = c.get(f"{REK_BASE}/properties", params=params)
            r.raise_for_status()
            body = r.json()

            # Capture server time from the FIRST page only — using it
            # as next `since` means rows updated mid-pagination get
            # picked up on the next run.
            if new_timestamp is None:
                new_timestamp = body["sync_timestamp"]

            for prop in body["results"]:
                upsert_property(prop)  # your local DB write

            if not body["next_cursor"]:
                break
            # Drop `since` once paginating — cursor encodes our position.
            params = {"limit": 50, "cursor": body["next_cursor"]}

    return new_timestamp
Tip: save sync_timestamp from the first page of each run as the next call's since — never the last page. This guarantees rows updated between page 1 and page N are picked up next time.

Status codes

CodeWhenBody
200Success — full or delta page returned.Connector response shape (see below).
400Malformed cursor parameter.{"detail":"Invalid cursor: …"}
401Missing / invalid / revoked X-API-Key.{"detail":"Invalid or revoked API key"}
404Single-property lookup on unknown / inactive / stub listing_id.{"detail":"Property not found"}
429Rate limit exceeded.{"detail":"Rate limit exceeded","retry_after_seconds":N}
500Unexpected server-side error.{"detail":"Internal error","request_id":"…"}

Response field reference

The Connector wraps a list of full property payloads in a small envelope. The envelope tells you how to paginate; each property payload matches the Research API shape one-for-one.

Envelope (top-level keys)

KeyTypeDescription
resultsarray<object>The page of properties. Each entry has the per-property keys documented below.
next_cursorstring \| nullOpaque pagination token. Pass it as the next request's cursor to get the following page. null when the page is the last one.
sync_timestampstring (ISO-8601)REK server clock at request time. Save and pass as the next call's since to fetch only what changed in between.
countintNumber of items in this page's results array.

Per-property payload — top-level keys

KeyTypeDescription
idintInternal REK row id. Not stable across deployments — prefer listing_id as your cross-system key.
listing_idstringStable, unique identifier for the property. Use as the primary key in Datahouse.
property_namestring \| nullFriendly name, when available.
property_typestring \| nulle.g. "Multifamily", "Apartment".
urlstring \| nullCanonical source listing URL.
address / city / state / zip_codestringUSPS-style address fields.
priceint \| nullAsking / sale price, USD.
cap_ratefloat \| nullCapitalization rate (computed).
unitsint \| nullNumber of units in the building.
building_size_sfint \| nullTotal square footage.
year_builtint \| nullYear of construction.
price_per_unitint \| nullComputed convenience field.
descriptionstring \| nullMarketing copy from the source listing.
image_url / primary_image_urlstring \| nullCover image URL (aliases of each other).
image_countint \| nullNumber of images on the source listing.
imagesarray<object>Expanded image array: {id, url, caption, is_primary}.
highlightsarray\|object \| nullSource-provided bullet highlights, when present.
latitude / longitudefloat \| nullGeocoded coordinates.
units_detailsarray<object>Per-unit breakdown: {bedrooms, bathrooms, square_feet, rent, seventh_percentile_rent, unit_count, unit_type, updated_at}.
brokerobject \| null{name, company, contact_id} — when listing carries broker info.
public_recordsobject \| nullFull provider blob (Bridge for LA / qPublic for GA). Large; inspect for fields not promoted to the summary.
public_records_summaryobject \| nullPromoted public-records fields — see next table.
sourcestringWhere REK first scraped this property: "loopnet", "redfin", or "zillow". External-API stubs are filtered out.
created_at / updated_atstring (ISO-8601)Row creation and last-modification timestamps. Use updated_at for your local "last modified" mirror.

public_records_summary — promoted fields

KeyTypeNotes
parcel_idstringCounty APN / parcel number.
owner_namestring \| nullCurrent owner(s) of record.
owner_mailing_addressstring \| nullTax-mailing address on file with the assessor (qPublic counties).
assessed_value_totalint \| nullTaxable assessed value, USD.
market_value_totalint \| nullEstimated market value, USD.
tax_amount_currentint \| nullMost recent annual tax bill, USD.
annual_tax_amountint \| nullSame as tax_amount_current; derived from tax_history when the canonical column is null.
tax_yearint \| nullCalendar year of the tax amount.
effective_year_builtint \| nullPer the assessor; may differ from year_built.
zoning_codestring \| nullLocal zoning code (e.g. "LAR1", "RG2").
last_sale_datestring \| nullMost recent transfer date (ISO YYYY-MM-DD).
last_sale_amountint \| nullSale price recorded with that transfer.
tax_districtstring \| nullCounty tax-district code. qPublic counties only.
homesteadstring \| null"Y" or "N" — homestead-exemption flag. qPublic only.
neighborhoodstring \| nullCounty-internal neighborhood code (e.g. "CA02"). qPublic only.
Schema additions are additive. REK occasionally adds new keys to the per-property payload. Datahouse should ignore unknown keys gracefully and persist public_records / source_specific as opaque JSONB.