# The History Call

`POST /v1/history` returns one property's recorded transaction story: the chain of title
newest first, and any buy/sell flips detected in it. It is the call for the moment a
comparable sale looks too high — the one that turns *"that comp is way above the others"*
into *"that comp was itself a flip, bought in July and resold in February by a company"*
before it anchors your offer.

It is a **Metered Call** and it costs **one Credit**, the same at every Tier. The
member-by-member schema is in the [API reference](/api); this page is what the call is, what
it promises, and — the part you should read before you send one — what it charges for.

## Input is exactly one of `address` or `property_id`

Send **one** of them. A body carrying **both**, or **neither**, is rejected with **400** and
**nothing is charged**.

```json
{ "address": "742 Evergreen Terrace, Springfield, IL 62701" }
```

```json
{ "property_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90" }
```

**Presence of the member is what counts, not whether its value is usable.** Sending an empty
`address` alongside a real `property_id` counts as sending both, and is rejected. That is
deliberate: choosing one for you would mean answering about a different house from the one
your empty field was reaching for, and you would have no way to tell.

This endpoint accepts **no other members**. `condition`, `rehab_budget`, `target_margin` and
`overrides` belong to the Scored Call and are rejected here.

### The two arms behave differently, and the response says which one ran

- **By `address`** — the address is resolved exactly the way a Scored Call resolves it: the
  same matching, the same refusals for an ambiguous address, the same coverage rules.
- **By `property_id`** — take the identifier straight off any comparable property in a Scored
  Call answer, or off a subject you have already scored. **No address matching is performed
  on this arm at all.** The record is looked up directly.

Either way the response carries a `subject_property` block — **the same block a Scored Call
publishes** — so you can always see which house the answer is about. On the `property_id`
arm its `address_quality.code` reads `identified-by-property-id`, which says plainly that no
house number and no ZIP code were compared.

## ⚠️ A property with nothing on file is a charged answer

**If the county records are available and hold no recorded transactions for the property, the
call returns 200 with an empty `transactions` array — and one Credit is consumed.**

This is stated here rather than left for you to discover from a line on your Ledger. *"We
checked; nothing on file"* is an answer: the records were read, and that is the work you paid
for. A property with a clean, short title history is a real and useful finding.

Every such response carries `records_statement`, a plain sentence saying exactly that.

**A failure is a different thing entirely, and it is not charged.** If the records could not
be read — the service was unavailable, timed out, or returned an incomplete record — the call
returns a 5xx and the Credit is **returned to your Account as a Refund**. You will never be
charged for an answer we did not obtain, and an incomplete record is never quietly reported
as an empty history.

## `transactions[]` — the chain of title

Newest first. Each event carries its recorded date and price — **either is `null` where the
record carries none, and neither absence removes the event from the chain** — and a `kind`
from a small fixed vocabulary:

| `kind` | What it means |
| --- | --- |
| `standard-sale` | An ordinary arms-length sale |
| `foreclosure` | An involuntary taking |
| `bank-repo` | A lender reselling a repossessed property |
| `family-transfer` | A transfer between family members |
| `new-construction` | A first conveyance from a builder |
| `unknown` | The record did not establish a category |

**`unknown` is a real answer, not a placeholder.** County records are inconsistent, and where
one does not establish what kind of transaction occurred, this API says so rather than
guessing. Treat `unknown` as "not established", never as "probably a sale".

Each event also carries a `buyer_type` — `individual`, `company`, `trust` or `unknown`.

### What these categories are, honestly

**Every category in this response is a derived conclusion, not a fact the county asserted.**
They are this API's reading of the recorded documents. Where the county itself drew a
conclusion — that a transfer was a foreclosure, say — that conclusion is passed through and is
also published as its own field. Where it did not, the category is derived, and where nothing
supports a derivation the answer is `unknown`.

Two consequences worth planning for:

- **A transfer recorded at a nominal price is never reported as a `standard-sale`.** A deed
  recorded at one dollar is a transfer of title, not what the house changed hands for, and
  reporting it as a sale would put a fabricated number in front of you.
