CSV export
Stream a flat-file projection of every parcel matching a recipe — addresses, signals, score. Drop straight into a mail-merge template or BI tool without remapping. v1 ships one-shot CSV; scheduled delivery is a v1.x line item, use webhooks if you need streaming.
csv_export feature key (Operator, Team, Custom). Starter requests with format=csv return 403 forbidden_tier with an upgrade-prompt URL. See Errors.Overview
CSV is not a separate endpoint family — it's a format=csv query param on the existing list, single-folio, and batch endpoints. Same auth, same filter DSL, same tier gating — just a different response codec.
Three use cases:
- Mail-merge / outbound — list mode (
GET /v1/leads?format=csv) for the wholesaler send. - Pipeline re-hydration — batch mode (
POST /v1/leads/batch?format=csv) when you have a saved folio list and want fresh detail. - Single-parcel export — detail mode (
GET /v1/leads/{folio}?format=csv) for handoff to a non-API operator.
Endpoint shape
Three flavors of the same CSV schema. The response is streaming Content-Type: text/csv; charset=utf-8.
# List CSV — every parcel matching a recipe.curl 'https://api.terminal.house/v1/leads?recipe=oos_long_tenure_high_signal&format=csv' \ -H "Authorization: Bearer $TERMINAL_HOUSE_API_KEY" \ -o leads.csv # Single-row CSV for one parcel.curl 'https://api.terminal.house/v1/leads/30-3122-008-0440?format=csv' \ -H "Authorization: Bearer $TERMINAL_HOUSE_API_KEY" # Bulk CSV for a known folio list.curl -X POST 'https://api.terminal.house/v1/leads/batch?format=csv' \ -H "Authorization: Bearer $TERMINAL_HOUSE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"folios": ["30-3122-008-0440", "30-4011-002-0210", ...]}'
Python · streaming consumer
Large exports stream — don't buffer the full response into memory. Use httpx's streaming + the stdlib csv.DictReader:
import osimport csvimport httpx with httpx.stream( "GET", "https://api.terminal.house/v1/leads", params={"recipe": "oos_long_tenure_high_signal", "format": "csv"}, headers={"Authorization": f"Bearer {os.environ['TERMINAL_HOUSE_API_KEY']}"}, timeout=60.0,) as resp: resp.raise_for_status() reader = csv.DictReader(resp.iter_lines()) for row in reader: print(row["folio"], row["composite_score"], row["signals"])
Schema (locked v1)
11 columns, locked at v1. New columns ship additively at the end of the row — your existing mail-merge template will keep resolving the same fields. We will never insert, rename, or remove a v1 column without a v2.
| Column | Type | Description |
|---|---|---|
folio | string | County parcel identifier. The primary key. |
address | string | Normalized street address (CSV-quoted because addresses contain commas). |
year_built | integer | From the property appraiser's building record. |
sq_ft | integer | Heated living area square footage. |
beds | integer | Bedroom count. |
baths | decimal | Bathroom count (half-baths render as 1.5, 2.5, etc.). |
tenure_years | integer | Years since the most recent qualified sale. The tenure_years_long signal fires above the tenure threshold (Tri-County defaults). |
homestead_exempt | boolean | Whether the parcel currently has a homestead exemption on record. true / false lower-case. |
signals | string | ;-separated list of signal types that fired on this parcel. Just the type names — for scores + evidence use the JSON endpoint with include=signals. |
composite_score | decimal | Composite Phase 0 heuristic score, range 0.0 – 1.0. See composite scoring. |
first_observed | timestamp | RFC3339 UTC — the moment the parcel first entered the feed. Useful for de-duping incremental pulls. |
Sample row
The first row is the locked column header. Body rows render one parcel each.
folio,address,year_built,sq_ft,beds,baths,tenure_years,homestead_exempt,signals,composite_score,first_observed30-3122-008-0440,"4567 NW 22nd Av, Miami FL 33125",1962,1840,3,2,14,false,out_of_state_owner;tenure_years_long;no_homestead;code_violation,0.78,2026-05-12T08:14:00Z30-4011-002-0210,"1219 SW 8th St, Miami FL 33135",1955,1320,2,1,21,false,tenure_years_long;lis_pendens;tax_delinquent,0.84,2026-05-18T11:02:00Z
Quota + size
Every row in the CSV consumes one lead-view against the monthly quota, same as the JSON path. A 5,000-row export on Operator (2,000/mo cap) won't complete — at row 2,000 the stream terminates with a trailing comment row indicating the quota was reached:
...30-7331-009-0112,"2300 NW 12th Av, Miami FL 33136",1973,1610,3,2,9,false,code_violation;no_homestead,0.62,2026-05-19T14:51:00Z# quota_exceeded: lead-views exhausted at row 2000 of 4982 matches# next reset: 2026-06-12T00:00:00Z# upgrade: https://terminal.house/api-keys?action=upgrade
Plan big back-fills around the billing-anniversary reset, or upgrade tier for a one-month burst. See per-tier caps.
Calendar-day idempotency still applies
Re-running the same CSV recipe later the same UTC day does NOT re-consume lead-views for parcels you've already seen — the same (folio, calendar-day-UTC) idempotency rule from the JSON path applies. Useful for iterating on filter recipes without burning quota on each tweak.
Scheduled exports
A common interim pattern: cron a CSV pull on your side and diff against last run. The first_observed column makes incremental diff trivial — only parcels with a first_observed after your last run are new to the feed.