Published · Updated
How to Use the Shoply AI Search API
You can query Shoply AI Search from your own backend, without loading Shoply’s storefront widgets or sending the request through your Shopify storefront.
The standard Search API is available on every Shoply plan for Shopify, including Forever Free. Each plan indexes a different number of products; see our pricing page. Searches made through the API do not appear in your Shoply analytics.
If you expect high or steady API traffic, or you need a service-level agreement, custom authentication, guaranteed throughput, or a catalog that is not a Shoply-indexed Shopify store, contact us about an Enterprise plan.
Before You Start
Install Shoply AI on the Shopify store whose catalog you want to search and allow the initial product index to finish. You will need the store’s permanent myshopify.com domain, such as:
your-store.myshopify.comThe API can be called from any backend environment. A separate API key is not required for the standard product-search endpoint because it returns the same public catalog information that Shoply can display in the storefront search experience.
Endpoints
The Search API has two endpoints. Most integrations need only the first one.
| Endpoint | What it does |
|---|---|
POST https://api.shoplyai.ai/product_query_v2 | Returns ranked products, the result count, the sort options, and the filters (in search-page mode, the filters are sometimes sent separately) |
POST https://api.shoplyai.ai/search_display_filters | Returns the filters for a search-page result that came back without them; see Loading Filters Separately |
Both endpoints take a JSON body with Content-Type: application/json.
Add store_key to the URL of every request, and send the same value in the body:
https://api.shoplyai.ai/product_query_v2?store_key=your-store.myshopify.comShoply may route requests by this URL value, so include it even though the body also carries it.
Request Body
| Field | Required | Description |
|---|---|---|
store_key | Yes | Your store’s permanent myshopify.com domain |
query | Yes | A JSON-encoded string containing at least a keywords field |
start | No | Zero-based result offset for pagination; defaults to 0 |
limit | No | Maximum number of products to return, from 1 to 200; defaults to 4 |
question_context | No | A JSON object that selects search-page mode, applies filters, sorts results, or sets the language; see Search-Page Mode, Filters, and Sorting |
query is a JSON string inside the request body, not a nested JSON object. For example, the search phrase waterproof hiking boots is sent as:
"{\"keywords\":\"waterproof hiking boots\"}"query can also include description, a longer request in the shopper’s own words, for example {"keywords":"boots","description":"waterproof, for wide feet"}.
Send question_context as a normal JSON object, not as a string.
cURL Example
Replace your-store.myshopify.com with your store’s permanent Shopify domain in both places:
curl --request POST "https://api.shoplyai.ai/product_query_v2?store_key=your-store.myshopify.com" \
--header "Content-Type: application/json" \
--data '{
"store_key": "your-store.myshopify.com",
"query": "{\"keywords\":\"waterproof hiking boots\"}",
"start": 0,
"limit": 8
}'JavaScript Example
This example runs in Node.js 18 or later as an ES module (for example, a .mjs file). Elsewhere, wrap it in an async function.
const storeKey = "your-store.myshopify.com";
// Build the inner query separately because the API expects it as a JSON-encoded string.
const searchQuery = JSON.stringify({
keywords: "waterproof hiking boots"
});
const url = `https://api.shoplyai.ai/product_query_v2?store_key=${encodeURIComponent(storeKey)}`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
store_key: storeKey,
query: searchQuery,
start: 0,
limit: 8
})
});
if (!response.ok) {
throw new Error(`Shoply search failed with status ${response.status}`);
}
const results = await response.json();
console.log(results.docs);Python Example
import json
import requests
store_key = "your-store.myshopify.com"
# The query value is a JSON-encoded string, not a nested object.
payload = {
"store_key": store_key,
"query": json.dumps({"keywords": "waterproof hiking boots"}),
"start": 0,
"limit": 8,
}
response = requests.post(
"https://api.shoplyai.ai/product_query_v2",
params={"store_key": store_key},
json=payload,
timeout=30,
)
response.raise_for_status()
results = response.json()
print(results["docs"])Response
A successful request returns a response like this one, shortened to a single product:
{
"num_results": 34,
"docs": [
{
"product_id": "gid://shopify/Product/1234567890",
"url": "https://your-store.example/products/example-product",
"product_name": "Example Product",
"brand": "Example Brand",
"current_price": "129.00",
"currency": "USD",
"main_image": "https://cdn.shopify.com/example-product.jpg",
"selected_variant_id": "gid://shopify/ProductVariant/1234567891"
}
],
"display_filters": [
{
"key": "current_price",
"name": "Price",
"selections": [
{ "value": "range_0_100", "name": "$0.00 - $99.99", "count": 12 },
{ "value": "range_100_200", "name": "$100.00 - $199.99", "count": 15 }
]
}
],
"display_filters_pending": false,
"num_results_final": true,
"sort_by": "relevance",
"sort_options": { "price": true, "rating": false }
}num_resultsis the number of matching products Shoply ranked, at most 200. When filters are applied, it counts only the products that match them.docscontains the ranked products for the requested page. Prices, currency, and product links are for the store’s default market.display_filterscontains the available filters, such as price, brand, color, or other catalog-specific attributes. Each filter has akey, a displayname, and a list ofselections. Each selection has avalueto send back when applying the filter, a displayname, and usually a productcount. The list is empty when the merchant has hidden filters or the search found one product or none.num_results_finalisfalsewhile the AI is still checking the relevance of lower-ranked products, which happens mostly in search-page mode. Until it becomestrue,num_resultsis provisional: it can go up or down when the check finishes. Shoply’s own search page shows such a count with a plus sign, for example “34+”.display_filters_pendingistrueonly in search-page mode, when the products were sent before the filters were ready. In that casedisplay_filtersis empty; see Loading Filters Separately.sort_byandsort_optionsdescribe sorting; see Sorting.
To get the final count of a provisional result, repeat the same request about two seconds later. You can set limit to 1 for the repeat.
Product objects can contain additional fields depending on the store’s catalog, variants, pricing, and Shoply configuration. Filters can also contain extra fields, such as min and max for a price range. Clients should use the fields they need and safely ignore unfamiliar fields.
Pagination
Use start and limit to request additional results. For example, after requesting eight products with start: 0 and limit: 8, request the next page with:
{
"start": 8,
"limit": 8
}Stop when docs is empty. You can also stop when num_results_final is true and start + docs.length has reached num_results. Pages past the 200th product are always empty.
While num_results_final is false, do not stop on the count alone, because it may still change. A later page can also repeat a product from an earlier page or skip one, so drop products whose product_id you have already shown.
Search-Page Mode, Filters, and Sorting
Without question_context, product_query_v2 returns a short list of the best matches, the way Shoply’s chat does. To get the same results as Shoply’s AI Search page, add question_context with ui_type set to search_app:
{
"store_key": "your-store.myshopify.com",
"query": "{\"keywords\":\"snowboard\"}",
"start": 0,
"limit": 24,
"question_context": {
"ui_type": "search_app"
}
}Search-page mode returns the longer list that Shoply’s AI Search page shows. It also applies the merchant’s search-page settings, such as hiding sold-out products. Use it for a custom search results page.
question_context accepts these fields:
| Field | Description |
|---|---|
ui_type | Set to search_app for search-page results; omit it for the short default list |
selected_filters | Filters to apply, as an object that maps a filter key to a list of selection values |
sort_by | price_asc, price_desc, or rating; omit it to sort by relevance |
language_code | The shopper’s language, such as fr. Shoply uses it to match keywords and to translate filter names. Omit it to use the store’s default language. |
Filters
To apply a filter, copy the filter’s key and the selection’s value from display_filters. For example, this request keeps only products priced under $100:
{
"store_key": "your-store.myshopify.com",
"query": "{\"keywords\":\"snowboard\"}",
"start": 0,
"limit": 24,
"question_context": {
"ui_type": "search_app",
"selected_filters": {
"current_price": ["range_0_100"]
}
}
}Sorting
The response’s sort_options field tells you which sorts the store can offer for that search, for example {"price": true, "rating": false}. A sort the store cannot offer, or an unrecognized sort_by value, falls back to relevance. The response’s sort_by field names the sort that was applied.
Loading Filters Separately
Filters take longer to build than the product list because they need an extra AI step. For some short searches in search-page mode, Shoply returns the products first and builds the filters in the background.
Shoply decides this for each search, so always check display_filters_pending instead of predicting it. It can happen only when all of these are true:
question_context.ui_typeissearch_app.- The search is one or two words, with no description, and is not a product or model code.
- At least 12 of the top 18 results already contain the searched words.
When it happens, the product_query_v2 response has display_filters_pending: true and an empty display_filters list. Show the products right away, then post these fields to search_display_filters:
| Field | Required | Description |
|---|---|---|
store_key | Yes | The same store_key you sent to product_query_v2 |
query | Yes | The same query string you sent to product_query_v2 |
question_context | Yes | The same question_context object you sent to product_query_v2 |
Shoply saves each search’s full result for up to a day. This endpoint reads the filters from that saved search, which it finds from these fields, so send the same values you sent to product_query_v2. start and limit are not needed because the filters cover the whole result list, not one page.
The response looks like this:
{
"found": true,
"display_filters": [
{
"key": "current_price",
"name": "Price",
"selections": [
{ "value": "range_0_100", "name": "$0.00 - $99.99", "count": 12 }
]
}
]
}- If the filters are still being built, the endpoint waits up to 5 seconds before it answers.
- If
display_filtersis still empty after that wait, show the products without filters. An empty list can also mean the merchant hides filters or the search has too few products. Do not rerun the search just to get filters. foundisfalsewhen Shoply has no saved search for thatstore_key,query, andquestion_context. This endpoint never runs a search itself, so callproduct_query_v2first.
You do not need to wait for the filters before applying one. If a shopper picks a filter, send it in selected_filters to product_query_v2 as usual. Shoply waits for its own filter data before applying the selection.
const API_BASE = "https://api.shoplyai.ai";
// Both endpoints take store_key in the URL as well as in the body.
async function postToShoply(endpoint, body) {
const url = `${API_BASE}/${endpoint}?store_key=${encodeURIComponent(body.store_key)}`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`Shoply ${endpoint} failed with status ${response.status}`);
}
return response.json();
}
// Returns the products at once, plus a promise for the filters, so the page can show
// products before the filters arrive.
async function searchWithFilters(storeKey, keywords) {
const searchRequest = {
store_key: storeKey,
query: JSON.stringify({ keywords }),
question_context: { ui_type: "search_app" }
};
const results = await postToShoply("product_query_v2", { ...searchRequest, start: 0, limit: 24 });
const filters = results.display_filters_pending
// Same store_key, query, and question_context, so Shoply can find the saved search.
? postToShoply("search_display_filters", searchRequest)
.then(answer => (answer.found ? answer.display_filters : []))
: Promise.resolve(results.display_filters);
return { results, filters };
}Requests without question_context, or with a ui_type other than search_app, always return their filters in the product_query_v2 response. They never set display_filters_pending.
Errors
| Status | When it happens | What to do |
|---|---|---|
400 | The body is not valid JSON, store_key is missing or blank, a field has the wrong type, query is not a JSON-encoded object, question_context is not an object, start is negative, or limit is outside 1 to 200 | Fix the request. The response’s detail field names the problem. |
409 | Shoply has no search index for this store_key: the key is misspelled, it is a custom domain instead of the myshopify.com domain, the store has not installed Shoply AI, or the first index is still being built | Check the store_key first. If it is correct and the store installed Shoply AI recently, retry in a few minutes. |
Integration Notes
- Keep the call on your backend if you want to centralize caching, logging, retries, or access control.
- Your Shopify store must remain connected to Shoply AI so its product index can stay current.
- In search-page mode, handle
display_filters_pendingso short searches still show filters. - In every mode, treat
num_results_final: falseas a provisional count. - If your custom storefront also needs conversation, add chat to a headless Shopify storefront.
For help confirming your store key or planning a production integration, contact Shoply AI.
