> For the complete documentation index, see [llms.txt](https://alhena.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://alhena.gitbook.io/docs/developer-reference/api/ai-search.md).

# AI Search

The AI Search API brings Alhena's AI-powered product search to any surface: mobile apps, kiosks, voice assistants, or your own storefront UI. Send a natural-language query and get back ranked products from your live catalog together with a conversational answer, in one request.

AI Search understands what shoppers mean, not just what they type. A query like *"white cotton t-shirt under $50"* is parsed into intent and constraints such as category, color, material, and price, then matched against your synced product catalog:

* **Natural-language understanding.** Constraints like price caps, colors, materials, and occasions are extracted automatically. No filter syntax to build.
* **Conversational refinement.** Every response returns a `thread_id`. Send it back with the next query ("show it in black", "something cheaper") and results refine in context.
* **Live catalog data.** Results come from your connected store (Shopify, BigCommerce, Salesforce Commerce Cloud, and more) with real-time stock status, current prices, and review ratings.

{% hint style="info" %}
API access is available to enterprise customers. Contact the Alhena team to request access. Your API key must include the **AI Search** scope. See [Managing API Keys](/docs/developer-reference/api/managing-api-keys.md).
{% endhint %}

## Base URL and Versioning

All requests use HTTPS and JSON request/response bodies.

```
https://api.alhena.ai/api/v1
```

The API is versioned in the URL path. Backwards-compatible changes (new optional request fields, new response fields) may be added to `v1` at any time; breaking changes will only ship in a new version. Build your integration to ignore response fields it doesn't recognize.

## Terminology

A **query** is the shopper's search input, in plain language. Full sentences work: *"a gift for a runner under $100"* is a valid query.

An **answer** is a short conversational response generated with the results, e.g. *"I found a few white cotton tees under $50. The Classic Cotton Tee is the closest match."* Display it above or beside the product grid, or ignore it if you only want results.

A **result** is a product from your catalog, ranked by relevance. Each result is a specific product variant (the white / size M version of a tee), so prices, images, and stock are exact.

A **search session** is a sequence of related queries sharing a `thread_id`. It works the same way as [threads in Send Message](/docs/developer-reference/api.md#how-thread_id-works): omit `thread_id` to start a new session, and pass the returned `thread_id` on follow-up queries to refine in context.

***

## Search

<mark style="color:green;">`POST`</mark> `https://api.alhena.ai/api/v1/search`

Run an AI-powered search against your product catalog.

#### Headers

| Name               | Type   | Description             |
| ------------------ | ------ | ----------------------- |
| `Authorization` \* | string | `Bearer <your_api_key>` |
| `Content-Type` \*  | string | `application/json`      |

#### Request Body

| Name                 | Type    | Description                                                                                                                                              |
| -------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query` \*           | string  | The shopper's search, in natural language, e.g. `"white cotton t-shirt under $50"`                                                                       |
| `thread_id`          | string  | Continue a search session so this query refines the previous results. Omit to start a new session; the response always returns the session's `thread_id` |
| `similar_product_id` | string  | Find products similar to one already returned by passing that result's `product_id`. Useful for "more like this" buttons                                 |
| `max_results`        | integer | Maximum number of results to return. Default and maximum: `20`                                                                                           |
| `locale`             | string  | Language code for the `answer` text (default: `en`). Product links use locale-specific URLs when your catalog provides them                              |
| `page_url`           | string  | URL of the page the shopper is on, for context and analytics. Must be http or https                                                                      |
| `user_metadata`      | object  | Custom metadata to associate with the search session. See [Custom Data](/docs/developer-reference/website-sdk/custom-data.md)                            |

#### Response

| Field                | Type    | Description                                                                                                                                  |
| -------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`              | string  | Echo of the query you sent                                                                                                                   |
| `answer`             | string  | Conversational answer generated with the results                                                                                             |
| `results`            | array   | Ranked product results, best matches first. Empty when the query needs no products, e.g. when the AI asked a clarifying question in `answer` |
| `total_results`      | integer | Number of results returned                                                                                                                   |
| `thread_id`          | string  | Search session ID. Pass it back on the next request to refine                                                                                |
| `processing_time_ms` | integer | Server-side processing time in milliseconds                                                                                                  |
| `search_id`          | string  | Unique ID for this request. Include it when contacting support about a specific search                                                       |

#### Result Object

Each entry in `results` is a product variant with everything needed to render a product card:

| Field                 | Type    | Description                                                                                            |
| --------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `product_id`          | string  | Alhena ID of the product. Use it as `similar_product_id` in follow-up searches                         |
| `variant_id`          | string  | Alhena ID of the specific matched variant                                                              |
| `match_type`          | string  | `MATCH`: every detected constraint matched. `PARTIAL_MATCH`: a close alternative matching some of them |
| `matched_attributes`  | object  | Which query constraints this result matched, e.g. `{"color": "white", "material": "cotton"}`           |
| `title`               | string  | Product title                                                                                          |
| `link`                | string  | Product page URL                                                                                       |
| `images`              | array   | Image URLs for the matched variant                                                                     |
| `current_price`       | number  | Current price of the variant                                                                           |
| `original_price`      | number  | Pre-discount price; equals `current_price` when not on sale                                            |
| `currency`            | string  | ISO currency code, e.g. `USD`                                                                          |
| `category`            | string  | Product category from your catalog                                                                     |
| `attributes`          | object  | The variant's own options, e.g. `{"color": "white", "size": "M"}`                                      |
| `in_stock`            | boolean | Real-time stock status of the variant                                                                  |
| `platform`            | string  | Source platform: `SHOPIFY`, `BIGCOMMERCE`, `SALESFORCECC`, or `OTHER`                                  |
| `external_product_id` | string  | Product ID on the source platform                                                                      |
| `external_variant_id` | string  | Variant ID on the source platform, e.g. for Shopify's `cart/add?id=` URL                               |
| `rating`              | number  | Average review rating, when available                                                                  |
| `rating_count`        | number  | Number of reviews, when available                                                                      |

#### Example Request

{% tabs %}
{% tab title="cURL" %}

```shell
curl https://api.alhena.ai/api/v1/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ALHENA_API_KEY" \
  -d '{
    "query": "white cotton t-shirt under $50"
  }'
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const response = await fetch("https://api.alhena.ai/api/v1/search", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${process.env.ALHENA_API_KEY}`,
  },
  body: JSON.stringify({ query: "white cotton t-shirt under $50" }),
});

