Batch & CSV
Score a whole book of properties in one call, then read the results straight back into the spreadsheet you started from.
POST/v1/bulk-underwrite takes up to 500 properties in a single request. Each item is a full underwrite request — the same fields as the single endpoint — and every item is scored independently.
curl -X POST https://api.territas.com/v1/bulk-underwrite \
-H "X-API-Key: $TERRITAS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "address": "…", "purchase_price": 450000, "annual_rent": 42000, "annual_taxes": 6200, "annual_insurance_base": 1800 },
{ "address": "…", "purchase_price": 610000, "annual_rent": 54000, "annual_taxes": 8100, "annual_insurance_base": 2400 }
],
"concurrency": 8
}'The response: a summary and per-row results
You always get 200 OK, even on partial failure. The summary counts how many succeeded and how long the batch took; results holds one entry per input, in the same order. Each entry is tagged with ok — a success carries result, a failure carries a typed error:
{
"summary": { "count": 2, "ok": 1, "failed": 1, "elapsed_ms": 5120.4 },
"results": [
{
"ok": true,
"address": "…",
"result": { "flood": { "fema_zone": "AE", "…": "…" }, "…": "…" }
},
{
"ok": false,
"address": "…",
"error": { "type": "geocode_failed", "detail": "No match for address" }
}
]
}Tuning a large batch
- concurrency (1–32) sets how many addresses are underwritten at once. Higher finishes sooner but presses harder on the federal endpoints; leave it unset for a safe default.
- prefer_county_rates uses the county NFIP cohort instead of the state one — more accurate, but the lookup is slower. It’s meant for exactly this batch path, not the interactive single call.
- Give the request a long client timeout. A few hundred live underwrites take minutes, not seconds.
CSV in, spreadsheet out
The whole point of batch is the analyst’s loop: start from a CSV of addresses and deal economics, score them, and open the results in Excel. This script does exactly that — reads book.csv, underwrites every row, and writes a flat book_scored.csv you can double-click:
import csv, os, requests
# Input CSV columns: address, purchase_price, annual_rent, annual_taxes, annual_insurance_base
with open("book.csv", newline="") as f:
items = [
{
"address": row["address"],
"purchase_price": float(row["purchase_price"]),
"annual_rent": float(row["annual_rent"]),
"annual_taxes": float(row["annual_taxes"]),
"annual_insurance_base": float(row["annual_insurance_base"]),
# county NFIP cohort is more accurate but slower — fine for a batch
"prefer_county_rates": True,
}
for row in csv.DictReader(f)
]
resp = requests.post(
"https://api.territas.com/v1/bulk-underwrite",
headers={"X-API-Key": os.environ["TERRITAS_API_KEY"]},
json={"items": items, "concurrency": 8},
timeout=600,
)
resp.raise_for_status()
data = resp.json()
print(data["summary"]) # {'count': …, 'ok': …, 'failed': …, 'elapsed_ms': …}
# Flatten each result into one spreadsheet row (Excel opens this directly)
with open("book_scored.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["address", "fema_zone", "flood_required", "wildfire_rating", "median_home_value", "error"])
for row in data["results"]:
if row["ok"]:
r = row["result"]
w.writerow([
row["address"],
r["flood"]["fema_zone"],
r["flood_insurance_requirement"]["required"],
r["climate_wildfire"]["wildfire_risk_rating"],
r["demographics"]["median_home_value"],
"",
])
else:
w.writerow([row["address"], "", "", "", "", row["error"]["type"]])Add whatever columns your model needs — every field in the reference is available on each result. Rows that failed keep their address and an error type, so nothing silently disappears from your book.

