curl --request GET \
--url https://www.closient.com/search/api/v1/search/productsimport requests
url = "https://www.closient.com/search/api/v1/search/products"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://www.closient.com/search/api/v1/search/products', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://www.closient.com/search/api/v1/search/products",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://www.closient.com/search/api/v1/search/products"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://www.closient.com/search/api/v1/search/products")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/search/api/v1/search/products")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_body{
"disambiguation": [],
"facets": {
"availability_confidence": [
{
"count": 3,
"value": "confirmed"
}
],
"brand": [
{
"count": 3,
"value": "Acme Beauty"
}
],
"category": [
{
"count": 3,
"value": "Beauty > Hair Care > Shampoo"
}
],
"distance_bands": [
{
"count": 2,
"max_km": 5,
"min_km": 0
}
],
"price_range": {
"max_price": 14.49,
"min_price": 6.99
},
"retailer": [
{
"count": 4,
"value": "retailerXyZ789"
}
]
},
"is_automatic_winner": true,
"local": [
{
"availability_confidence": "confirmed",
"currency": "USD",
"distance_km": 2.4,
"gln": "0614141000012",
"price": 9.99,
"retailer_id": "retailerXyZ789",
"retailer_name": "Acme Beauty",
"store_id": "storeAbC123",
"store_name": "Acme Beauty — Downtown"
}
],
"local_total_count": 1,
"online": [],
"page": 1,
"page_size": 20,
"query": "acme shampoo",
"winner": {
"brand": "Acme Beauty",
"category_path": "Beauty > Hair Care > Shampoo",
"gtin": "00012345678905",
"is_default_variant": true,
"product_name": "Acme Shampoo",
"score": 12.4381,
"variant_group": "fam_9f2c1a",
"variant_name": "12oz"
}
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}Search for a product and where to buy it nearby
Two-stage product search: BM25 match against the product catalog (fuzzy, synonym-aware), then a geo-filtered lookup of physical store availability. Local results always precede online; online offers appear only when local results are empty or include_online is set. Pass retailer_id or store_id to scope the same pipeline to one retailer or one store — a store-scoped query skips distance sorting and just answers whether that store carries the product.
Auth: unauthenticated (anonymous IP-throttled at 60 requests/minute).
curl --request GET \
--url https://www.closient.com/search/api/v1/search/productsimport requests
url = "https://www.closient.com/search/api/v1/search/products"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://www.closient.com/search/api/v1/search/products', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://www.closient.com/search/api/v1/search/products",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://www.closient.com/search/api/v1/search/products"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://www.closient.com/search/api/v1/search/products")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/search/api/v1/search/products")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_body{
"disambiguation": [],
"facets": {
"availability_confidence": [
{
"count": 3,
"value": "confirmed"
}
],
"brand": [
{
"count": 3,
"value": "Acme Beauty"
}
],
"category": [
{
"count": 3,
"value": "Beauty > Hair Care > Shampoo"
}
],
"distance_bands": [
{
"count": 2,
"max_km": 5,
"min_km": 0
}
],
"price_range": {
"max_price": 14.49,
"min_price": 6.99
},
"retailer": [
{
"count": 4,
"value": "retailerXyZ789"
}
]
},
"is_automatic_winner": true,
"local": [
{
"availability_confidence": "confirmed",
"currency": "USD",
"distance_km": 2.4,
"gln": "0614141000012",
"price": 9.99,
"retailer_id": "retailerXyZ789",
"retailer_name": "Acme Beauty",
"store_id": "storeAbC123",
"store_name": "Acme Beauty — Downtown"
}
],
"local_total_count": 1,
"online": [],
"page": 1,
"page_size": 20,
"query": "acme shampoo",
"winner": {
"brand": "Acme Beauty",
"category_path": "Beauty > Hair Care > Shampoo",
"gtin": "00012345678905",
"is_default_variant": true,
"product_name": "Acme Shampoo",
"score": 12.4381,
"variant_group": "fam_9f2c1a",
"variant_name": "12oz"
}
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}{
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"status": 404,
"timestamp": "2026-03-31T12:00:00+00:00",
"title": "Not Found",
"type": "https://closient.com/docs/errors/not_found"
}Query Parameters
Search query text.
1 - 255^[^\x00]{1,255}$Latitude in decimal degrees (WGS84). Pair with longitude.
-90 <= x <= 90Longitude in decimal degrees (WGS84). Pair with latitude.
-180 <= x <= 180Postal code to resolve to a location when latitude/longitude aren't available. Ignored when latitude/longitude are both given. Resolved against Closient's own store location data (no third-party geocoder); returns 422 if it cannot be resolved.
^[^\x00]{0,}$Search radius in kilometers from the resolved location.
x <= 100Scope to one retailer's stores (a retailer/store landing page's own search box). Distinct from retailer, the multi-select facet filter — both apply together if both are set.
^[^\x00]{0,}$Scope to one physical store. Skips distance sorting — answers whether this store has it.
^[^\x00]{0,}$Include online offers even when local results are non-empty.
Page number of local results.
1 <= x <= 10000000Local results per page.
1 <= x <= 100C-4718 facet: filter offers to one or more retailer ids. Repeat the parameter for multiple.
^[^\x00]{0,}$Filter BM25 candidates to one or more brand names (case-insensitive). Repeat for multiple.
^[^\x00]{0,}$Filter BM25 candidates to one or more category paths (case-insensitive). Repeat for multiple.
^[^\x00]{0,}$Filter offers to one or more confidence levels. Repeat the parameter for multiple.
Mirror of apps.retailers.models.InventoryConfidence (C-4099/C-4716).
Computed at OpenSearch index time from an offer's stock signals (source
provenance, freshness, quantity) rather than stored on the offer row
itself — see apps.retailers.services.inventory.calculate_confidence.
Surfaced on every availability index document as
availability_confidence and echoed on each local/online offer row
returned by the product search endpoint.
confirmed, likely, uncertain, unknown, out_of_stock Minimum offer price.
x >= 0Maximum offer price.
x >= 0Reported accuracy in meters of latitude/longitude, when the caller has one (e.g. the browser Geolocation API's own accuracy reading). Never affects search results — captured only for the C-4723 demand-signal event, where it gates whether at_store_candidate is computed.
x >= 0Which UI surface issued this request. Never affects search results — captured only for the C-4723 demand-signal event.
navbar, scan_page, watching_notice, esl, mcp, api Response
OK
Response envelope for GET /search/products.
The search query that was executed.
True when the top BM25 score beat the next distinct product by at least the tunable ratio (1.5x) and the winner can be presented without disambiguation.
Total matching physical offers, before pagination.
x >= 0Page number of local results returned.
x >= 1Page size applied to local results.
x >= 1The best-matching product. Null only when the query matched nothing at all.
Show child attributes
Show child attributes
{
"brand": "Acme Beauty",
"category_path": "Beauty > Hair Care > Shampoo",
"gtin": "00012345678905",
"is_default_variant": true,
"product_name": "Acme Shampoo",
"score": 12.4381,
"variant_group": "fam_9f2c1a",
"variant_name": "12oz"
}
Other distinct near-scoring products, for C-4718 disambiguation chips. Always empty when is_automatic_winner is true.
Show child attributes
Show child attributes
The winner's physical stores, sorted by distance (or unsorted if no location was given).
Show child attributes
Show child attributes
The winner's online offers. Populated only when local is empty or include_online is set.
Show child attributes
Show child attributes
Why the online section is populated. Null when online is empty.
no_local_results, requested Facet aggregations over the candidate pool.
Show child attributes
Show child attributes
{
"availability_confidence": [
{ "count": 3, "value": "confirmed" },
{ "count": 2, "value": "likely" }
],
"brand": [{ "count": 3, "value": "Acme Beauty" }],
"category": [
{
"count": 3,
"value": "Beauty > Hair Care > Shampoo"
}
],
"distance_bands": [
{ "count": 2, "max_km": 5, "min_km": 0 },
{ "count": 3, "max_km": 10, "min_km": 5 }
],
"price_buckets": [
{ "count": 2, "max_price": 10 },
{
"count": 3,
"max_price": 25,
"min_price": 10
}
],
"price_range": { "max_price": 14.49, "min_price": 6.99 },
"retailer": [{ "count": 4, "value": "retailerXyZ789" }]
}
Correlation token for this search (C-4721) — pass it to POST /search/api/v1/search/click when the caller reports a post-search interaction (a result was clicked, the radius was expanded, etc.), so the click can be attributed back to this search for CTR-by-position measurement. Null only if demand-signal capture itself failed to dispatch (never surfaces as a request error — see apps.search.services.demand_signal.dispatch_capture); a caller that gets null has nothing meaningful to click-report against.