Runs and polling
Kick off once, GET the same resource until status is terminal, then read the results on that payload.
A run takes a minute or two: it gathers listings, scores them, and has a model read each finalist's words and photos. No request is held open that long. Starting one queues work and answers at once with where to read (Location) and how long to wait (Retry-After). You poll that same URL until status is terminal; when it is, the results are on the same response. There is no second "fetch results" endpoint. To be told when it ends instead of asking, name a callback URL: Get told when a run finishes.
Two paths: product or headless
| Path | Kick off | Poll (same URL) | Shows in the app? |
|---|---|---|---|
| Product (prefer this) | POST /api/v1/searches/{searchId}/runs | GET /api/v1/runs/{runId} until complete / failed / canceled | Yes — Searches and Clients, as a run the agent started. |
| Headless | POST /api/v1/search | GET the Location (an execution) until succeeded / failed / canceled | No. An execution is not a run of a search. Nothing appears under Searches or Clients. |
If you triggered a search through the API and expected homes in the platform: you used the headless door. Run it through a search's runs, or walk the full journey. Headless is for a server that only wants homes back.
The contract (for a person or an AI)
- Kick off.
POSTwith anIdempotency-Key. Expect202(live market) or201(data set, already complete). ReadLocationandRetry-Afterfrom the response headers. - Poll.
GETthatLocation(or the run or execution id you already have). HonorRetry-Afterbetween reads (2 seconds while unfinished). Do not poll faster — a run changes about once a second at most. - Stop when terminal. Product path:
complete,failed, orcanceled. Headless:succeeded,failed, orcanceled. While unfinished, progress fields (step/stepLabel/progress, or the run's status) are for narration only. - Read the payload on the same GET. Product:
results(homes, scores, reasons) are on the run oncestatusiscomplete. Headless:result(withshortlist) is on the execution oncestatusissucceeded. No extra call.
// Product path — same pattern works for an AI loop.
const headers = { Authorization: `Bearer ${process.env.LIVIA_API_KEY}` }
const started = await fetch(`https://liviamatch.com/api/v1/searches/${searchId}/runs`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': `run-${searchId}-1` },
})
const location = started.headers.get('Location')! // /api/v1/runs/{id}
const terminal = new Set(['complete', 'failed', 'canceled'])
let run
for (let attempt = 0; attempt < 150; attempt++) {
const response = await fetch(`https://liviamatch.com${location}`, { headers })
run = (await response.json()).data
if (terminal.has(run.status)) break
const wait = Number(response.headers.get('Retry-After') ?? 2)
await new Promise((resolve) => setTimeout(resolve, wait * 1000))
}
if (run.status !== 'complete') throw new Error(`ended as ${run.status}`)
// The results are on this same payload.
for (const home of run.results) console.log(home.rank, home.address, home.score)What an AI should do
Idempotency-Key, then loop on the same GET. Branch only on status. Treat Retry-After as the sleep. Cap attempts (a few minutes). On complete / succeeded, read results / result.shortlist on that response; on failed, read error and stop or retry with a new key.Status values
Product runs use running → complete | failed | canceled. Headless executions use queued → running → succeeded | failed | canceled (with step / progress while unfinished).
| Headless status | Means | What to do |
|---|---|---|
queued | Waiting for a driver to claim it. | Keep polling. |
running | A driver is working. step, stepLabel and progress say where. | Keep polling. |
succeeded | Done. result is present and final. | Read result on this response. |
failed | It could not finish. error has a code and a digest. | Read error; start again if it is a fault on our side, quoting the digest if it keeps happening. |
canceled | Someone called cancel. | Nothing. |
Polling details
- Always
GETtheLocationfrom the kickoff (or the run or execution you already hold). Same resource for progress and results. - Wait
Retry-Afterseconds between reads. While unfinished every read answers with it (2 seconds). - Give up after a few minutes of unfinished status and read once more before you call it lost. A typical run is done in two.
- A run keeps itself moving: when one slice of work runs out of time, it starts the next. A run nobody is driving (a crash, a deploy) is picked up again by the next read, and an hourly sweep is the backstop for runs nobody reads, so keep polling until the run ends.
How many at once
An owner may have 3 runs queued or running at once, and may start 60 an hour. Past either, a start answers 429 (too-many-active or rate-limited) with Retry-After. Finished and canceled runs do not count.
What a result holds
- Product path:
resultson the run oncestatusiscomplete. - Headless:
result.shortlist— the homes that fit, best first, each withscore,scores,take,pros,cons,commutesand a photo gallery. excluded: homes the run graded that did not make the shortlist, each with acategoryand areasonin a sentence.funnel: how many homes each stage kept or dropped.notesandcredits: every stand-in the run used, and the attribution to show wherever the result is shown.compliance: a fair housing checklist, or"not_reviewed"when the reviewer did not run. It is never a clearance.
A headless run's whole pool of considered homes is separately readable for 30 days after it finishes. The result stays forever.
Canceling
Headless: POST .../cancel ends a queued or running execution at once. Product: POST to the run's execution.links.cancel. A call to a provider that was already out may still finish, and what the run spent so far stays on the ledger. Canceling a run that already finished is 409 already-finished.