Skip to the documentation

Browser-only

Endpoints the signed-in app uses. An API key is refused.

Serve a stored picture

GET/api/v1/images/{id}Browser only

A headshot or logo uploaded in Settings, by id, to any signed-in person. The id is the only secret, and it is a random UUID. Not for API keys.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
idPathstring (uuid)RequiredThe image's id.

Responses

200

The bytes with their stored Content-Type, Cache-Control: private, max-age=31536000, immutable and Cross-Origin-Resource-Policy: same-origin.

The bytes, with their own content type.

Errors

StatusCodeWhen
404not-foundNo such image, or a malformed id.

Any call of this kind can also answer401 unauthenticated500 internal

Serve a stored picture to a client

GET/api/v1/client/images/{id}Browser only

The same picture door for a signed-in client of an agent. It answers only for pictures on the face of an agent whose shortlist the client can see: that agent's headshot, or a logo their shortlists wear. Anything else answers the same 404 as a picture that never existed.

A signed-in client of an agent. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
idPathstring (uuid)RequiredThe image's id.

Responses

200

The bytes with their stored Content-Type, Cache-Control: private, max-age=31536000, immutable and Cross-Origin-Resource-Policy: same-origin.

The bytes, with their own content type.

Errors

StatusCodeWhen
404not-foundNo such image, or one this client may not see.

Any call of this kind can also answer401 unauthenticated500 internal

Render the share studio's draft as a PDF

POST/api/v1/search-runs/{id}/package-pdfBrowser only

