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"}'
401 Unauthorized.
Revoked keys are rejected immediately on the next request.
Base URL
| Environment | URL |
|---|---|
| Production | https://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
| Field | Type | Required | Notes |
|---|---|---|---|
address | string | yes | Street address (no city/state suffix). |
city | string | yes | e.g. "Atlanta", "Los Angeles". |
state | string | yes | Two-letter USPS code ("GA", "CA", …). |
zip_code | string | yes | 5-digit ZIP. Improves match accuracy when sources disagree on address normalization. |
callback_url | string | no | HTTPS URL where the completed payload will be POSTed when ready. |
force_refresh | boolean | no | true 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
| Status | Meaning |
|---|---|
| queued | Job accepted, not yet started. |
| running | A worker is currently scraping / merging. |
| completed | Done. Payload present. |
| failed | Unrecoverable error. Inspect error field. |
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
| Header | Value |
|---|---|
Content-Type | application/json |
X-REK-Signature | HMAC-SHA256 hex digest of the raw request body |
X-REK-Job-Id | The 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)
Status codes
| Code | When | Body |
|---|---|---|
| 200 | Synchronous success — cache hit on /research or a completed /jobs/{id}. | Full research payload. |
| 202 | Queued — /research miss. | {job_id, poll_url, eta_seconds} |
| 401 | Missing / invalid / revoked X-API-Key. | {"detail":"Invalid or revoked API key"} |
| 404 | Unknown job_id on a polling call. | {"detail":"Job not found"} |
| 429 | Rate limit exceeded. | {"detail":"Rate limit exceeded","retry_after_seconds":N} |
| 500 | Unexpected 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
| Key | Type | Description |
|---|---|---|
property | object | Canonical property columns (address, beds, baths, sqft, year_built, price, image_url, latitude/longitude, etc.). |
redfin | object \| null | Listing details from Redfin: price, price_per_sqft, listing_type, listing_date, listing_agent, beds, baths, sqft, days_on_redfin, last_updated, last_checked. |
zillow | object \| null | Same shape as redfin — populated only when Redfin has no listing for this address. |
public_records | object \| null | Full provider blob (Bridge or qPublic shape). Large. Inspect when you need a field that isn't promoted. |
public_records_summary | object \| null | Promoted fields lifted out of public_records — see table below. |
sources_used | string[] | Which providers contributed to this payload, e.g. ["redfin","public_records"]. |
listing_source | string \| null | "redfin" or "zillow" — which listing block carried the data. |
listing_url | string \| null | Canonical URL on the listing source. |
public_records_summary — promoted fields
| Key | Type | Notes |
|---|---|---|
parcel_id | string | County APN / parcel number. |
owner_name | string \| null | Current owner(s) of record. |
owner_mailing_address | string \| null | Tax-mailing address on file with the assessor (qPublic counties). |
assessed_value_total | int \| null | Taxable assessed value, USD. |
market_value_total | int \| null | Estimated market value, USD. |
tax_amount_current | int \| null | Most recent annual tax bill, USD. |
annual_tax_amount | int \| null | Same as tax_amount_current; derived from tax_history when the canonical column is null. |
tax_year | int \| null | Calendar year of the tax amount. |
effective_year_built | int \| null | Per the assessor; may differ from property.year_built. |
zoning_code | string \| null | Local zoning code (e.g. "LAR1", "RG2"). |
last_sale_date | string \| null | Most recent transfer date (ISO YYYY-MM-DD). |
last_sale_amount | int \| null | Sale price recorded with that transfer. |
tax_district | string \| null | County tax-district code. qPublic counties only. |
homestead | string \| null | "Y" or "N" — homestead-exemption flag. qPublic only. |
neighborhood | string \| null | County-internal neighborhood code (e.g. "CA02"). qPublic only. |
null for LA / CA addresses since
Bridge doesn't expose them.