From a client to shared results
Add a client, write a search, run it, curate the results, share them: the path an agent takes in the app, as calls.
This is the product's own path, client, search, run, results, share, as API calls — and the path whose results show up in Searches and Clients in the app. Every command below was run against a build of this code with a key made in Settings, and the answers shown are the ones it gave. Kick off with POST .../runs ("Search now"), then GET that same run until status is terminal; the results are on that payload. The headless search doors stay for a server that only wants homes back and has no client to share them with: an execution made there is not a run of a search, so nothing on this page opens for it and nothing appears in the platform. Every path on this page used to have an older name, which still answers: see old words and new.
Before you start
Make a key in Settings, API keys with these six scopes. Writing a thing includes reading it, so each write scope also opens the reads next to it.
| Scope | Opens |
|---|---|
profile:read | Who the key is: GET /me |
search:read | What a search can run against, runs and their results, the PDF, what is left of the month |
search:run | Starting a run, and curating its results |
clients:write | Adding, editing and archiving clients |
briefs:write | Writing and editing searches, and drafting one from words |
shares:write | Giving a client access, sharing, emailing and stopping |
export BASE="https://liviamatch.com"
export LIVIA_API_KEY="paste-your-key-here"
# One helper, so each call below reads as what it does. -sS: quiet, but say if curl itself fails.
livia() { curl -sS -H "Authorization: Bearer $LIVIA_API_KEY" -H "Content-Type: application/json" "$@"; }The commands use curl and jq, and keep each answer in a file so the next call can read from it. Keep them in one shell session: later steps use the variables earlier ones set. The client's address below receives real mail on a deployment that sends it, so use your own while you try this.
1. Check the key, and what is left
livia "$BASE/api/v1/me" | jq '.data | { name, email, org: .org.name, key: .key | { name, kind, scopes } }'
livia "$BASE/api/v1/data-sets" | jq -c '.data[]'
livia "$BASE/api/v1/search/usage" | jq .data.allowance/me says who the key acts as, and either kind of key may ask with profile:read. A personal key acts as the person who made it. An org key belongs to the organization and acts as the admin who made it, so the clients, searches and runs it makes are saved to that admin's workspace: key.kind says which you hold, org names the organization, and the person is that admin. A script can say whose workspace it is about to write into before it writes anything. /data-sets is what a search can run against. Each row's source is what you put on a search: homes-for-sale for the market, or one of your data sets. The row marked default is where a new search starts. allowance says how many runs the org has used and how many you may still start: a run counts one when it lands, and the check is worth making before a batch.
2. Add the client
Choose the client's id yourself and PUT to it. If the request times out and you send it again, you get the same client, not two: 201 the first time, 200 after. (POST /clients makes one and chooses the id for you.)
CLIENT_ID=$(uuidgen | tr 'A-Z' 'a-z')
livia -X PUT "$BASE/api/v1/clients/$CLIENT_ID" \
-d '{ "name": "Amara Okafor", "email": "amara@example.com", "note": "Moving from Boston in January" }'{
"data": {
"id": "4b9e2d71-0c58-4a36-8d1f-7e3a5c9b0f24",
"name": "Tunde Bello",
"email": "tunde.bello@example.com",
"note": null,
"phone": null,
"archivedAt": null,
"createdAt": "2026-10-06T09:41:03.118Z",
"hasAccess": false,
"links": {
"self": "/api/v1/clients/4b9e2d71-0c58-4a36-8d1f-7e3a5c9b0f24",
"access": "/api/v1/clients/4b9e2d71-0c58-4a36-8d1f-7e3a5c9b0f24/access"
}
}
}3. Write the search
Two ways in, and they meet at POST /searches. From words, when you have notes from a consultation: the draft reads them, saves nothing, and names what it assumed.
jq -n --arg text 'Amara and her husband are moving from Boston in January with two kids and a dog. Budget is around $750k, three bedrooms at least, two baths, somewhere in Nashville near Vanderbilt hospital. A fenced yard is a must, and a home office would be nice.' \
--arg client "$CLIENT_ID" '{ text: $text, clientId: $client }' \
| livia -X POST "$BASE/api/v1/searches/draft" -d @- > draft.json
# What it read, and what it left at a starting value.
jq '.data.search | { title, budget, beds, baths, areas, mustHaves, inferred }' draft.json{
"data": {
"brief": {
"title": "Boston family, near Vanderbilt",
"clientId": "9c4e1a73-2b85-4d06-8f1a-7d3b5e9c0a28",
"source": "homes-for-sale",
"tenure": "buy",
"budget": {
"value": 750000,
"fixed": false,
"buffer": 10
},
"beds": {
"value": 3,
"fixed": true,
"buffer": 0
},
"baths": {
"value": 2,
"fixed": true,
"buffer": 0
},
"propertyType": "House",
"timeline": "Exploring",
"areas": [
"Nashville"
],
"commutes": [],
"areaPins": [],
"mustHaves": [
"Fenced yard"
],
"niceToHaves": [
"Home office"
],
"likedHomes": {
"urls": [],
"importance": "nice"
},
"feel": {
"text": "",
"tags": [],
"importance": "nice"
},
"inferred": {
"fields": [
"title",
"budget",
"beds",
"baths",
"cities",
"feature"
],
"assumed": [
"propertyType"
]
}
},
"search": {
"title": "Boston family, near Vanderbilt",
"clientId": "9c4e1a73-2b85-4d06-8f1a-7d3b5e9c0a28",
"source": "homes-for-sale",
"tenure": "buy",
"budget": {
"value": 750000,
"fixed": false,
"buffer": 10
},
"beds": {
"value": 3,
"fixed": true,
"buffer": 0
},
"baths": {
"value": 2,
"fixed": true,
"buffer": 0
},
"propertyType": "House",
"timeline": "Exploring",
"areas": [
"Nashville"
],
"commutes": [],
"areaPins": [],
"mustHaves": [
"Fenced yard"
],
"niceToHaves": [
"Home office"
],
"likedHomes": {
"urls": [],
"importance": "nice"
},
"feel": {
"text": "",
"tags": [],
"importance": "nice"
},
"inferred": {
"fields": [
"title",
"budget",
"beds",
"baths",
"cities",
"feature"
],
"assumed": [
"propertyType"
]
}
},
"setAside": []
}
}inferred.fields is what the words said. inferred.assumed names the essentials they never mentioned, left at the value a new search starts with: budget and propertyType, and on a search drafted before 2026-10-22 beds and baths too. Set those yourself when you know them. Beds and baths the words never named now get no floor (value 0). A field that claims nothing, like that or timeline at Exploring, is not an assumption and is never listed. The words go to the model under the no-retention routing rule and are never stored. Words that name nothing a search can use are refused (422 draft-empty) rather than drafted from starting values, and drafts are capped at an hour's worth, so a loop that drafts gets 429 with Retry-After. Save what you accept:
# Homes for sale. With no areas it looks around the org's market, or Nashville, TN.
SEARCH_ID=$(jq '.data.search | .source = "homes-for-sale"' draft.json \
| livia -X POST "$BASE/api/v1/searches" -d @- | tee search.json | jq -r .data.id)
echo "$SEARCH_ID"Or from structure, when you already know it. A search must say its budget, its beds and its baths, so no search from the API is made of starting values; everything else has an honest default.
jq -n --arg client "$CLIENT_ID" '{
title: "Okafors, East Nashville", clientId: $client, source: "homes-for-sale",
budget: { value: 750000 }, beds: { value: 3 }, baths: { value: 2 },
areas: ["Nashville"], mustHaves: ["Fenced yard"], niceToHaves: ["Home office"]
}' | livia -X POST "$BASE/api/v1/searches" -d @-{
"data": {
"id": "7d2f8a40-b6c1-4e35-9a07-c4e1d8b3f592",
"version": 1,
"title": "Okafors, East Nashville",
"clientId": "9c4e1a73-2b85-4d06-8f1a-7d3b5e9c0a28",
"source": "homes-for-sale",
"tenure": "buy",
"budget": {
"value": 750000,
"fixed": false,
"buffer": 10
},
"beds": {
"value": 3,
"fixed": true,
"buffer": 0
},
"baths": {
"value": 2,
"fixed": true,
"buffer": 0
},
"propertyType": "House",
"timeline": "Exploring",
"areas": [
"Nashville"
],
"commutes": [
{
"id": "commute-1",
"label": "Vanderbilt University Medical Center",
"coords": {
"lat": 36.1421,
"lng": -86.8016
},
"maxMin": 25
}
],
"areaPins": [],
"mustHaves": [
"Fenced yard",
"Walkable to coffee"
],
"niceToHaves": [
"Home office"
],
"details": null,
"likedHomes": {
"urls": [],
"importance": "nice"
},
"feel": {
"text": "",
"tags": [],
"importance": "nice"
},
"inferred": null,
"archivedAt": null,
"updatedAt": "2026-10-06T13:58:20.310Z",
"links": {
"self": "/api/v1/searches/7d2f8a40-b6c1-4e35-9a07-c4e1d8b3f592",
"matches": "/api/v1/searches/7d2f8a40-b6c1-4e35-9a07-c4e1d8b3f592/matches",
"runs": "/api/v1/searches/7d2f8a40-b6c1-4e35-9a07-c4e1d8b3f592/runs"
}
}
}4. Change it without overwriting anyone
A search saves as a new revision each time it changes, and a run pins the revision it ran, so editing a search never alters a run you already have. A read returns the revision as the ETag; send it back as If-Match to change the search only if it is still the one you read. The body is the search as it was read, without the five fields the server owns (id, version, archivedAt, updatedAt, links).
livia -D headers.txt -o search.json "$BASE/api/v1/searches/$SEARCH_ID"
ETAG=$(grep -i '^etag:' headers.txt | cut -d' ' -f2 | tr -d '\r')
# The search as it was read, minus what the server owns, with a bigger budget.
jq '.data | del(.id, .version, .archivedAt, .updatedAt, .links) | .budget.value += 25000' search.json > edit.json
livia -X PUT "$BASE/api/v1/searches/$SEARCH_ID" -H "If-Match: $ETAG" -d @edit.json | jq '.data | { version, budget }'
# The same tag again is stale now: nothing is written.
livia -X PUT "$BASE/api/v1/searches/$SEARCH_ID" -H "If-Match: $ETAG" -d @edit.json | jq .error{ "error": { "code": "revision-mismatch", "digest": "ce54f0b1-6a4c-4b50-8f6e-5b1a0d3c9e72" } }5. Search now
This counts one run against the org's month and your own limit, so send an Idempotency-Key: a retry with the same key answers the run the first call made (Idempotent-Replayed: true) and charges nothing more. Against Homes for sale the run works in the background for a minute or two, so the answer is 202 with the run running, a Location to read, and a Retry-After. Against a data set the run is complete when it is made and the answer is 201 with the results in it.
livia -i -X POST "$BASE/api/v1/searches/$SEARCH_ID/runs" -H "Idempotency-Key: okafor-$SEARCH_ID" > started.txt
head -n 1 started.txt
RUN_ID=$(tail -n 1 started.txt | jq -r .data.id){
"data": {
"id": "1e8b4c27-5a93-4d60-b2f7-9c0d3a6e8f15",
"kind": "market",
"status": "running",
"clientId": "9c4e1a73-2b85-4d06-8f1a-7d3b5e9c0a28",
"createdAt": "2026-10-06T14:03:11.402Z",
"updatedAt": "2026-10-06T14:03:11.402Z",
"dataAsOf": null,
"summary": null,
"funnel": null,
"reasonCounts": {},
"credits": [],
"notes": [],
"notify": {
"asked": {
"agent": {
"text": true,
"email": false
},
"client": true,
"also": []
},
"recipients": [
{
"audience": "agent",
"channel": "sms",
"to": null,
"state": "asked",
"reason": null,
"sendAfter": null
},
{
"audience": "client",
"channel": "sms",
"to": null,
"state": "asked",
"reason": null,
"sendAfter": null
},
{
"audience": "client",
"channel": "email",
"to": null,
"state": "asked",
"reason": null,
"sendAfter": null
}
]
},
"searchId": "7d2f8a40-b6c1-4e35-9a07-c4e1d8b3f592",
"searchVersion": 1,
"execution": {
"id": "0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83",
"status": "queued",
"step": null,
"stepLabel": null,
"attempts": 0,
"progress": {
"done": 0,
"total": 9,
"percent": 0
},
"startedAt": null,
"finishedAt": null,
"error": null,
"links": {
"self": "/api/v1/search/7d2f8a40-b6c1-4e35-9a07-c4e1d8b3f592/executions/0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83",
"cancel": "/api/v1/search/7d2f8a40-b6c1-4e35-9a07-c4e1d8b3f592/executions/0d9e7c1a-52b8-4f3a-8e6d-1c7a9b2f4e83/cancel"
},
"review": null
},
"counts": null,
"results": [],
"removed": [],
"excluded": [],
"emptyReason": null,
"compliance": {
"kind": "not_reviewed",
"overall": null,
"items": [],
"reviewedAt": null,
"source": null
},
"links": {
"self": "/api/v1/runs/1e8b4c27-5a93-4d60-b2f7-9c0d3a6e8f15",
"search": "/api/v1/searches/7d2f8a40-b6c1-4e35-9a07-c4e1d8b3f592",
"results": "/api/v1/runs/1e8b4c27-5a93-4d60-b2f7-9c0d3a6e8f15/results",
"shares": "/api/v1/runs/1e8b4c27-5a93-4d60-b2f7-9c0d3a6e8f15/shares",
"pdf": "/api/v1/runs/1e8b4c27-5a93-4d60-b2f7-9c0d3a6e8f15/pdf"
}
}
}A search that names no place still runs and counts as a run. Homes for sale looks around the org's market, or Nashville, TN while none is set. A search that cannot run is refused before any run starts: a place Homes for sale cannot find, a commute or a rental against a data set, no source. That is 422 search-unrunnable (brief-unrunnable on the old path), and issues names the field to fix.
6. Wait for it, then read the results
GET the same run URL until its status is not running: complete, failed or canceled. Wait Retry-After seconds between reads (it is 2). When complete, the results are on that response — status and payload together, no second endpoint. Polling is the contract, and a read is also what moves a stranded run along. To be told instead, send notify.callbackUrl when you start the run: Get told when a run finishes. Full loop notes for agents: Runs and polling.
until [ "$(livia "$BASE/api/v1/runs/$RUN_ID" | jq -r .data.status)" != "running" ]; do
sleep 2
done
echo "done"livia -D headers.txt -o run.json "$BASE/api/v1/runs/$RUN_ID"
jq -r '.data | "\(.status), \(.counts.results) homes of \(.counts.considered) considered"' run.json
jq -r '.data.results[] | "\(.rank). \(.address), \(.area) $\(.price) score \(.score)"' run.json
jq -r '.data.credits[].text' run.jsonA complete run carries every home with its facts, a blended score from 0 to 100 and the parts it is made of, its reasons and tradeoffs, and a take in a sentence. It also carries the homes you took out of the results (removed) and left out (excluded) with why, the credits MLS rules require wherever the homes are shown, and notes naming any stand-in a result used. An empty notes means everything was live.
7. Curate the results
The results are the agent's to arrange, as it is in the app. One call replaces the whole ordered list: leave a home out to remove it, change the order to reorder, and name a home from excluded to add it back. Only your list changes; the run's own result never does. The run's ETag is "own" while the run keeps its own order and a fingerprint once you have arranged it, so If-Match refuses to overwrite a list you have not seen.
RUN_ETAG=$(grep -i '^etag:' headers.txt | cut -d' ' -f2 | tr -d '\r')
# Put the second home first.
jq '{ homes: ([.data.results[].id] | [.[1], .[0]] + .[2:]) }' run.json \
| livia -X PUT "$BASE/api/v1/runs/$RUN_ID/results" -H "If-Match: $RUN_ETAG" -d @- \
| jq -r '.data.results[] | "\(.rank). \(.address)"'8. Share it with the client
Sharing is three calls, as it is in the app, so each can be done, retried or skipped on its own. Giving access emails the client a sign-in link naming you, good for three days; a share without access is 409 client-has-no-access. A share is the default package of your results at a depth (specs, summary or full) with an optional expiresAt, and sharing again updates the same share. Telling them is its own call, limited to one email every ten minutes (429 recently-emailed, with Retry-After).
# 1. Give them access: this emails them a sign-in link.
livia -X POST "$BASE/api/v1/clients/$CLIENT_ID/access" -d '{}' | jq '.data | { name, hasAccess }'
# 2. Share the results as they stand.
SHARE_ID=$(livia -X POST "$BASE/api/v1/runs/$RUN_ID/shares" -d "{ \"clientId\": \"$CLIENT_ID\", \"depth\": \"summary\" }" | tee share.json | jq -r .data.id)
jq '.data | { status, homeCount, depth, expiresAt }' share.json
# 3. Tell them it is ready.
livia -X POST "$BASE/api/v1/runs/$RUN_ID/shares/$SHARE_ID/email" -d '{}' | jq .data.lastEmailedAt{
"data": {
"id": "d5a1f7c3-8e29-4b64-a0d8-3f6c2b9e7a41",
"clientId": "9c4e1a73-2b85-4d06-8f1a-7d3b5e9c0a28",
"status": "live",
"depth": "summary",
"homeCount": 2,
"homes": [
"NSH-2283104",
"NSH-2279012"
],
"expiresAt": null,
"revokedAt": null,
"lastEmailedAt": null,
"createdAt": "2026-10-06T14:20:03.118Z",
"updatedAt": "2026-10-06T14:20:03.118Z",
"reactions": [],
"runId": "1e8b4c27-5a93-4d60-b2f7-9c0d3a6e8f15",
"links": {
"self": "/api/v1/runs/1e8b4c27-5a93-4d60-b2f7-9c0d3a6e8f15/shares/d5a1f7c3-8e29-4b64-a0d8-3f6c2b9e7a41",
"email": "/api/v1/runs/1e8b4c27-5a93-4d60-b2f7-9c0d3a6e8f15/shares/d5a1f7c3-8e29-4b64-a0d8-3f6c2b9e7a41/email",
"run": "/api/v1/runs/1e8b4c27-5a93-4d60-b2f7-9c0d3a6e8f15",
"client": "/api/v1/clients/9c4e1a73-2b85-4d06-8f1a-7d3b5e9c0a28"
}
}
}# What they answered, as they answer.
livia "$BASE/api/v1/runs/$RUN_ID/shares/$SHARE_ID" | jq '.data | { status, reactions }'
# The same PDF the app prints.
livia -o results.pdf "$BASE/api/v1/runs/$RUN_ID/pdf" && file results.pdf
# Stop sharing.
livia -X DELETE "$BASE/api/v1/runs/$RUN_ID/shares/$SHARE_ID" | jq .data.statusThe PDF is at /runs/{runId}/pdf, which the run's own links.pdf names (the old /search-runs/{id}/pdf still answers). It is the page the agent sees, printed, so it can take several seconds the first time.
9. What it cost
livia "$BASE/api/v1/search/usage" | jq .data.allowanceOne run was charged for the whole journey, however many times the replays were sent. That is the number to check in a script that must not spend twice.
The same path in TypeScript
// journey.ts: node 20 or later, run with: npx tsx journey.ts
const base = 'https://liviamatch.com'
const key = process.env.LIVIA_API_KEY
if (!key) throw new Error('set LIVIA_API_KEY')
type Json = Record<string, any>
async function call(method: string, path: string, body?: unknown, headers: Record<string, string> = {}) {
const response = await fetch(base + path, {
method,
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', ...headers },
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(60_000),
})
const json: Json | null = response.headers.get('content-type')?.includes('json') ? await response.json() : null
if (response.status >= 400) throw new Error(`${method} ${path}: ${response.status} ${json?.error?.code} (digest ${json?.error?.digest})`)
return { status: response.status, json: json?.data as Json, etag: response.headers.get('etag'), retryAfter: Number(response.headers.get('retry-after') ?? 2) }
}
// A client, made at an id of ours, so sending it twice makes one.
const clientId = crypto.randomUUID()
await call('PUT', `/api/v1/clients/${clientId}`, { name: 'Amara Okafor', email: 'amara@example.com', note: null })
// A search, from structure.
const search = await call('POST', '/api/v1/searches', {
title: 'Okafors, East Nashville', clientId, source: 'homes-for-sale',
budget: { value: 750_000 }, beds: { value: 3 }, baths: { value: 2 }, areas: ['Nashville'], mustHaves: ['Fenced yard'],
})
// A run ("Search now"), started once however often this line runs, and waited for as Retry-After says.
let run = await call('POST', `/api/v1/searches/${search.json.id}/runs`, undefined, { 'Idempotency-Key': `okafor-${search.json.id}` })
while (run.json.status === 'running') {
await new Promise((resolve) => setTimeout(resolve, run.retryAfter * 1000))
run = await call('GET', `/api/v1/runs/${run.json.id}`)
}
if (run.json.status !== 'complete') throw new Error(`the run ended as ${run.json.status}`)
for (const home of run.json.results) console.log(`${home.rank}. ${home.address} (score ${home.score})`)
// The results, the second home first, only if nobody changed them since we read them.
const [first, second, ...rest] = run.json.results.map((home: Json) => home.id)
await call('PUT', `/api/v1/runs/${run.json.id}/results`, { homes: [second, first, ...rest] }, { 'If-Match': run.etag! })
// Share it with the client and tell them.
await call('POST', `/api/v1/clients/${clientId}/access`, {})
const share = await call('POST', `/api/v1/runs/${run.json.id}/shares`, { clientId, depth: 'summary' })
await call('POST', `/api/v1/runs/${run.json.id}/shares/${share.json.id}/email`, {})
console.log('shared', share.json.id, share.json.homeCount, 'homes')Or let the script walk it
The repository's scripts/api-journey.ts makes these calls, and the checks that matter between them (a replay answers the same run and is marked, a stale If-Match is refused, the PDF starts with %PDF, one run is charged), against any base URL. It says first where it is going and which key it holds (the key's prefix and last four characters, and whether that prefix says personal or organization), then, once GET /me answers, who the key acts as: the person for a personal key, the organization and the admin whose workspace it saves to for an org key. Then it prints each step with its status and time, and never the rest of the key:
LIVIA_API_KEY="paste-your-key-here" npm run api:journey -- --base https://liviamatch.com --client-email you@example.combase https://liviamatch.com
key lmp_…Q1Zr, a personal key
acting as Harper Quinn <harper@hearthandkey.example>, admin of Hearth & Key Realty
named "crm-journey", expires 2027-01-04
ok 200 435 ms GET /me
ok 200 107 ms GET /search/usage (496 runs left)
ok 200 130 ms GET /data-sets (matching against homes-for-sale)
ok 201 468 ms PUT /clients/{id}
ok 200 106 ms PUT /clients/{id} again (a replay)
ok 200 230 ms PATCH /clients/{id}
ok 200 61 ms GET /clients
ok 200 43 ms POST /searches/draft (1 assumed, nothing saved)
ok 201 28 ms POST /searches (the draft, as it was answered) (revision "1")
ok 200 160 ms GET /searches/{id}
ok 200 167 ms PUT /searches/{id} with If-Match (revision "2", budget 775000)
ok 412 172 ms PUT /searches/{id} with a stale If-Match (refused)
ok 202 760 ms POST /searches/{id}/runs (market run running)
ok 202 333 ms POST /searches/{id}/runs again, same key (a replay) (the same run, nothing charged twice)
ok 200 8356 ms GET /runs/{id} until it finishes (complete after 5 reads, 4 homes)
ok 200 160 ms PUT /runs/{id}/results with If-Match (3 of 4 homes, in the order sent)
ok 200 388 ms POST /clients/{id}/access
ok 201 295 ms POST /runs/{id}/shares (3 homes, live)
ok 200 714 ms POST /runs/{id}/shares/{id}/email
ok 200 168 ms GET /runs/{id}/shares/{id} (live, emailed)
ok 200 14637 ms GET /runs/{id}/pdf (372208 bytes of application/pdf, engine chromium)
ok 200 510 ms DELETE /runs/{id}/shares/{id}
ok 200 299 ms GET /search/usage again (one run charged, 495 left)
ok 200 252 ms PATCH /clients/{id} archived
journey passed, 24 steps in 29.0 s: 24 ok, 0 skipped, 0 failedThe first two lines come from the key's own spelling, before any call, so they appear even when the first call is refused. The next two come from GET /me. With an organization key the script says so, and whose workspace it saves to:
base https://liviamatch.com
key lmo_…sZbI, an organization key
acting for Hearth & Key Realty, the organization
saves to the workspace of Harper Quinn <harper@hearthandkey.example>, the admin who made it
named "brokerage-sync", expires 2027-01-04Its exit code is 0 when every step did what it should, 3 when one did not (the failed step's line carries the digest to quote), and 4 when a run was still working when --poll-ms ran out. It makes a client, a search and a run on the key's account, so run it against a tier the key belongs to.
Which calls are safe to send twice
| Call | Sent twice |
|---|---|
GET anything | Always safe. |
PUT /clients/{id}, PUT /searches/{id} | Lands once. The same body again is 200 and writes nothing; a different body is a new revision, or 412 under a stale If-Match. |
POST /searches/{id}/runs | Safe with an Idempotency-Key: the same run, Idempotent-Replayed: true, nothing more charged. Without one, a second run. |
PUT /runs/{id}/results | Safe: it sets the list, it does not add to it. |
POST /runs/{id}/shares | Safe: a share is one per run and client, and sending again updates it. |
POST /clients/{id}/access | Sends the client a fresh sign-in link and ends the last. Send it when you mean to. |
POST .../shares/{id}/email | At most one every ten minutes; the second is 429 with Retry-After. |
POST /clients, POST /searches | Makes another. Use PUT at an id you choose when a retry may happen. |
What is not in the API
- Deleting a client or a search. Archive and restore a client are here; deleting takes their shares and answers with them, and stays in Settings.
- The share studio's document: sections, brand, per-home words, Ask Livia, the public link and access codes. An API share is the default package of your results.
- One call that gives access, shares and emails. The three stay three, so a retry cannot email a client twice or not at all.
The reference for every call is under Clients, Searches, Runs, Shares and Data sets; Retries and concurrency explains Idempotency-Key and If-Match.