The agent's pre-send PDF of one of their own runs: the studio's draft as a tagged PDF in their brand, with a QR code to the public link when one is on. state is the share document (version 3, spec 023), or a version 2 package from a page loaded before it, which is upgraded. The draft is cleaned against the run first (homes the run lacks, hidden fields on a live market run, pictures that are not the agent's). Headless Chromium prints the share's own print page on a deployed tier; if it fails, PDFKit draws the PDF, and if that fails too the answer is a plain PDF. X-Pdf-Engine says which engine printed (chromium or pdfkit) and X-Pdf-Plain: 1 says when it is the plain version, never an empty download.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
idPathstring (uuid)RequiredThe run's id.

Request body

The draft to print and how much reasoning it shows.

Fields (2)
FieldTypeRequiredDescription
stateobjectRequired
depth"specs" | "summary" | "full"Optionaldefault "summary"
Examples
{
  "state": {},
  "depth": "summary"
}

Responses

200

The PDF's bytes, as an attachment, with X-Pdf-Engine, X-Pdf-Plain and X-Pdf-Photos-Missing.

The bytes, with their own content type.

Errors

StatusCodeWhen
400invalidThe body has no state, a version 3 state that does not hold against the run, or a depth that is not specs, summary or full; issues names each.
400invalid-jsonThe body is empty or not JSON.
404not-foundNo such run, or one that is not yours.
413too-largeThe body is over the size limit.

Any call of this kind can also answer401 unauthenticated500 internal403 bad-origin

Warm the PDF door

HEAD/api/v1/search-runs/{id}/package-pdfBrowser only

The studio sends this when it opens, so the first PDF skips a cold start: it answers at once and then unpacks Chromium on that instance without launching it. Nothing is read or written.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
idPathstring (uuid)RequiredThe run's id.
warmQuery"1"RequiredMust be 1.

Responses

204

Warming; no body.

Errors

StatusCodeWhen
400invalidThe warm flag is missing.

Any call of this kind can also answer401 unauthenticated500 internal

Download a shared shortlist as a PDF

GET/api/v1/client/shares/{id}/pdfBrowser only

A signed-in client's PDF of a share they can see: the document the share page shows, in their agent's brand, printed by the same engines as the agent's PDF (X-Pdf-Engine says which). A share that is stopped, expired or someone else's answers the same 404 as one that never existed.

A signed-in client of an agent. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
idPathstring (uuid)RequiredThe share's id.

Responses

200

The PDF's bytes, as an attachment.

The bytes, with their own content type.

Errors

StatusCodeWhen
404not-foundNo such share, or one this client may not see.

Any call of this kind can also answer401 unauthenticated500 internal

Download a public share as a PDF

GET/api/v1/shares/{token}/pdfBrowser only

The PDF of a public share (/s/{token}), to anyone holding that link: the same document the public page shows, in the agent's brand, with the credits a listing needs, printed by the same engines as every other PDF.

X-Pdf-Engine says which printed, X-Pdf-Plain: 1 says when it is the plain version, and X-Pdf-Photos: 7/9 says how many photos printed of those wanted. The file is named for the share's title and its top pick.

A print is dear, so each address may ask for ten an hour. Dataset and Homes for sale shares both get this door when the agent turned the public link on. The signed-in client PDF is GET /api/v1/client/shares/{id}/pdf.

Open to anyone. No key needed.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
tokenPathstringRequiredThe 16-character public link token from /s/{token} (share alphabet; not a share short id).

Examples

curl "https://liviamatch.com/api/v1/shares/k3m9x2p7q4w8t5v6/pdf"

Responses

200

The PDF's bytes, as an attachment, with X-Pdf-Engine, X-Pdf-Plain and X-Pdf-Photos.

The bytes, with their own content type.

Errors

StatusCodeWhen
404not-foundNo such share, one that is stopped or ended, or a malformed token.
429rate-limitedThis address has asked for its ten PDFs this hour; Retry-After says when the hour turns.

Any call of this kind can also answer500 internal

Serve a picture on a public share

GET/api/v1/shares/{token}/images/{id}Browser only

A picture on the face of a public share (/s/{token}): the agent's headshot, a brand logo, or one the studio chose for that share. Anyone holding the link may load it; looks are counted per address.

Anything else answers the same 404 as a picture that never existed. Signed-in portal shares use the client image doors, not this one.

Open to anyone. No key needed.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
tokenPathstringRequiredThe 16-character public link token from /s/{token} (share alphabet; not a share short id).
idPathstring (uuid)RequiredThe image's id.

Examples

curl "https://liviamatch.com/api/v1/shares/k3m9x2p7q4w8t5v6/images/3a8d1e47-9c52-4b0f-a6d3-5e7b2c9f1a04"

Responses

200

The bytes with their stored Content-Type, Cache-Control: private, max-age=31536000, immutable and Cross-Origin-Resource-Policy: same-origin.

The bytes, with their own content type.

Errors

StatusCodeWhen
404not-foundNo such image, a share that is stopped or expired, or a picture that is not on its face.

Any call of this kind can also answer500 internal

Ask Livia about a public share

POST/api/v1/shares/{token}/askBrowser only

A visitor of a public share (/s/{token}) asks Livia about the homes on the page. The link is the credential. The answer is written from the share's own homes and screened for Fair Housing before it leaves; a reply that fails the screen is never sent.

Each address may ask twenty times an hour. A history turn longer than 900 characters is kept to its first 900, never refused, so one long earlier turn cannot fail the asks that carry it.

Open to anyone. No key needed.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
tokenPathstringRequiredThe 16-character public link token from /s/{token} (share alphabet; not a share short id).

Request body

The question, optionally the home it is about and the last turns of the conversation.

Fields (5)
FieldTypeRequiredDescription
questionstringRequired1 to 500 characters
focusIdstring | nullOptionalThe id of the home the question is about.1 to 80 characters
historyobject[]OptionalThe turns so far, oldest first; the last six are used.at most 12 items
history[].role"visitor" | "livia"Required
history[].textstringRequiredOne turn's words; the first 900 characters are kept.
Examples
{
  "question": "Which of these is quietest?",
  "focusId": null
}

Examples

curl -X POST "https://liviamatch.com/api/v1/shares/k3m9x2p7q4w8t5v6/ask" \
  -H "Content-Type: application/json" \
  -d '{
  "question": "Which of these is quietest?",
  "focusId": null
}'

Responses

200

Livia's answer.

Answered
{
  "data": {
    "answer": "The Oak Street home sits back from the road, so it is the quietest of the three."
  }
}
Fields (2)
FieldTypeDescription
dataobject
data.answerstring

Errors

StatusCodeWhen
400invalidThe body failed validation: no question, one over 500 characters, or a malformed field. issues names each problem by its path.
404not-foundNo such share, one that is stopped or ended, or a malformed token.
422answer-screenedThe reply did not pass the Fair Housing screen, so it was never sent.
429rate-limitedThis address has asked its twenty questions this hour; Retry-After says when the hour turns.
502ai-failedEvery model call failed, so there is no answer.
503ai-unavailableNo model is configured here, or its studio prompt cannot be composed.

Any call of this kind can also answer500 internal

Suggest places

GET/api/v1/places/suggestBrowser only

Typeahead for a commute destination or an area to live in: places in Tennessee that match what was typed, each tagged kind area (a city, neighborhood, ZIP code or county) or place (a street, address or business); scope=areas answers areas only, biased toward Nashville unless lat and lng say otherwise. attribution says whose places they are: osm (OpenStreetMap, whose contributors must be credited where the places show), google, or null for the stand-in. Cacheable by the browser for 30 seconds.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
qQuerystringOptionalWhat was typed. Empty returns no suggestions.
scopeQuery"areas"OptionalPass areas to answer only cities, neighborhoods, ZIP codes and counties. Anything else answers every place.
latQuerynumberOptionalLatitude to bias toward; 36.1627 when absent or not a number.
lngQuerynumberOptionalLongitude to bias toward; -86.7816 when absent or not a number.

