Investorlift DS
  • Getting started
  • The contract
  • Errors
  • API reference
IdempotencyRate limitsGlossaryReading a scoreThe History CallBatch scoringVersioning & deprecation
The contract

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; 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.

Code
{ "address": "742 Evergreen Terrace, Springfield, IL 62701" }
Code
{ "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:

kindWhat it means
standard-saleAn ordinary arms-length sale
foreclosureAn involuntary taking
bank-repoA lender reselling a repossessed property
family-transferA transfer between family members
new-constructionA first conveyance from a builder
unknownThe 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.

MemberWhat it is
owned_sinceThe recorded date the current ownership period runs from — the newest dated event in transactions[] above. null when the chain carries no dated event.
days_ownedWhole 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_availableWhether a recorded loan for this property is available to us.
basisA 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

MemberWhat it is
coverage_knowntrue only when at least one permit record was actually returned. Never inferred from an empty result.
statementA 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, demolitionThe 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_permitWhether 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:

MemberWhat it is
work_statuscompleted, inactive, open, in-review, or unknown.
job_valueThe recorded value of the work, or null. null is ordinary — most permit records carry no job value at all.
file_date, issue_date, final_dateRecorded dates, or null.
construction_durationA 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

MemberWhat it is
stateactive, sold, failed, off-market, rental or unknown.
rental_listingWhether the listing is a rental. null means the record does not say.
days_on_marketDays on market for a sale. Sparse — most records carry none.
original_listing_date, failed_listing_date, sold_price, sold_dateAs recorded, or null.
list_price_high / list_price_high_date, list_price_low / list_price_low_dateThe high- and low-water asking prices and when each was recorded.
price_reducedWhether 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 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 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.

Last modified on September 1, 2026
Reading a scoreBatch scoring
On this page
  • Input is exactly one of address or property_id
    • The two arms behave differently, and the response says which one ran
  • ⚠️ A property with nothing on file is a charged answer
  • transactions[] — the chain of title
    • What these categories are, honestly
    • Dates the record does not carry
  • flips[] — detected buy/sell pairs
  • seller_position — how long they have held it, and what we cannot tell you
    • There is no payoff estimate and no equity estimate, and here is exactly why
  • permits — recorded work, and an honest word about coverage
    • "We cannot get permit records for this" is never "this property has no permits"
    • permits[] is a selection, not the whole file
  • market_status — the current listing story
  • summary — the same facts, shaped for a CRM field
  • Names never appear
  • Errors
  • Idempotency
  • What this endpoint does not return yet
JSON
JSON