Skip to the documentation

Quickstart

Make a key, start a match, wait for it, read the shortlist.

You will make a key, check it works, start a headless search, wait for it, and read the shortlist. Each step has the call in cURL, TypeScript and Python.

Headless vs the app

This quickstart uses POST /api/v1/search. That path returns a shortlist on the execution but does not create a run of a search, so nothing appears under Searches or Clients. For the product path (client → search → run → share, visible in the app), use From a client to shared results. The poll contract is the same either way: Runs and polling.

1. Make a key

  1. Sign in and open Settings, API keys.
  2. Name it for the tool and the job, in lowercase with dashes: crm-search.
  3. Leave Run matches ticked. That is the one scope this quickstart needs. Choose how long the key lives; three months is a good start.
  4. Press Make key, and copy the key now. It is shown once.

Put it in your environment, never in code or a URL:

export LIVIA_API_KEY="paste-your-key-here"

Treat it like a password

A key acts as the person who made it, within its scopes, until it expires. Anyone holding it can run matches on your allowance. Keys start lmp_ (yours) or lmo_ (your organization's), so a secret scanner can recognize one if it leaks.

2. Check the key

List your searches. A new account has none, and an empty list with 200 means the key works and may read searches (a key that runs matches may read them too).

curl "https://liviamatch.com/api/v1/search" \
  -H "Authorization: Bearer $LIVIA_API_KEY"
An empty account
{ "data": [], "links": { "next": null } }

A 401 means the key was not accepted. A 403 with insufficient_scope means it is live but lacks a scope. See Authentication.

3. Start a match

POST /api/v1/search saves the request and queues its first run. It answers 202 at once: the run takes a minute or two and works in the background. Send an Idempotency-Key so a retry after a network error cannot start a second run.

Only location, price.max, beds.min, baths.min and a place to start from (a commute, an area, or location.center) are required. Everything else makes the match better. The full list is in the Searches reference.

curl -X POST "https://liviamatch.com/api/v1/search" \
  -H "Authorization: Bearer $LIVIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: okafor-east-nashville-1" \
  -d '{
  "label": "Okafors, East Nashville",
  "listingType": "buy",
  "location": {
    "label": "Nashville, TN",
    "center": {
      "lat": 36.1627,
      "lng": -86.7816
    }
  },
  "price": {
    "min": 450000,
    "max": 750000
  },
  "beds": {
    "min": 3
  },
  "baths": {
    "min": 2
  },
  "sqft": {
    "min": 1800
  },
  "propertyTypes": [
    "single-family"
  ],
  "commutes": [
    {
      "label": "Vanderbilt University Medical Center",
      "lat": 36.1421,
      "lng": -86.8016,
      "maxMinutes": 25
    }
  ],
  "mustHaves": [
    "Fenced yard",
    "Walkable to coffee"
  ],
  "niceToHaves": [
    "Home office"
  ],
  "dealBreakers": [
    "Busy road"
  ],
  "idealHome": "A renovated bungalow with big windows and a porch, close to the park.",
  "neighbourhood": [
    "great-schools",
    "walkable-lively"
  ],
  "options": {
    "shortlist": 4
  }
}'

The answer names the search, the queued run, and where to read the run:

202 Accepted
{
  "data": {
    "search": {
      "id": "6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60",
      "label": "Okafors, East Nashville",
      "clientId": null,
      "revision": 1,
      "request": {
        "version": 1,
        "clientId": null,
        "label": "Okafors, East Nashville",
        "listingType": "buy",
        "location": {
          "label": "Nashville, TN",
          "center": {
            "lat": 36.1627,
            "lng": -86.7816
          }
        },
        "price": {
          "min": 450000,
          "max": 750000
        },
        "beds": {
          "min": 3
        },
        "baths": {
          "min": 2
        },
        "sqft": {
          "min": 1800
        },
        "propertyTypes": [
          "single-family"
        ],
        "commutes": [
          {
            "label": "Vanderbilt University Medical Center",
            "lat": 36.1421,
            "lng": -86.8016,
            "maxMinutes": 25
          }
        ],
        "areas": [],
        "mustHaves": [
          "Fenced yard",
          "Walkable to coffee"
        ],
        "niceToHaves": [
          "Home office"
        ],
        "dealBreakers": [
          "Busy road"
        ],
        "idealHome": "A renovated bungalow with big windows and a porch, close to the park.",
        "dislikes": null,
        "movingReason": null,
        "lifestyle": {
          "household": null,
          "hasKids": null,
          "kidsAges": null,
          "schoolPriority": null,
          "workFromHome": null,
          "wfhDaysPerWeek": null,
          "noiseSensitive": null,
          "yardImportance": null,
          "hasPets": null,
          "petType": null,
          "renovationTolerance": null,
          "hoaPreference": null,
          "neighborhoodVibe": null,
          "weekendLifestyle": null
        },
        "neighbourhood": [
          "great-schools",
          "walkable-lively"
        ],
        "vibe": {
          "tags": [],
          "urbanToQuiet": 50,
          "moveInToProject": 50
        },
        "tasteReactions": [],
        "includeComingSoon": true,
        "onMarketSince": null,
        "options": {
          "shortlist": 4,
          "grade": 25,
          "photos": 8
        }
      },
      "createdAt": "2026-10-02T14:03:11.402Z",
      "updatedAt": "2026-10-02T14:03:11.402Z",
      "latestExecution": {
        "id": "0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83",
        "searchId": "6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60",
        "status": "queued",
        "step": null,
        "stepLabel": null,
        "attempts": 0,
        "progress": {
          "done": 0,
          "total": 9,
          "percent": 0
        },
        "createdAt": "2026-10-02T14:03:12.118Z",
        "startedAt": null,
        "finishedAt": null,
        "heartbeatAt": null,
        "links": {
          "self": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60/executions/0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83",
          "search": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60",
          "cancel": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60/executions/0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83/cancel"
        }
      },
      "links": {
        "self": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60",
        "run": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60",
        "executions": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60/executions"
      }
    },
    "execution": {
      "id": "0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83",
      "searchId": "6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60",
      "status": "queued",
      "step": null,
      "stepLabel": null,
      "attempts": 0,
      "progress": {
        "done": 0,
        "total": 9,
        "percent": 0
      },
      "createdAt": "2026-10-02T14:03:12.118Z",
      "startedAt": null,
      "finishedAt": null,
      "heartbeatAt": null,
      "links": {
        "self": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60/executions/0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83",
        "search": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60",
        "cancel": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60/executions/0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83/cancel"
      }
    }
  }
}
Response headers
Location: /api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60/executions/0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83
Retry-After: 2