Responses

200

The suggestions, best first.

One suggestion
{
  "data": {
    "predictions": [
      {
        "placeId": "ChIJ-example-place-id",
        "mainText": "Vanderbilt University Medical Center",
        "secondaryText": "Nashville, TN, USA",
        "fullText": "Vanderbilt University Medical Center, Nashville, TN, USA",
        "kind": "place"
      }
    ],
    "attribution": "google"
  }
}
From OpenStreetMap
{
  "data": {
    "predictions": [
      {
        "placeId": "photon:W131042264@36.1438011,-86.8028189",
        "mainText": "Vanderbilt University",
        "secondaryText": "2201 West End Avenue, Nashville, TN",
        "fullText": "Vanderbilt University, 2201 West End Avenue, Nashville, TN",
        "kind": "place"
      }
    ],
    "attribution": "osm"
  }
}
Fields (8)
FieldTypeDescription
dataobject
data.predictionsobject[]
data.predictions[].placeIdstring
data.predictions[].mainTextstring
data.predictions[].secondaryTextstring
data.predictions[].fullTextstring
data.predictions[].kind"area" | "place"
data.attribution"osm" | "google" | null

Errors

StatusCodeWhen
503not-configuredNo place-lookup service is configured here.
502places-failedThe place-lookup provider failed or did not answer in time. Never an empty list.

Any call of this kind can also answer401 unauthenticated500 internal

Read a place

POST/api/v1/places/detailsBrowser only

The coordinates and names of a place picked from the suggestions. coords is null when the provider has none for it. An OpenStreetMap id (photon:…) carries its own coordinates and answers them with no lookup, with empty names: keep the suggestion's. attribution says whose place it is, as the suggestions do.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Request body

The place to read.

Fields (1)
FieldTypeRequiredDescription
placeIdstringRequired1 to 256 characters
Examples
{
  "placeId": "ChIJ-example-place-id"
}

Responses

200

The place.

A place
{
  "data": {
    "coords": {
      "lat": 36.1421,
      "lng": -86.8016
    },
    "mainText": "Vanderbilt University Medical Center",
    "secondaryText": "Nashville, TN, USA",
    "attribution": "google"
  }
}
From OpenStreetMap
{
  "data": {
    "coords": {
      "lat": 36.1438011,
      "lng": -86.8028189
    },
    "mainText": "",
    "secondaryText": "",
    "attribution": "osm"
  }
}
Fields (7)
FieldTypeDescription
dataobject
data.coordsobject | null
data.coords.latnumber
data.coords.lngnumber
data.mainTextstring
data.secondaryTextstring
data.attribution"osm" | "google" | null

Errors

StatusCodeWhen
400invalidThe body has no placeId, or has a field it does not know; issues names each.
400invalid-jsonThe body is empty or not JSON.
413too-largeThe body is over 64 KiB.
502places-failedThe place-lookup provider failed or did not answer in time. Never an empty list.
503not-configuredNo place-lookup service is configured here.

Any call of this kind can also answer401 unauthenticated500 internal403 bad-origin

Transcribe a recording

POST/api/v1/transcriptionsBrowser only

Turns one recording into text for dictation. The type, length and the bytes themselves are checked before anything is sent on. The audio is never stored or logged; only its size and length are. Each person may transcribe 60 clips an hour. Up to 3 MB.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Request body

The recording itself, as the request body.

The recording itself, as the request body. Accepted types: audio/webm, audio/ogg, audio/mp4.

Responses

200

The words, empty when nothing was heard.

Heard
{
  "data": {
    "text": "Three bedrooms, a fenced yard, near the hospital."
  }
}
Fields (2)
FieldTypeDescription
dataobject
data.textstring

Errors

StatusCodeWhen
400invalidThe body is empty.
413too-largeThe recording is over the size limit.
415unsupported-typeThe type is not one we take, or the bytes are not that type.
429rate-limitedThe hourly cap on transcriptions is spent.
502transcription-failedThe transcriber failed or timed out.
503transcription-unavailableNo transcriber is configured here.

Any call of this kind can also answer401 unauthenticated500 internal403 bad-origin

Write summary alternatives

POST/api/v1/search-runs/{id}/summariesBrowser only