- **A price is always published with its `price_is_estimated` flag beside it.** When that flag
  is `true` the price is the record's own estimate, not a recorded consideration.

### Dates the record does not carry

Some recorded events carry no sale date. They are **still listed** — dropping them would hide
real transactions, including foreclosures — but they cannot be ordered, so they appear **last**,
after every event that can be. Their `sale_date` is `null`. They are never presented as the
oldest event on the property.

## `flips[]` — detected buy/sell pairs

Each entry carries the two dates, the two prices, `days_held`, `percentage_gain` (a fraction —
`0.4195` is a 41.95% gain), and `seller_posture` — `professional`, `private` or `unknown`.
It is present on every entry: `professional` is a company or a trust selling at the end of the
hold, `private` is an individual.

**`unknown` is a real answer, not a placeholder.** It means no seller was recorded on the sell
leg, which is ordinary and common. A rule written as *"if it is not `professional` it is
`private`"* labels every unrecorded seller an individual — a confident claim about a real
person's house that the record does not support.

- **A negative `percentage_gain` is a real outcome.** A flip that lost money is reported as it
  happened.
- **A zero-day hold is real too** — a same-day buy and sell is a simultaneous close.
- **A flip is a short hold, and that rule decides most of them.** A buy and a resale are
  reported as a flip only when **540 days or fewer** separate the two recorded sale dates. The
  boundary is inclusive — a hold of exactly 540 days is a flip — so a property bought in 2015
  and resold in 2024 is ownership rather than a trade, and never appears here however large the
  gain.
- A pair is reported only when **both legs carry a real market price**, because a percentage
  gain computed against a nominal price would be a fabricated number.
- A pair involving a **family transfer** is not reported as a flip.

## `seller_position` — how long they have held it, and what we cannot tell you

This chapter answers one question honestly and refuses to answer another one at all.

| Member | What it is |
|---|---|
| `owned_since` | The recorded date the current ownership period runs from — the newest **dated** event in `transactions[]` above. `null` when the chain carries no dated event. |
| `days_owned` | Whole days from `owned_since` to now. `null` when ownership length could not be established, and `null` rather than a negative number if the record carries a sale date in the future. |
| `recorded_loan_available` | Whether a recorded loan for this property is available to us. |
| `basis` | A plain sentence stating what this chapter rests on. Fixed wording, selected from a table — it never repeats your address or identifier. |

### There is no payoff estimate and no equity estimate, and here is exactly why

`recorded_loan_available` is **`false` on every answer today**, and that is a measured statement
rather than a placeholder. This chapter was specified to publish a payoff **range** and an
equity **range** derived from the loan recorded at the property's last sale. Before building
it we went looking for that loan record — through the property data service's full published
route list, through thirty-one candidate record-set names, and by matching every one of 579
distinct column names it returns across a sample of properties. **There is no per-property
loan, mortgage or lien record available to us**: no loan amount, no loan date, no lender, no
interest rate, no maturity and no payoff.

So no payoff figure and no equity figure is published, and **nothing is inferred from that
absence**. In particular this answer never says a property is owned free and clear, and never
says a purchase was made in cash. A missing record means we did not find one to look at — it
does not mean no loan exists. A confident financial claim about someone's house, built on the
absence of a column, is exactly the kind of answer this API exists not to give.

**Two things are worth knowing even for the day a loan record does become available**, because
they are limits the question itself carries rather than limits of our data:

- A payoff derived from the loan recorded at the last sale is an **estimate about the past**.
  **Refinances after that sale are invisible to the public record**, so an owner who
  refinanced would carry a balance the recorded loan cannot show.
- For that reason such an estimate would be published as a **range**, never as a point figure,
  and it would state its basis in the answer.

Until then, what this chapter reports is ownership length, taken from the chain of title above
so the two can never disagree.

## `permits` — recorded work, and an honest word about coverage