const { answer, results, thread_id } = await response.json();
```

{% endtab %}

{% tab title="Python" %}

```python
import os
import requests

response = requests.post(
    "https://api.alhena.ai/api/v1/search",
    headers={"Authorization": f"Bearer {os.environ['ALHENA_API_KEY']}"},
    json={"query": "white cotton t-shirt under $50"},
)

search = response.json()
print(search["answer"])
for product in search["results"]:
    print(product["title"], product["current_price"])
```

{% endtab %}
{% endtabs %}

#### Example Response

```json
{
  "query": "white cotton t-shirt under $50",
  "answer": "I found a few white cotton t-shirts under $50. The Classic Cotton Tee is the closest match, and there are two similar options you might like.",
  "results": [
    {
      "product_id": "a3f5c1b2e4d67890a3f5c1b2e4d67890",
      "variant_id": "d41d8cd98f00b204e9800998ecf8427e",
      "match_type": "MATCH",
      "matched_attributes": { "color": "white", "material": "cotton", "max_price": "50" },
      "title": "Classic Cotton Tee",
      "link": "https://store.example.com/products/classic-cotton-tee?variant=44567890123456",
      "images": ["https://cdn.example.com/products/classic-tee-white.jpg"],
      "current_price": 39.0,
      "original_price": 49.0,
      "currency": "USD",
      "category": "T-Shirts",
      "attributes": { "color": "white", "size": "M" },
      "in_stock": true,
      "platform": "SHOPIFY",
      "external_product_id": "8712345678901",
      "external_variant_id": "44567890123456",
      "rating": 4.6,
      "rating_count": 128
    },
    {
      "product_id": "b4e6d2c3f5a78901b4e6d2c3f5a78901",
      "variant_id": "9e107d9d372bb6826bd81d3542a419d6",
      "match_type": "PARTIAL_MATCH",
      "matched_attributes": { "color": "white" },
      "title": "Everyday Blend Tee",
      "link": "https://store.example.com/products/everyday-blend-tee",
      "images": ["https://cdn.example.com/products/everyday-tee-white.jpg"],
      "current_price": 45.0,
      "original_price": 45.0,
      "currency": "USD",
      "category": "T-Shirts",
      "attributes": { "color": "white", "size": "L" },
      "in_stock": true,
      "platform": "SHOPIFY",
      "external_product_id": "8712345678902",
      "external_variant_id": "44567890123999",
      "rating": 4.2,
      "rating_count": 54
    }
  ],
  "total_results": 2,
  "thread_id": "bb708939-b626-48eb-a403-2ea25bb38941",
  "processing_time_ms": 1240,
  "search_id": "7f3d0b6a-91c4-4e2a-b7a4-6f2f4f8f2d10"
}
```

#### Response with a Clarifying Question

When a query is too broad to search well, the AI asks for more detail instead of guessing. `results` is empty and `answer` carries the question. Show the answer and let the shopper reply in the same session:

```json
{
  "query": "something nice for my wife",
  "answer": "Happy to help you find a gift! Is she more into jewelry, cozy loungewear, or something for her morning coffee ritual?",
  "results": [],
  "total_results": 0,
  "thread_id": "c4d17a02-59f3-42c8-9d11-8a2e6f0b3c77",
  "processing_time_ms": 850,
  "search_id": "2b9d4e61-7c3a-4f5e-8d20-1a6b9c8e4f03"
}
```

#### Request with All Parameters

```shell
curl https://api.alhena.ai/api/v1/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ALHENA_API_KEY" \
  -d '{
    "query": "waterproof hiking boots for wide feet",
    "thread_id": "bb708939-b626-48eb-a403-2ea25bb38941",
    "max_results": 10,
    "locale": "en",
    "page_url": "https://store.example.com/collections/hiking",
    "user_metadata": { "customer_tier": "gold", "store_location": "US" }
  }'