Three summaries of each named home, warm, crisp and lifestyle, in one model call a home. Each is screened for Fair Housing risk before it is kept: a flagged one is dropped, never stored, and dropped says which.

Every call is on the run's ledger. Counts as one of the studio's 30 AI requests an hour.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
idPathstring (uuid)RequiredThe run's id.

Request body

The homes to write for.

Fields (1)
FieldTypeRequiredDescription
propertyIdsstring[]RequiredHomes of this run, by id.1 to 12 items
Examples
{
  "propertyIds": [
    "L-1042",
    "L-2210"
  ]
}

Responses

201

What was written, and what was left out.

One home, one tone dropped
{
  "data": {
    "homes": [
      {
        "propertyId": "L-1042",
        "alternatives": [
          {
            "altId": "c8e2a6f9-3d41-4b07-9f5c-1b7d0a4e8c36",
            "tone": "crisp",
            "text": "A fenced yard for Biscuit and ten minutes to the hospital. One gap: no garage.",
            "model": "google/gemini-3.1-flash-lite",
            "createdAt": "2026-10-02T14:05:47.930Z"
          }
        ]
      }
    ],
    "dropped": [
      {
        "propertyId": "L-1042",
        "tone": "warm"
      }
    ],
    "failed": [],
    "unknown": [],
    "notes": [
      "I left out 1 version that described who might live in a home."
    ]
  }
}
Fields (16)
FieldTypeDescription
dataobject
data.homesobject[]What this request wrote, by home, in the order asked.
data.homes[].propertyIdstring
data.homes[].alternativesobject[]
data.homes[].alternatives[].altIdstring (uuid)The alternative's id: what a share document stores.
data.homes[].alternatives[].tone"warm" | "crisp" | "lifestyle" | "custom_seed"
data.homes[].alternatives[].textstring
data.homes[].alternatives[].modelstring | nullThe model that wrote it; null when unknown.
data.homes[].alternatives[].costUsdnumber | null, optionalLivia staff only. Its share of the call's cost in dollars; null when unknown. Omitted unless the caller, or the key's maker, is Livia staff.
data.homes[].alternatives[].createdAtstring (date-time)
data.droppedobject[]Tones the Fair Housing screen flagged. They were never stored.
data.dropped[].propertyIdstring
data.dropped[].tone"warm" | "crisp" | "lifestyle"
data.failedstring[]Homes whose call failed; nothing was written for them.
data.unknownstring[]Ids the run does not have; ignored.
data.notesstring[]A sentence for each kind of thing left out.

Errors

StatusCodeWhen
400invalidThe body names no homes, too many, a field it does not know, or no home this run has; issues names each body problem.
400invalid-jsonThe body is empty or not JSON.
404not-foundNo such run, or one that is not yours.
413too-largeThe body is over the size limit.
429rate-limitedThe studio's 30 AI requests an hour are spent; Retry-After says when the hour turns.
502ai-failedEvery model call failed. Nothing was written.
503ai-unavailableNo model is configured here, or its studio prompt cannot be composed.

Any call of this kind can also answer401 unauthenticated500 internal403 bad-origin

List summary alternatives

GET/api/v1/search-runs/{id}/summariesBrowser only

Every summary alternative written for the run's homes, by home, oldest first.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
idPathstring (uuid)RequiredThe run's id.

Responses

200

The alternatives by home.

One home
{
  "data": {
    "homes": [
      {
        "propertyId": "L-1042",
        "alternatives": [
          {
            "altId": "c8e2a6f9-3d41-4b07-9f5c-1b7d0a4e8c36",
            "tone": "warm",
            "text": "A fenced yard for Biscuit and ten minutes to the hospital. One gap: no garage.",
            "model": "google/gemini-3.1-flash-lite",
            "createdAt": "2026-10-02T14:05:47.930Z"
          }
        ]
      }
    ]
  }
}
Fields (10)
FieldTypeDescription
dataobject
data.homesobject[]Every alternative by home, oldest first.
data.homes[].propertyIdstring
data.homes[].alternativesobject[]
data.homes[].alternatives[].altIdstring (uuid)The alternative's id: what a share document stores.
data.homes[].alternatives[].tone"warm" | "crisp" | "lifestyle" | "custom_seed"
data.homes[].alternatives[].textstring
data.homes[].alternatives[].modelstring | nullThe model that wrote it; null when unknown.
data.homes[].alternatives[].costUsdnumber | null, optionalLivia staff only. Its share of the call's cost in dollars; null when unknown. Omitted unless the caller, or the key's maker, is Livia staff.
data.homes[].alternatives[].createdAtstring (date-time)

Errors

StatusCodeWhen
404not-foundNo such run, or one that is not yours.