| Member | What it is |
|---|---|
| `coverage_known` | `true` **only when at least one permit record was actually returned.** Never inferred from an empty result. |
| `statement` | A plain sentence you can show a colleague, saying what was retrieved or what could not be. |
| `permits[]` | The permits themselves — see the selection rule below. |
| `roof`, `hvac`, `water_heater`, `electrical`, `plumbing`, `demolition` | The most recent recorded permit in each category, as `most_recent_date`, `most_recent_job_value` and `work_status`. All three are `null` where no permit for that category came back. |
| `has_open_permit` | Whether work is still open. See below — this is three-valued on purpose. |

### "We cannot get permit records for this" is never "this property has no permits"

**This is a promise about honesty, not a footnote.** There is no signal available to us that
distinguishes *this property has no permits* from *this county publishes no permit records*.
We know that because we checked: an identifier that **does not exist at all** gets the same
cheerful empty answer as a real property with a clean record. Three completely different
facts, one identical response.

So this endpoint reports **records-unavailable** and never reports an absence of permits.
`coverage_known` is `false`, `permits` is empty, and `statement` says so in words. **A permit
lookup that fails outright is reported the same way and the call is still charged** — the
statement it produces is identical, because it is identical in truth: we cannot report permit
records for this property. The rest of your answer — the chain of title, the flips, the
listing story — is unaffected and was never at risk.

**Read `coverage_known`, not the length of the array.** An empty array on its own means
nothing, and that is the whole reason the two members are published together.

### `permits[]` is a selection, not the whole file

It carries the **most recent permit in each of the six categories above**, plus **every permit
still open** and **every demolition**. A property with eighty-four recorded permits does not
hand you eighty-four rows. Each entry carries:

| Member | What it is |
|---|---|
| `work_status` | `completed`, `inactive`, `open`, `in-review`, or `unknown`. |
| `job_value` | The recorded value of the work, or `null`. **`null` is ordinary** — most permit records carry no job value at all. |
| `file_date`, `issue_date`, `final_date` | Recorded dates, or `null`. |
| `construction_duration` | A recorded duration figure, or `null`. **It carries no unit, and that is deliberate** — see below. |
| `tags[]` | What the work was, as a fixed set of categories. Never the county's free-text description. |

- **`work_status: unknown` is not `completed`.** A permit whose status the county did not state
  is a permit we cannot tell you is finished.
- **`has_open_permit` is three-valued.** `true` when a permit is open or in review. `false`
  **only** when records were retrieved and every permit's status was readable. `null` whenever
  records were unavailable **or** any permit's status was unreadable — because `false` claims
  we checked, and an unreadable status has not been checked.
- **`construction_duration` has no unit and we will not invent one.** The record carries a bare
  number with nothing anywhere naming days, weeks, months or inspection cycles. Rather than
  guess, we publish the figure as it is recorded and say plainly that its unit is not
  established. If that changes, the unit will be added then.

## `market_status` — the current listing story

| Member | What it is |
|---|---|
| `state` | `active`, `sold`, `failed`, `off-market`, `rental` or `unknown`. |
| `rental_listing` | Whether the listing is a rental. `null` means the record does not say. |
| `days_on_market` | Days on market for a **sale**. Sparse — most records carry none. |
| `original_listing_date`, `failed_listing_date`, `sold_price`, `sold_date` | As recorded, or `null`. |
| `list_price_high` / `list_price_high_date`, `list_price_low` / `list_price_low_date` | The high- and low-water asking prices and when each was recorded. |
| `price_reduced` | Whether the asking price was cut. Three-valued — see below. |

- **A rental listing is not a sale listing.** When the record marks a listing as a rental,
  `state` is `rental` and the sale-specific figures — days on market, sold price, sold date —
  are withheld rather than reported as if they described a sale.
- **`unknown` is a real answer.** The listing status vocabulary published by the underlying
  record is not a fixed list we can rely on, so a status we have not seen before is reported
  as `unknown` rather than filed under the nearest-looking category. A cancelled listing in
  particular is **not** reported as `failed`: a withdrawn listing may simply have been
  relisted, and `failed_listing_date` is published separately for the case the record does
  establish.