```

***

## Query Patterns

The same engine that powers Alhena's search widget handles a wide range of query shapes:

| Query                                             | What the AI does                                                          |
| ------------------------------------------------- | ------------------------------------------------------------------------- |
| `"white cotton t-shirt under $50"`                | Extracts category, material, color, and price cap, then matches variants  |
| `"waterproof jacket for spring hiking"`           | Maps the use case to product attributes like waterproof rating and weight |
| `"a gift for a runner under $100"`                | Interprets audience and budget, searching across categories               |
| `"show me the same thing in black"` + `thread_id` | Refines the previous results, swapping the color constraint               |
| `"something cheaper"` + `thread_id`               | Lowers the price constraint on the previous search                        |
| `"more like this"` + `similar_product_id`         | Finds visually and functionally similar products to the one referenced    |

Queries can be full sentences, fragments, or questions, in any language your catalog and `locale` support.

## Match Types and Ranking

Results are ordered in two groups:

1. **`MATCH` results first.** Every constraint the AI detected in the query is satisfied. Within the group, results with more matched attributes rank higher.
2. **`PARTIAL_MATCH` results after.** Close alternatives that satisfy some constraints, ranked the same way.

The `matched_attributes` object on each result tells you exactly which constraints it satisfied, so you can explain the match to the shopper ("Matches: white, cotton") or badge partial matches differently. The Alhena search widget shows `MATCH` results as the primary grid and `PARTIAL_MATCH` results as "you might also like" suggestions; we recommend the same split.

If nothing in the catalog satisfies every constraint, the response contains only `PARTIAL_MATCH` results and the `answer` explains the trade-off, e.g. *"I couldn't find a fully waterproof option under $50, but these water-resistant jackets come close."*

## Rendering Results

Each result carries everything needed for a product card. A minimal renderer:

```javascript
function renderProductCard(result) {
  const onSale = result.original_price > result.current_price;

  return `
    <a href="${result.link}" class="product-card">
      <img src="${result.images[0]}" alt="${result.title}" loading="lazy" />
      <h3>${result.title}</h3>
      <p class="price">
        ${onSale ? `<s>${result.original_price}</s> ` : ""}${result.current_price} ${result.currency}
      </p>
      ${result.rating ? `<p class="rating">${result.rating} (${result.rating_count})</p>` : ""}
      ${result.in_stock ? "" : `<p class="stock">Out of stock</p>`}
    </a>
  `;
}
```

For Shopify stores, `external_variant_id` lets you build a direct add-to-cart URL: `https://yourstore.com/cart/add?id=<external_variant_id>`.