Any call of this kind can also answer401 unauthenticated500 internal

Label listing photos

POST/api/v1/search-runs/{id}/photo-labelsBrowser only

Says which room each photo of the named homes shows, how good it is, how bright it looks and by what light, and how it is framed: one vision call a home, up to 10 photos each, sent at the size the grader sees. A photo is labeled once, whoever's run it is in, so a photo already labeled costs nothing.

The model fetches each photo by its address, so only photos on hosts the operator allows are sent. The rest are counted in refused and never labeled.

Every call is on the run's ledger. Counts as one of the studio's 30 AI requests an hour.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
idPathstring (uuid)RequiredThe run's id.

Request body

The homes to label; send none, or null, for the shortlist. The body is optional.

Fields (1)
FieldTypeRequiredDescription
propertyIdsstring[] | nullOptionalHomes of this run, by id; null for the shortlist.1 to 12 items, default null
Examples
{
  "propertyIds": [
    "L-1042"
  ]
}

Responses

200

How many photos were labeled, and how many were left out and why.

One home
{
  "data": {
    "labelled": 8,
    "cached": 2,
    "refused": 1,
    "deferred": 0,
    "failed": [],
    "unknown": [],
    "more": [],
    "notes": [
      "1 photo is on a site we may not read, so I did not label it."
    ]
  }
}
Fields (9)
FieldTypeDescription
dataobject
data.labelledintegerPhotos labeled by this request.
data.cachedintegerPhotos already labeled, in this run or any other. They cost nothing.
data.refusedintegerPhotos on a host the license gate does not allow. Never sent, never labeled.
data.deferredintegerPhotos past 10 a home, or that the model skipped. Send the request again to label the next ones.
data.failedstring[]Homes whose call failed.
data.unknownstring[]Ids the run does not have; ignored.
data.morestring[]Homes with photos still waiting: past 10 a home, or past the 12 homes one request labels. Send them again to continue.
data.notesstring[]A sentence for each kind of photo left out.

Errors

StatusCodeWhen
400invalidThe body names no homes, too many, a field it does not know, or no home this run has; issues names each body problem.
400invalid-jsonThe body is not JSON.
404not-foundNo such run, or one that is not yours.
413too-largeThe body is over the size limit.
429rate-limitedThe studio's 30 AI requests an hour are spent; Retry-After says when the hour turns.
502ai-failedEvery model call failed. Nothing was written.
503ai-unavailableNo model is configured here, or its studio prompt cannot be composed.

Any call of this kind can also answer401 unauthenticated500 internal403 bad-origin

List photo labels

GET/api/v1/search-runs/{id}/photo-labelsBrowser only

The labels of every home in the run, by the photo's place in the home's photos. A photo not yet labeled is absent.

A signed-in browser session. An API key is refused.

Required and optional fields, allowed values and errors, as the page shows them.

Parameters

NameInTypeRequiredDescription
idPathstring (uuid)RequiredThe run's id.

Responses

200

Labels by home and photo index.

One home
{
  "data": {
    "homes": [
      {
        "propertyId": "L-1042",
        "photoCount": 3,
        "labels": [
          {
            "index": 0,
            "room": "exterior_front",
            "quality": 4,
            "orientation": "landscape",
            "brightness": 5,
            "lighting": "natural",
            "caption": "Brick front with a covered porch"
          }
        ]
      }
    ]
  }
}
Fields (12)
FieldTypeDescription
dataobject
data.homesobject[]
data.homes[].propertyIdstring
data.homes[].photoCountinteger
data.homes[].labelsobject[]Labeled photos by index; a photo with no label is absent.
data.homes[].labels[].indexintegerThe photo's place in the home's photos, from 0.
data.homes[].labels[].room"kitchen" | "living" | "dining" | "bedroom" | "bathroom" | "exterior_front" | "exterior_back" | "yard" | "pool" | "view" | "office" | "garage" | "laundry" | "community" | "floorplan" | "other"
data.homes[].labels[].qualityintegerHow good it is as a photo, 5 the best.1 to 5
data.homes[].labels[].orientation"landscape" | "portrait" | "square"
data.homes[].labels[].brightnessinteger | nullHow bright and evenly lit the room looks, 5 the brightest. Null for a label written before it was asked.1 to 5
data.homes[].labels[].lighting"natural" | "artificial" | "mixed" | "dusk" | nullThe light in the photo. Null for a label written before it was asked.
data.homes[].labels[].captionstring | null

Errors

StatusCodeWhen
404not-foundNo such run, or one that is not yours.

Any call of this kind can also answer401 unauthenticated500 internal