- **`price_reduced` is `true` only when the low price was recorded *after* the high one.** The
  two figures are high- and low-water marks with no order of their own, so a low price
  recorded first is a price that went **up**. It is `null` whenever any of the four figures is
  missing, because "we could not tell" is not "it was not cut" — and missing is the common
  case here.

## `summary` — the same facts, shaped for a CRM field

A short set of at-a-glance values drawn from the chapters above: `has_recorded_transactions`,
`last_recorded_sale_kind`, `last_recorded_buyer_type`, `flip_detected`, `long_tenured_owner`,
`permit_records_available`, `open_permit_work`, `demolition_recorded`, `market_state`,
`asking_price_reduced` and `recorded_loan_available`.

Every member is a boolean, a fixed category or `null` — **no free text and no sentence**, so
these are safe to map straight onto a field or branch on in an agent chain.

Two rules make them trustworthy:

- **The summary is computed from the chapters above it**, not from a second reading of the
  underlying record. It is structurally incapable of disagreeing with the detail it summarises.
- **A summary value whose chapter established nothing is `null` or `unknown`, never `false`.**
  `false` means *we checked, and no*. When permit records could not be retrieved,
  `open_permit_work` and `demolition_recorded` are `null` — not `false`.

`long_tenured_owner` is `true` when `days_owned` reaches a configured threshold, and `null` when
ownership length could not be established at all.

## Names never appear

The underlying county records contain buyer, seller and owner names, their mailing addresses,
and recorder document numbers. **None of it appears in the response.**

Those columns are read in memory only, to reach a conclusion — *the buyer was a company* — and
then discarded. What is published is the conclusion: a boolean, a count, a duration, a range,
or a fixed category. No name, no mailing address and no document number reaches a response, an
error body, a log line or an alert.

## Errors

The [error contract](/errors) applies unchanged: the same rows, the same distinct `type`
values, the same Refund behaviour, and **every response carries a Trace ID** in the `trace-id`
header. Quote it to support and it identifies the call.

Two things specific to this endpoint:

- **`property-not-found`** (422) — no property record could be found for the identifier on
  your request. It is decided after the Spend, so the Credit is **Refunded**. It is its own
  `type`, distinct from "which one did you mean" and from "not that state yet".
- **`unscorable-address` is never returned by this endpoint.** Nothing is being scored here,
  so that row would describe something that did not happen.

## Idempotency

Send an `Idempotency-Key` and a retry can never charge you twice — the same seven promises the
[idempotency contract](/idempotency) makes, unchanged.

One clarification specific to this endpoint: **the same key and the same body sent to
`/v1/predict` is a conflict, not a replay.** The two endpoints answer different questions, and
a key is scoped to the request you made, not to the bytes you sent.

## What this endpoint does not return yet

The response carries `subject_property`, `records_statement`, `transactions`, `flips`,
`seller_position`, `permits`, `market_status` and `summary`, and **nothing else**.

The rule that governed the six chapters above has not changed, only what it applies to:
**a chapter that establishes nothing publishes what it could not establish, rather than a
zero.** You will not find `permits: []` standing alone here — the permits chapter carries
`coverage_known` and a plain sentence beside its list, and `seller_position` carries
`recorded_loan_available` beside its ownership figures. An empty array on its own would be a
claim; a stated absence is not.

Three things are deliberately still absent, and each is absent for a measured reason rather
than because nobody got to it:

- **No payoff figure and no equity figure**, and no range of either — not as a number, not as
  `null`, not as an empty object. See the `seller_position` section above.
- **No permit number and no permit reference id.** A permit number plus the county it was
  issued in retrieves a public record naming the applicant, and this endpoint publishes no
  applicant. Withholding the name and handing over the lookup that retrieves it would not be
  withholding anything.
- **No count of permits.** The underlying record's own count is a page size rather than a
  total, so reporting it as "how many permits this property has" would invent a certainty.

When further chapters ship they will appear as new members, which is an additive change under
the [versioning policy](/versioning).