***

## More Examples

### Refine the Search

Pass the `thread_id` from the previous response and the query is understood in context:

```shell
curl https://api.alhena.ai/api/v1/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ALHENA_API_KEY" \
  -d '{
    "query": "show me the same thing in black",
    "thread_id": "bb708939-b626-48eb-a403-2ea25bb38941"
  }'
```

### Find Similar Products

Power a "more like this" button by passing a result's `product_id`:

```shell
curl https://api.alhena.ai/api/v1/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ALHENA_API_KEY" \
  -d '{
    "query": "similar products to the Classic Cotton Tee",
    "similar_product_id": "a3f5c1b2e4d67890a3f5c1b2e4d67890"
  }'
```

### Localized Search

Set `locale` and the answer comes back in that language. Product links use locale-specific URLs when your catalog provides them:

```shell
curl https://api.alhena.ai/api/v1/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ALHENA_API_KEY" \
  -d '{
    "query": "t-shirt en coton blanc à moins de 50 $",
    "locale": "fr"
  }'
```

```json
{
  "query": "t-shirt en coton blanc à moins de 50 $",
  "answer": "J'ai trouvé quelques t-shirts en coton blanc à moins de 50 $. Le Classic Cotton Tee correspond le mieux à votre recherche.",
  "results": ["..."],
  "total_results": 3,
  "thread_id": "e8a25c40-1b7d-4f6e-9c33-5d0f8a2b6e91",
  "processing_time_ms": 1310,
  "search_id": "9c1f7d24-3e8b-4a60-b5d2-7f4e0a9c8b15"
}
```

***

## Errors

| Status | Body                                                            | Cause                                                              |
| ------ | --------------------------------------------------------------- | ------------------------------------------------------------------ |
| 400    | `{"error": "Missing or invalid query."}`                        | `query` missing or not a string                                    |
| 400    | `{"error": "No matching thread for provided thread_id."}`       | `thread_id` doesn't belong to an existing session for this account |
| 400    | `{"error": "No AI credits left"}`                               | Account is out of AI credits                                       |
| 401    | (no body)                                                       | Missing or invalid API key                                         |
| 403    | `{"status": "error", "message": "Missing scope: search:query"}` | API key doesn't include the AI Search scope                        |
| 429    | (no body)                                                       | Rate limit exceeded. Retry with backoff                            |
| 500    | `{"error": "Internal server error."}`                           | Unexpected server error. Safe to retry once                        |

### Retries and Timeouts

* Set your client timeout to at least **10 seconds**. Most searches complete in 1 to 3 seconds, but complex queries over large catalogs can take longer.
* Retry `429` responses with exponential backoff. The rate-limit window is one minute, so a first retry after a few seconds usually succeeds.
* Retry `500` responses once. Do not retry `400` responses; fix the request instead.
* Searches are independent requests. Retrying a failed search is always safe and has no side effects on the session.

## Rate Limits

This endpoint is rate-limited to **100 requests per minute** per API key. Contact your Alhena account team if you need a higher limit.

## Usage Notes

* **Search on submit, not on keystroke.** AI Search runs a full retrieval and reasoning pass per request (typically 1 to 3 seconds), so trigger it when the shopper submits a query. It's not intended as a type-ahead autocomplete backend.
* **Reuse the session for refinements only.** Keep passing the same `thread_id` while the shopper narrows one search. When they start a genuinely new search, omit it. A fresh session gives cleaner results.
* **Handle the empty-results case.** When `results` is empty, the `answer` is a clarifying question. Render it and keep the input focused so the shopper can reply in the same session.
* **Prices and stock are live.** `current_price` and `in_stock` reflect your store at query time, so avoid caching results beyond a few minutes.

## Related Pages

* [REST API](/docs/developer-reference/api.md): Send Message and Product FAQs endpoints
* [Managing API Keys](/docs/developer-reference/api/managing-api-keys.md): Create, scope, and revoke API keys from the dashboard
* [AI Search](/docs/features/ai-search.md): The hosted AI Search page for your website
* [Custom Data](/docs/developer-reference/website-sdk/custom-data.md): How to use `user_metadata` to pass user context