4. Wait for it

GET the Location until status is succeeded, failed or canceled. Wait Retry-After seconds between reads (it is 2), and stop after a few minutes. progress.percent and stepLabel are for showing a person how far along it is.

RUN="https://liviamatch.com/api/v1/search/<searchId>/executions/<executionId>"
while true; do
  STATUS=$(curl -s "$RUN" -H "Authorization: Bearer $LIVIA_API_KEY" | jq -r .data.status)
  echo "$STATUS"
  case "$STATUS" in succeeded|failed|canceled) break;; esac
  sleep 2
done
Partway through (abridged: a read also carries steps, usage and the rest)
{
  "data": {
    "id": "0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83",
    "searchId": "6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60",
    "status": "running",
    "step": "grade",
    "stepLabel": "Reading each home's words and photos",
    "attempts": 1,
    "progress": {
      "done": 6,
      "total": 9,
      "percent": 71
    },
    "createdAt": "2026-10-02T14:03:12.118Z",
    "startedAt": null,
    "finishedAt": null,
    "heartbeatAt": null,
    "links": {
      "self": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60/executions/0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83",
      "search": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60",
      "cancel": "/api/v1/search/6f1c2b9e-0a45-4d7e-9a1c-3b2f8d4e5a60/executions/0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83/cancel"
    }
  }
}

5. Read the shortlist

Once status is succeeded, result is present and never changes again. result.shortlist is the homes that fit, best first. Each has its facts, a blended score from 0 to 100 with its parts in scores, a take in a sentence, and its pros and cons.

if (run.status !== 'succeeded') throw new Error(`Run ${run.status}: ${run.error?.code}`)

for (const home of run.result.shortlist) {
  console.log(`${home.rank}. ${home.address}, ${home.city} $${home.price} (score ${home.score})`)
  console.log(`   ${home.take}`)
}

// Required wherever the homes are shown.
for (const credit of run.result.credits) console.log(credit.text)
for (const note of run.result.notes) console.warn(`stand-in: ${note}`)
One shortlisted home
{
  "rank": 1,
  "listingKey": "NSH-2283104",
  "address": "1204 Boscobel St",
  "city": "Nashville",
  "state": "TN",
  "postalCode": "37206",
  "neighbourhood": "East Nashville",
  "price": 689000,
  "beds": 3,
  "baths": 2.5,
  "sqft": 2140,
  "yearBuilt": 1926,
  "propertyType": "Residential",
  "photo": "https://photos.example.com/NSH-2283104/hero.jpg",
  "score": 91.4,
  "scores": {
    "math": 88.2,
    "text": 93,
    "photos": 92.5,
    "neighbourhood": 90
  },
  "take": "A renovated 1926 bungalow with the fenced yard and the porch they asked for, 14 minutes from the hospital.",
  "pros": [
    "Fenced backyard",
    "Walkable to two coffee shops",
    "Dedicated home office"
  ],
  "cons": [
    "One bathroom is down a hallway from the primary"
  ],
  "highlights": [
    "Original heart-pine floors",
    "Deep front porch"
  ],
  "concerns": [],
  "lookAndFeel": "Warm, bright, restored rather than flipped.",
  "commutes": [
    {
      "label": "Vanderbilt University Medical Center",
      "minutes": 14,
      "estimated": false
    }
  ],
  "signals": {
    "school": 78,
    "safety": 71
  },
  "breakdown": {
    "price": 24.1,
    "sqft": 18.5,
    "beds": 9.2,
    "baths": 9.6,
    "travel": {
      "location_1": 26.8
    },
    "bonuses": 0,
    "total": 88.2
  },
  "blendWeights": {
    "math": 0.4,
    "text": 0.3,
    "photos": 0.2,
    "neighbourhood": 0.1
  },
  "officeName": "Hearth & Key Realty",
  "unconfirmed": [
    "two-car garage"
  ],
  "confirmed": [
    "fenced yard"
  ],
  "keyPhotos": [
    {
      "url": "https://photos.example.com/NSH-2283104/kitchen.jpg",
      "label": "Kitchen"
    }
  ],
  "gallery": [
    "https://photos.example.com/NSH-2283104/hero.jpg",
    "https://photos.example.com/NSH-2283104/kitchen.jpg"
  ]
}

Show what the result asks you to show

result.credits is attribution a provider's terms require wherever the result is displayed, and officeName is the listing broker, which MLS rules require beside a home. result.notes lists every stand-in the run used, so a result is never passed off as live when part of it was not. An empty notes means everything was live.

Where next