Reference
HTTP API
Every endpoint the SDKs use, for calling Findlane from any language.
Requests
The API is at https://api.findlane.dev. Requests and responses are JSON: send Content-Type: application/json with every request body.
Index endpoints are under the workspace and index:
/api/teams/{workspaceId}/indexes/{index}Authenticate with an API key as a bearer token:
API=https://api.findlane.dev
curl "$API/api/teams/$FINDLANE_WORKSPACE_ID/indexes/products/search" \
-H "Authorization: Bearer $FINDLANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "q": "trail shoe", "facets": ["brand.name"], "limit": 10 }'Errors
Failed requests answer with an HTTP status and a message:
{ "error": "Filter field color is not configured" }| Status | Meaning |
|---|---|
400 | The request isn't valid. The message says why. |
401 | The API key is missing or invalid. |
402 | A plan limit: records, indexes, or monthly searches, or the workspace's spending limit. Searches our cache can answer still work. |
403 | The key lacks the scope or index, the origin isn't allowed, or the index's Limits and bots settings block this kind of traffic. |
404 | The index, or a staged replacement, doesn't exist. |
409 | A conflict, such as a record version that changed or another write during a staged replacement. |
415 | The request body isn't sent as application/json. |
429 | A rate limit: per visitor or per index for browser search, per API key, or the workspace's analytics reports. Wait for the Retry-After seconds. |
503 | Too many of the workspace's searches were running at once, the search took longer than 10 seconds, or Findlane couldn't reach its database for a moment (with Retry-After). Retry shortly, or simplify a slow search. |
5xx | A temporary problem. Retry with a backoff. |
Search
| Method | Path | Scope | |
|---|---|---|---|
POST | /search | search | Search the index. |
POST | /suggest | search | { "didYouMean": ... }: a did-you-mean suggestion for a search that found nothing, or null. Not counted as a search. |
POST | /recommendations | search | { "objectID": "sku-123" }: a product's recommendations, for indexes with the add-on. Not counted as a search. |
POST | /query-suggestions | search | { "q": "hrek", "limit": 5 }: { "suggestions": [{ "q", "typed" }] }, query suggestions for what was typed, or with q empty the popular searches. Not counted as searches. |
The body is a search request, with up to 100 hits a request, and the response is a search response. "subindexes" searches the index's subindexes too, as one search. /suggest takes the same request. "explain": true adds why each hit matched and what ordered it; see Explain results. "enableRules": false searches without the index's rules, and "rulesAt" checks them as at a time, in milliseconds since 1970.
Browser search
POST /api/public/indexes/{publicId}/search
POST /api/public/indexes/{publicId}/suggest
POST /api/public/indexes/{publicId}/recommendations
POST /api/public/indexes/{publicId}/query-suggestions
POST /api/public/search/api/public/search is a multi-index search: { "queries": [{ "index": "pub_…", "q": "…" }] }, up to 10, counted as one search for each workspace they search.
Browser search needs no key. It answers only requests whose Origin header is one of the index's allowed origins, returns up to 50 hits a request, doesn't take explain, enableRules or rulesAt, and follows the index's public search policy. It supports CORS, so browsers call it directly.
Lists
| Method | Path | Scope | |
|---|---|---|---|
GET | /lists/{id} | search | One of the index's lists, or 404. ?limit= gives fewer products or values. Not counted as a search. |
GET | /lists | search | { "lists": [...] }: the lists ?ids=a,b names (up to 10), in that order, or all of them without ids. |
GET | /lists/settings | either | How the lists are set up, each with how many products or values it holds and why its last build failed; the last build; and cap, the lists the plan allows per index. |
PUT | /lists/{id} | write | Creates or changes a list (its settings) and builds it. One that doesn't build is refused with why (400); a new list past the plan's cap gets 402. |
DELETE | /lists/{id} | write | Deletes a list. |
PUT | /lists/order | write | { "lists": ["new", "popular"] }: every list's ID once, in the order /lists answers them. |
POST | /lists/refresh | write | Builds the lists again now; once a minute, else 429 with Retry-After. |
POST | /lists/preview | write | A list's settings in the body: what it would hold, without saving it. |
The changes answer with the lists' settings, as /lists/settings does. List IDs are lowercase letters, digits and hyphens, and can't be settings, refresh, preview or order.
From browsers, without a key, from the index's allowed origins:
GET /api/public/indexes/{publicId}/lists/{id}
GET /api/public/indexes/{publicId}/lists?ids=popular,themesEvents
| Method | Path | Scope | |
|---|---|---|---|
POST | /events | events or write | { "events": [...] }: up to 200 views, clicks, add-to-carts, orders, and cancels. |
From browsers, without a key, up to 100 events a request from the index's allowed origins:
POST /api/public/indexes/{publicId}/eventsA server passing on a visitor's events from its own domain sends them to /events with its key and findlane-visitor-ip, plus findlane-visitor-agent, findlane-visitor-origin and findlane-visitor-country when it has them. They're then taken as browser events, limited per visitor. See Through your own domain.
Both answer 202 with { "accepted": 3, "rejected": [{ "index": 1, "reason": "..." }] }: one bad event doesn't refuse the rest. Events aren't counted as searches. See Events.
visitorId and visitId beside events link a shopper's events, when the index takes visitor IDs. { "forget": ["…"] } instead of events deletes the events sent with those IDs within a week: up to 2 IDs from browsers, 1,000 from servers.
Analytics
| Method | Path | Scope | |
|---|---|---|---|
GET | /analytics/searches | analytics | Searches, searches without results and response times, beside the same span before, with a count per hour or day. |
GET | /analytics/queries | analytics | Queries, the most searched first: ?list=top (the default) with their clicks, noResults, or noClicks. &limit= is 1 to 1,000, 100 by default. |
GET | /analytics/filters | analytics | The filter fields and sorts searches used. |
GET | /analytics/events | analytics | Events of each type per hour or day and by source, orders stored, and refused and bot events. |
GET | /analytics/products | analytics | The most viewed, most added to carts and most ordered products, with titles and images, and those viewed but rarely bought. |
GET | /analytics/product-counts | analytics | Every product with events or orders: views, viewers, clicks, add-to-carts and orders, for a table beside your own sales. |
GET | /analytics/products/{objectID} | analytics | One product's counts per hour or day, the queries it was clicked from, and its clicks in recommendations. |
GET | /analytics/revenue | analytics | Revenue from search: orders and add-to-carts of products found through search, per query and per product. |
GET | /analytics/search-terms | analytics | Search terms for ads and landing pages: the searches that sell, and those that found nothing, with why. format=csv and list=selling or list=missing download one list as CSV. |
GET | /analytics/brands | analytics | Brands: each brand's clicks on search results, and its products' add-to-carts, times bought and revenue from search. field picks the record field brands are in; format=csv downloads it. |
GET | /analytics/filter-sales | analytics | Sales by filter value: each facet value's clicks, add-to-carts, orders and revenue. |
An index's analytics, for showing them in your own admin. Only the analytics scope reads them. Call them from your server: requests from web pages, which carry an Origin header, are refused, so a key put in browser code fails at once. They aren't counted as searches.
The window is ?range= 24h, 7d (the default), 30d or 90d, ending now, or ?from= and &to= in UTC:
- A date such as
2026-10-01is the start of that day, and astoit includes the whole day. A time such as2026-10-01T12:00:00Zis taken as it is. tois now by default, and a window reaching past now ends now.- Analytics go back 90 days. A window starting earlier starts then, and the response's
fromsays where. /analytics/products,/analytics/product-countsand/analytics/revenuecount every event kept, which is 13 months, from 9 October 2026 10:20 UTC./analytics/filter-salesstarts on 26 October 2026, when clicks started carrying their filters.
&source= is all (the default), browser for searches and events from browsers, or server for those sent with API keys. Search reports leave out crawlers' searches, verified or not, from 16 October 2026; they're counted under Analytics → Traffic. &interval=hour or day sets the buckets of the reports with a series: hours up to 7 days and days beyond, by default.
{
"index": "products",
"from": "2026-10-01T00:00:00.000Z",
"to": "2026-10-08T00:00:00.000Z",
"source": "all",
"sampled": false,
"interval": "hour",
"totals": { "searches": 1840, "tracked": 1790, "noResults": 107, "noResultRate": 0.06, "browser": 1612, "server": 228,
"uniqueQueries": 604, "medianMs": 38, "p95Ms": 121 },
"previous": { "from": "2026-09-24T00:00:00.000Z", "to": "2026-10-01T00:00:00.000Z", "searches": 1702, "...": "..." },
"series": [{ "t": "2026-10-01T00:00:00.000Z", "searches": 4, "tracked": 4, "noResults": 0 }]
}-
Every report starts with
index,from,to,sourceandsampled. Whensampledistrue, some of the counts were sampled, so they're estimates: totals and lists stay close, and small counts less so. -
trackedis the searches whose query and result count were recorded, the base fornoResultRate.medianMsandp95Msare the API's response times. -
serieshas every hour or day of the window, with zeros where there were no searches. The first bucket starts at or beforefrom. -
/analytics/queriesanswers{ "list", "queries": [...] }. Queries are as searched, in lowercase, up to eight words. Top queries are{ "query", "searches", "noResults", "clicks", "averagePosition", "middlePosition" }: clicks on their results, the clicks' average place, and the middle click's place, which a few clicks far down don't pull up. Places are from 1, and each isnullwhen clicks aren't available. The other lists are{ "query", "searches" }. Clicks come from click events withqueryandposition, andnoClicksleaves out queries typed on the way to one that was clicked. -
/analytics/filtersanswers{ "searches", "filters": [{ "field", "searches" }], "sorts": [{ "sort", "searches" }] }, withsortnullfor searches that named none and used the index's default. -
/analytics/eventsanswers{ "types": [{ "type", "browser", "server" }], "orders": { "accepted", "stored" }, "bots", "refused": [{ "reason", "events" }], "series": [{ "t", "view", "click", "addToCart", "purchase", "cancel" }] }.
Products are { "objectID", "views", "viewers", "searchClicks", "carts", "orders" }:
viewersis how many visitor IDs viewed it, ornullwhen the index's views carry none.searchClicksis clicks on it outside recommendations, in search results and product lists.ordersis the orders stored with it, imported ones too.
/analytics/products and /analytics/product-counts are exact, and add computedAt, when they were counted (up to 5 minutes ago), and orders, the orders in the window:
/analytics/productslistsproducts, with each one'stitleandimagefrom its record, andviewedNotBought: products with 50 or more views, the fewest orders per view first, ornullwithout orders./analytics/product-countslists every product with events or orders inproducts, the most viewed first, up to 50,000.completeisfalsewhen there were more.
/analytics/revenue is exact too, with computedAt, and counts every source, so its source is all. Amounts are in currency, the one most orders use:
ordersandrevenueare the window's orders and their value, andsearchOrdersandsearchRevenuethose with a product the shopper clicked in search results in the 24 hours before.carts,cartValue,searchCartsandsearchCartValueare the same for add-to-carts, andsearchClicksis clicks on search results.openedis{ "orders", "revenue", "carts", "cartValue", "clicks", "byKind": { "ad", "email", "link" } }: orders whose last search click was on a search an ad, an email or another site opened, left out of the search numbers.cancelledis{ "orders", "revenue" }: orders in the main currency cancelled or refunded since, left out of every number.querieslists up to 100{ "query", "searches", "clicks", "carts", "orders", "revenue", "openedRevenue", "typed" }, the most revenue first.searchesisnullwhen the query isn't among the 1,000 most searched, andtypedlists shorter queries typed on the way, whose clicks count here.productslists up to 50{ "objectID", "title", "image", "orders", "revenue", "searchOrders", "searchRevenue", "openedRevenue", "topQuery" }bought after a search, the most revenue from search first.
/analytics/search-terms answers { "index", "from", "to", "computedAt", "currency", "selling", "missing" }. selling lists up to 100 { "query", "searches", "clicks", "carts", "orders", "revenue" } whose clicked products were added to carts or bought, the most revenue first, as in /analytics/revenue, and is null where revenue from search isn't set up. missing lists up to 500 { "query", "searches", "cause", "word" } searched at least 3 times that found nothing, the most searched first, without searches still being typed or junk; cause is "not-sold", "other-word" (with word, what the store calls it) or null before it's checked.
/analytics/brands is exact too, from 9 October 2026, and answers { "index", "from", "to", "computedAt", "field", "fields", "currency", "totals", "brands", "others" }. field is the record field brands came from (null when none was found; pick one of fields with field=), brands lists up to 50 { "brand", "searches", "clicks", "carts", "bought", "revenue" }, the most revenue first, and others adds up the rest with their number, brands. totals has the clicks and revenue of every product counted, for shares.
/analytics/filter-sales is exact too, with computedAt, since and currency. fields lists each facet field shoppers filtered on, { "field", "search", "page" }, with up to 20 values each on searches and on category pages, { "value", "clicks", "carts", "orders", "revenue" }, the most revenue first, and of ranges the 10 most clicked. unfiltered is the same for searches without filters.
otherCurrencieslists orders in other currencies, andcoveragesays what could be linked:{ "linkedOrders", "linkedClicks", "linkedSince", "visitorIds", "unpricedLines" }.
/analytics/products/{objectID} is fast, for a product page in your admin. It answers { "objectID", "totals", "series", "searchedFor": [{ "query", "clicks" }], "recommendationClicks": { "goesWith", "similar", "together" } }, with each bucket's counts in series. Its views and clicks are estimates when sampled, and an add-to-cart with several products counts toward the first one only. Its orders are exact.
/analytics/ab-test answers { "index", "running", "ended" }: the index's running A/B test with its results so far, and those that ended, as below. It takes no window.
Analytics are about a minute behind, and product counts about 2 minutes. A report is kept for 30 seconds, and for 10 minutes once its window has ended. A workspace can have 60 reports counted a minute, and kept reports don't count toward that.
Records
| Method | Path | Scope | Body |
|---|---|---|---|
POST | /records | write | { "records": [...] }: add or replace 1 to 1,000 records, all or none. |
PUT | /records | write | { "records": [...] }: replace the catalog with up to 100,000 records. |
DELETE | /records/{objectID} | write | |
GET | /records/{objectID} | either | |
POST | /record-writes | write | A conditional write, below. |
Writes answer 202 with a receipt, and are searchable when they do:
{ "acceptedRevision": 42, "indexedRevision": 42, "recordCount": 18230, "added": 3, "changed": 12, "unchanged": 985, "removed": 0 }added, changed, unchanged and removed say what the write did: records new to the index, records whose values changed, records sent exactly as the index holds them, which change nothing, and records deleted. Conditional writes and a staged replacement's commit have them too.
GET /records/{objectID} answers { "objectID", "version", "record" }, where record is null if the record doesn't exist.
Conditional writes
{
"operation": "upsert",
"objectID": "sku-1042",
"record": { "objectID": "sku-1042", "title": "Trail running shoe", "price": 119 },
"expectedVersion": 3,
"idempotencyKey": "price-update-8f14e45f"
}The write applies only if the record's version is expectedVersion (0 for a record that doesn't exist), and answers 409 otherwise. operation is upsert or delete; a delete has no record. Repeating a request with the same idempotencyKey (1 to 64 letters, digits, _, or -) returns the first receipt instead of writing again.
Staged replacements
A staged replacement uploads a catalog in batches and switches the index to it in one commit. stageId and batchId are 1 to 64 letters, digits, _, or -.
The paths are under /staged-replacements/{stageId}, and need the write scope:
| Method | Path | Body |
|---|---|---|
POST | /staged-replacements/{stageId} | Start the stage, or get its status. |
POST | …/batches/{batchId} | { "records": [...] }: 1 to 1,000 records. |
POST | …/commit | { "expectedBatches": 12, "expectedRecords": 11408 } |
GET | /staged-replacements/{stageId} | The stage, and what committing it now would do: added, changed, unchanged and removed records. |
DELETE | /staged-replacements/{stageId} | Abort the stage. |
Uploading a batch again with the same records has no effect; with different records it fails with 409. The commit checks the batch and record counts, and fails with 409 if other writes reached the index since the stage started; with "keepNewer": true, it commits anyway, and records written or deleted since the stage started keep that newer state. Committing again returns the first receipt.
A stage of more than 20,000 records commits in steps, and the commit answers once they're all done. While it commits, the stage's status is committing, other writes to the index wait for it (record writes go ahead with "keepNewer": true, and keep their state), and DELETE fails with 409. Committing again waits for the same commit.
Updates
What has come into the index, as the dashboard's Updates page shows it: for a sync's status in your own admin, or to check what an upload did. Like analytics, these need the analytics scope and work only from your server.
| Method | Path | Scope | |
|---|---|---|---|
GET | /writes | analytics | ?range= 24h (the default), 7d, 30d or 90d: totals, a series, each source, and replacements under way. |
GET | /writes/log | analytics | The latest operations, newest first. ?limit= is 1 to 200 (50 by default), &before= an operation's id for older ones, and &problems=1 gives refused ones only. |
/writes answers { "range", "status", "totals", "series", "sources", "underWay" }:
statushaslastData, the last operation that sent records, whether or not it changed any;lastChange, the last that changed something; andlastProblem, the last refused. It also hasacceptedRevision,indexedRevision, the revision searches everywhere have, andlagMs, how long the oldest change they don't have yet has waited.totalsand each ofsourcescountoperations,refusedandbytes, and the recordssent,added,changed,unchangedandremoved. A source is{ "kind", "label" }. Itskindiskey(labelled with the key's name now, orDeleted key),woocommerce,dashboard,mcp(with the app's name),feed,shopify,system(Findlane's own imports) orunknown.serieshas a point an hour for24h, one every 6 hours for7d, and one a day for longer ranges, oldest first.underWaylists replacements being uploaded (open) or committed in steps (committing), withdoneoftotalchanges so far.
/writes/log answers { "rows", "more" }, and each operation is { "id", "at", "lastAt", "repeats", "source", "op", "status", "reason", "exampleId", "sent", "added", "changed", "unchanged", "removed", "revision" }:
- Alike operations in a row are one, with
repeatsfromattolastAt, so a sync that changes nothing every 15 minutes takes one row until something changes. opisupsert,delete,conditional,replacement,settingsorload.- A refused operation has its
status(400 and up) andreason, andexampleIdis a refused record'sobjectID. Record values are never kept.
An index keeps its hourly totals for 90 days, and its latest 500 operations and 200 refused ones. The numbers start on 6 November 2026, and /writes is kept for 5 seconds.
Settings
| Method | Path | Scope | |
|---|---|---|---|
GET | /config | either | The settings, with a change in progress in rebuild. |
PUT | /config | write | Save the full settings. The response adds applied, rebuilt, and rebuild. |
GET | /fields | search | Field paths in the index's records, with their types and how many records have each. |
GET | /suggestions | either | Suggested settings for the index's kind of data: settings, each field's role, and the kind used. ?kind= suggests for another kind. Once the settings have been changed, only { "configured": true, "kind" }. |
PUT | /kind | write | { "kind": "products" }: what the index holds (products, docs, articles, listings, other, or null). |
GET | /indexing | either | { "acceptedRevision", "indexedRevision", "recordCount" } |
GET | /query-suggestions/status | either | The query suggestion settings, and the list's last build: builtAt, entries, how many came from shoppers' searches (searched) and the catalog, the catalog fields used, and error. |
POST | /query-suggestions/rebuild | write | Rebuild the query suggestion list now, and answer as the status does. |
POST | /compare | write | Compare settings with the saved ones without saving them, below. |
GET | /tests | either | { "tests": [...] }: the index's search tests. |
PUT | /tests | write | { "tests": [...] }: replace the search tests, up to 200; [] deletes them. A test the index can't run, such as one filtering a field that isn't a facet or filter field, is refused with 400 and a message that names it. |
A/B tests
| Method | Path | Scope | |
|---|---|---|---|
GET | /ab-tests | either | { "running", "ended" }: the running A/B test with its results so far, or null, and up to 10 that ended, newest first. |
POST | /ab-tests | admin | Start one: { "settings": {...} }, the settings to test as PUT /config takes them, or { "aa": true }; with share (0.1 to 0.5, 0.5 by default), days (7 to 60, 14 by default) and name. 409 while another runs, or for a change the index can't serve side by side. |
GET | /ab-tests/estimate?share= | either | { "searchesPerDay", "clicksPerSearch", "days", "extraMisses", "extraSearchesPerDay" }: the days a test needs to find a 10% change in clicks per search (null with too few), how many more searches miss the cache (0.3 is 30% more), and about how many a day, from the last 7 days (null with too few). |
POST | /ab-tests/{id}/end | admin | { "keep": "a" } keeps today's settings, { "keep": "b" } applies the change. |
POST | /ab-tests/{id}/apply | admin | Applies an ended test's change to today's settings. |
A test is { "id", "name", "share", "changes", "aa", "patch", "startedAt", "endsAt", "endedAt", "outcome", "results" }:
changesnames the settings it changes, andpatchholds their values for group B (nullwhere it goes without one).outcomeis"a"(kept today's settings),"b"(applied the change) or"guard"(ended early: the change made searches find nothing far more often).resultsis{ "days", "decided", "verdict", "searches": { "a", "b" }, "currency", "figures", "perDay", "computedAt" }.figureshasnoResults,clicks,topThree,carts,ordersandrevenue, each{ "a", "b", "change", "low", "high" }: each group's value per search (noResultsandtopThreeare shares), and B's change with its 95% interval.verdictis"b","a"or"none"oncedecided, andnullbefore.
Starting, ending and applying a test take an owner or admin signed in to the dashboard, or their agent on the MCP server; API keys read tests but don't change them.
A browser search on an index running a test answers with abTest: { "id", "variant" }, the shopper's group. An events request may send it back as abTest: "<id>:<variant>", as the browser SDK does. A server's own events count toward a group only with it.
Agents use start_ab_test, get_ab_test and end_ab_test on the MCP server.
Merchandising
Category pages and searches arranged as the dashboard's Merchandising page arranges them, for building it into your own admin. Each is saved as that page's rule, which applies within seconds and keeps anything else the rule does, such as a boost. A target is a category page, field and value (such as category and Jackets), or a search's exact words, q.
| Method | Path | Scope | |
|---|---|---|---|
GET | /merchandising | either | { "arrangements": [...] }: every arranged category page and search. |
GET | /merchandising/arrangement | either | One target's arrangement, from ?field=&value= or ?q=; status is none when nothing is arranged. |
PUT | /merchandising/arrangement | write | Change one: the target, and any of order, hidden, banner, active, enabled. Fields left out stay as they are. |
DELETE | /merchandising/arrangement | write | Stop arranging one, from ?field=&value= or ?q=: its order, hidden products and banner go. |
GET | /images | either | The index's uploaded images, the rules showing each, and its limits. |
POST | /images | write | Upload an image: its bytes as the body, or JSON { "url" } (an https address Findlane fetches) or { "data" } (base64). |
DELETE | /images/:key | write | Delete an image; refused with 409 while a rule's banner shows it. |
GET | /banners | analytics | Each banner's shown and clicks by its rule, over ?range= 7d, 30d (the default) or 90d. |
{
"field": "category",
"value": "Jackets",
"order": ["sku-311", "sku-120"],
"hidden": ["sku-098"],
"banner": { "image": "https://images.findlane.dev/…/….webp", "link": "/sale", "title": "Winter sale", "position": 1 },
"active": { "from": "2026-11-27T00:00:00Z", "until": "2026-11-30T23:59:00Z" }
}- order: up to 20 objectIDs that come first, at places 1 to N; the rest follow the index's ranking.
[]for none. - hidden: up to 100 objectIDs left off the page. A hidden product leaves the order.
- banner: an https
image(one uploaded here, or your own), and optionally alink(https, or a path such as/sale), atitle, and the place it shows before;nullfor none. - active: a campaign's times, ISO 8601 with their zone;
nullfor always. enabled:falseturns it off without losing it.
An arrangement answers with its target, rule, order (null when the rule's pins have gaps, set through rules, and listed in pins), hidden, banner, active, enabled, and status: live, scheduled, ended, off, or none. Images are JPEG, PNG, WebP or AVIF, up to 2 MB, 50 and 100 MB an index; one no rule shows is deleted a week later.
Product feeds
An index's product feed: a store's Google or Meta feed, or a JSON file of records, read on a schedule.
| Method | Path | Scope | |
|---|---|---|---|
GET | /feed | either | { "feed": ... }: the feed and its last run, or null. |
PUT | /feed | write | Connect a feed or change it: any of url, username, password, schedule, hour, variants, paused. |
DELETE | /feed | write | Disconnect it; the index keeps its records. |
POST | /feed/check | write | Read a feed's first 2,000 items without saving anything: { "url" }, or nothing for the connected feed. |
POST | /feed/runs | write | Read the feed now, in full, in the background: 202 with the run. |
GET | /feed/runs | either | { "runs": [...] }: the last 20, newest first. |
POST | /feed/runs/{runId}/import | write | Import a held run's copy of the feed as it is: 202 with the new run. |
{
"url": "https://shop.example/feed.xml",
"username": "findlane",
"password": "…",
"schedule": "daily",
"hour": 4,
"variants": "group"
}- url:
https, orhttpwithout a password, on a named host. A new address, sign-in orvariantsreads the feed at once. - username, password: for a feed behind HTTP basic sign-in;
nullclears one. The password is stored encrypted, andGETsays only whether there is one. - schedule:
hourly(the default),6h, ordailyathour(0 to 23, UTC). - variants:
group(the default) makes the items sharing anitem_group_idone record listing them;separatekeeps a record each. - source:
file(the default), one file at the address; orpages, a JSON endpoint answering{ records, next }a page at a time (see paged sources). - paused:
truestops the runs;falseresumes them, as after a week of failures.
A run has a status: running, unchanged (the file hadn't changed), imported, held (not imported: message says why), or failed. With them come items read, records made, records added, changed and removed, and refused items, with reasons (how many for each) and refusals (the first 100, with their IDs). A feed also says whether it's paused and why (owner or failing), its failures in a row, nextRunAt, and held, the run Import anyway would import.
Comparing settings
POST /compare searches with the saved settings and with the ones sent, without saving them: the index's search tests, the queries sent, and unless searched is false, its 100 most searched queries of the last 30 days. It runs for up to 20 seconds and doesn't count as searches.
{
"settings": { "rules": [] },
"queries": ["blöðrur", { "q": "kerti", "filters": { "category": "Candles" } }],
"searched": true
}settings takes what PUT /config does, or only the settings that change; {} runs the tests with the saved settings. The response:
{
"compared": 102,
"truncated": false,
"searches": 20419,
"summary": {
"topChanged": { "queries": 2, "searches": 32 },
"moreResults": { "queries": 0, "searches": 0 },
"fewerResults": { "queries": 2, "searches": 32 },
"nowNothing": { "queries": 0, "searches": 0 },
"nowSomething": { "queries": 0, "searches": 0 }
},
"tests": [
{ "id": "halloween-costumes", "q": "hrekkjavökubúningar", "expect": { "minResults": 100 }, "before": true, "after": false }
],
"changes": [
{
"q": "hrekkjavökubúningar",
"searches": 30,
"before": { "total": 1247, "top": [{ "objectID": "witch-hat", "title": "Witch hat" }] },
"after": { "total": 1, "top": [{ "objectID": "card-halloween", "title": "Halloween card" }] },
"gone": [{ "objectID": "witch-hat", "title": "Witch hat", "from": 1, "to": null }],
"added": [{ "objectID": "card-halloween", "title": "Halloween card", "from": 14, "to": 1 }],
"moved": []
}
],
"errors": []
}tests: every search test, withbeforeandafter: whether it passes with the saved settings and with the ones sent.skipped: truewhen time ran out before it ran.changes: up to 50 queries whose top 10 or number of results changed, the tests' queries first, then the most searched.topis the first 5 results;fromandtoare places in the first 20, andnullis outside them.truncated: time ran out before every query was compared.
Settings that change what the index stores (new searchable, facet, filter, sort, ranking, or location fields, or text normalization) answer 409: they can only be compared after saving. 503 means too many of the workspace's searches were running; retry shortly.
Browser search access
| Method | Path | Scope | |
|---|---|---|---|
GET | /public-access | write | { "publicId", "allowedOrigins", "policy", "imageHosts", "imageField" } |
PUT | /public-access/policy | write | { "policy": { "returnFields": [...] } }, and optionally the other lists: see Security. |
DELETE | /public-access/policy | write | Remove the policy. |
PUT | /public-access/images | write | Optimized images, on paid plans: { "hosts": ["cdn.shop.example.com"], "field": "image" }. [] turns them off. |
GET | /public-access/image-hosts | write | The image hosts and fields found in a sample of the records, placeholder progress, and the month's new copies. |
Optimized images are included in every paid plan. Findlane resizes the images in an index's records to the size a page shows them at and sends them as WebP from images.findlane.dev, and each hit carries a placeholder: a blurred preview of its image, shown until it loads. The browser SDK's Image component, client.imageUrl and the search widget use them.
hosts: the hosts the records' image addresses are on. Only images on these hosts that are in the index's records are resized; anything else loads as it is.field: the record field placeholders are made from:imageunless you set another, with a list's items asimages.src.- Copies: each plan includes 5 new resized copies a month per record it includes, one for each photo and width shown. Copies already made load free and don't count. Past it, photos without a copy yet load as their originals until the next month.
- Public answers name the hosts in
x-findlane-images, and where images load from inx-findlane-image-origin. - On Free, setting hosts answers
402.
Indexes, API keys, and allowed origins are managed in the dashboard, or by an AI agent through the MCP server.