> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voyantcloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Search products

> GET /v1/products/search - Search across your product catalog with full-text search and faceting

## Endpoint

`GET https://api.voyantcloud.com/v1/products/search`

Search for products across your catalog with full-text search and faceting. Results include both workspace-owned products and marketplace products from providers you have access to, with automatic workspace isolation and provider filtering.

## Authentication

<ParamField header="Authorization" type="string" required>
  Bearer token (e.g. `Authorization: Bearer YOUR_API_KEY`)
</ParamField>

## Query parameters

<ParamField query="q" type="string">
  Search query string. If omitted, returns all products matching filters.
</ParamField>

<ParamField query="locale" type="string" default="en">
  Locale for search. Options: `en`, `ro`
</ParamField>

<ParamField query="page" type="integer" default="0">
  Zero-based page number (for page-based pagination)
</ParamField>

<ParamField query="pageSize" type="integer" default="20">
  Number of results per page (1-100)
</ParamField>

<ParamField query="offset" type="integer">
  Starting offset (for offset-based pagination). Takes precedence over `page` if provided.
</ParamField>

<ParamField query="length" type="integer">
  Number of results (for offset-based pagination). Defaults to `pageSize` if not provided.
</ParamField>

<ParamField query="type" type="string">
  Filter by product type (e.g., `tour`, `cruise`, `accommodation`)
</ParamField>

<ParamField query="status" type="string">
  Filter by status (e.g., `active`, `draft`, `archived`)
</ParamField>

<ParamField query="organizationId" type="string">
  B2B: Filter by organization visibility (UUID format)
</ParamField>

<ParamField query="sortBy" type="string">
  Field to sort by. Available options:

  * `price` or `priceFrom` - Sort by starting price
  * `created` - Sort by creation date
  * `updated` - Sort by last update date (default)
  * `departure` - Sort by earliest departure date
  * `relevance` - Sort by text search relevance score
</ParamField>

<ParamField query="sortOrder" type="string" default="desc">
  Sort order. Options: `asc`, `desc`.
</ParamField>

<ParamField query="filters" type="string">
  Advanced filtering using Typesense filter syntax. This allows filtering on any indexed field.

  **Syntax:**

  * Array contains: `tags:=[Black Friday]`
  * Exact match: `status:=active`
  * Multiple values (OR): `tags:=[Black Friday, Featured]`
  * Numeric comparison: `priceFromValue:>100`
  * AND conditions: `tags:=[Black Friday] && type:=tour`
  * OR conditions: `type:=tour || type:=cruise`

  <Note>
    Remember to URL-encode the `filters` parameter value. For example, `tags:=[Black Friday]` becomes `tags%3A%3D%5BBlack%20Friday%5D`.
  </Note>
</ParamField>

## Request example

<CodeGroup>
  ```bash cURL theme={null}
  # Basic search
  curl "https://api.voyantcloud.com/v1/products/search?q=paris&locale=en" \
    -H "Authorization: Bearer YOUR_API_KEY"

  # Filter by tag (URL-encoded: tags:=[Black Friday])
  curl "https://api.voyantcloud.com/v1/products/search?filters=tags%3A%3D%5BBlack%20Friday%5D" \
    -H "Authorization: Bearer YOUR_API_KEY"

  # Sort by price ascending (cheapest first)
  curl "https://api.voyantcloud.com/v1/products/search?q=tour&sortBy=price&sortOrder=asc" \
    -H "Authorization: Bearer YOUR_API_KEY"

  # Combined: filter by tag + type, sort by departure date
  curl "https://api.voyantcloud.com/v1/products/search?filters=tags%3A%3D%5BBlack%20Friday%5D%20%26%26%20type%3A%3Dtour&sortBy=departure&sortOrder=asc" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    "https://api.voyantcloud.com/v1/products/search?q=paris&locale=en",
    {
      headers: {
        Authorization: `Bearer ${process.env.VOYANT_API_KEY}`,
      },
    }
  )

  const results = await response.json()
  console.log(results.items)
  ```

  ```python Python theme={null}
  import requests
  import os

  response = requests.get(
    'https://api.voyantcloud.com/v1/products/search',
    params={'q': 'paris', 'locale': 'en'},
    headers={'Authorization': f"Bearer {os.environ['VOYANT_API_KEY']}"}
  )

  results = response.json()
  print(results['items'])
  ```

  ```php PHP theme={null}
  <?php
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, 'https://api.voyantcloud.com/v1/products/search?q=paris&locale=en');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . getenv('VOYANT_API_KEY')
  ]);

  $response = curl_exec($ch);
  $results = json_decode($response, true);
  print_r($results['items']);
  ?>
  ```
</CodeGroup>

## Response

<ResponseField name="items" type="array" required>
  Array of product search results

  <Expandable title="Item properties">
    <ResponseField name="id" type="string">
      Product ID (objectID from search index)
    </ResponseField>

    <ResponseField name="title" type="string">
      Product title
    </ResponseField>

    <ResponseField name="source" type="string">
      Source type: `main` (workspace-owned), `marketplace` (provider product), or `override` (customized marketplace product)
    </ResponseField>

    <ResponseField name="locale" type="string">
      Locale of the result
    </ResponseField>

    <ResponseField name="snippet" type="string">
      Highlighted snippet of matching text with `<em>` tags
    </ResponseField>

    <ResponseField name="providerId" type="string">
      Provider ID (if `source: "marketplace"` or `source: "override"`)
    </ResponseField>

    <ResponseField name="workspaceId" type="string">
      Workspace ID (if `source: "main"`)
    </ResponseField>

    <ResponseField name="type" type="string">
      Product type
    </ResponseField>

    <ResponseField name="score" type="number">
      Search relevance score
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="page" type="integer">
  Current page number (page-based pagination only)
</ResponseField>

<ResponseField name="nbPages" type="integer">
  Total number of pages (page-based pagination only)
</ResponseField>

<ResponseField name="hitsPerPage" type="integer">
  Number of results per page (page-based pagination only)
</ResponseField>

<ResponseField name="offset" type="integer">
  Current offset (offset-based pagination only)
</ResponseField>

<ResponseField name="length" type="integer">
  Number of results returned (offset-based pagination only)
</ResponseField>

<ResponseField name="total" type="integer" required>
  Total number of matching results
</ResponseField>

<ResponseField name="tookMs" type="integer" required>
  Query execution time in milliseconds
</ResponseField>

<ResponseField name="facets" type="object">
  Facet buckets and counts for the current query (all facet attributes returned).
</ResponseField>

<ResponseField name="exhaustiveFacetsCount" type="boolean">
  Whether facet counts are exact (`true`) or approximate (`false`).
</ResponseField>

<ResponseExample>
  ```json 200 Success theme={null}
  {
    "items": [
      {
        "id": "main:550e8400-e29b-41d4-a716-446655440000:en",
        "title": "Paris City Tour",
        "source": "main",
        "locale": "en",
        "snippet": "Discover the beauty of <em>Paris</em> with our guided city tour",
        "workspaceId": "wksp_01h8z3y4x2w1v0u9t8s7r6q5p4",
        "type": "tour",
        "score": 1234,
        "priceFrom": {
          "value": 89.99,
          "currency": "EUR"
        },
        "thumbnailUrl": "https://cdn.voyantcloud.com/products/paris-tour.jpg"
      },
      {
        "id": "marketplace:provider_prod_123:en",
        "title": "Louvre Museum Skip-the-Line",
        "source": "marketplace",
        "locale": "en",
        "snippet": "Skip the lines at the <em>Paris</em> Louvre Museum",
        "providerId": "provider_abc",
        "type": "experience",
        "score": 1123
      }
    ],
    "page": 0,
    "nbPages": 5,
    "hitsPerPage": 20,
    "total": 87,
    "tookMs": 42,
    "facets": {
      "type": {
        "tour": 56,
        "experience": 22
      },
      "status": {
        "active": 70,
        "draft": 17
      }
    },
    "exhaustiveFacetsCount": true
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "error": "Invalid request parameters",
    "details": [
      {
        "path": ["pageSize"],
        "message": "Number must be less than or equal to 100"
      }
    ]
  }
  ```

  ```json 401 Unauthorized theme={null}
  {
    "error": "Unauthorized"
  }
  ```

  ```json 503 Service Unavailable theme={null}
  {
    "error": "Search service unavailable"
  }
  ```
</ResponseExample>

## Error handling

<AccordionGroup>
  <Accordion title="400 Bad Request">
    Invalid query parameters.

    **Possible causes:**

    * `pageSize` exceeds 100
    * Invalid `locale` value
    * Invalid `organizationId` format

    **Solution:** Check your query parameters match the documented formats and constraints.
  </Accordion>

  <Accordion title="401 Unauthorized">
    Missing or invalid API key.

    **Solution:** Check your `Authorization: Bearer` header is set correctly.
  </Accordion>

  <Accordion title="503 Service Unavailable">
    Search service is temporarily unavailable.

    **Solution:** Retry the request after a short delay. If the issue persists, contact support.
  </Accordion>
</AccordionGroup>

## Features

### Workspace isolation

All search results are automatically filtered to include only:

* Products owned by your workspace (`source: "main"`)
* Marketplace products from providers you have active connections or grants for

### Override precedence

If your workspace has created overrides for a marketplace product, the override version is returned instead of the base marketplace product, automatically de-duplicated.

### Relevance ranking

Results are ranked by a relevance algorithm, which considers:

* Text matching (query terms in title, description)
* Popularity signals
* Custom ranking factors

### Facet Filtering

The search response includes a `facets` object, which contains all filterable attributes for the current result set. You can use these facets to build a filtered search UI.

While you can use the top-level `type` and `status` parameters for basic filtering, the `filters` parameter provides advanced capabilities using Typesense filter syntax.

**Available facets:**

* `type` - Product type (tour, cruise, accommodation, etc.)
* `status` - Product status (active, draft, archived)
* `categories` - Category assignments
* `tags` - Product tags
* `countries` - Countries the product visits
* `regions` - Geographic regions
* `priceFromCurrency` - Price currency
* `source` - Product source (main, marketplace, override)

**Example: Filtering by multiple facets**

1. Perform an initial search to get available facets.
2. Use the `filters` parameter in a subsequent request to refine the results.

```javascript theme={null}
async function searchWithFacets() {
  // Filter for tours tagged as "outdoor" using Typesense syntax
  const filters = 'type:=tour && tags:=[outdoor]';

  const params = new URLSearchParams({
    q: 'adventure',
    filters: filters,
    pageSize: 20,
    locale: 'en',
  });

  const response = await fetch(
    `https://api.voyantcloud.com/v1/products/search?${params}`,
    {
      headers: { Authorization: `Bearer ${process.env.VOYANT_API_KEY}` },
    }
  );

  return await response.json();
}

// More filter examples:
// By country: filters=countries:=[ro]
// By price range: filters=priceFromValue:>=100 && priceFromValue:<=500
// Multiple tags (OR): filters=tags:=[Black Friday, Featured]
// Multiple tags (AND): filters=tags:=[Black Friday] && tags:=[Featured]
```

This allows for dynamic and precise filtering based on any attribute indexed in the product catalog.

## Use cases

### Product search autocomplete

Build a search-as-you-type experience:

```javascript theme={null}
let searchTimeout

async function searchProducts(query) {
  clearTimeout(searchTimeout)

  searchTimeout = setTimeout(async () => {
    const response = await fetch(
      `https://api.voyantcloud.com/v1/products/search?q=${encodeURIComponent(query)}&pageSize=10`,
      {
        headers: { Authorization: `Bearer ${process.env.VOYANT_API_KEY}` },
      }
    )

    const results = await response.json()
    displayResults(results.items)
  }, 300) // Debounce 300ms
}
```

### Filtered product search

Search with type and status filters:

```javascript theme={null}
async function searchTours(query, status = 'active') {
  const params = new URLSearchParams({
    q: query,
    type: 'tour',
    status,
    pageSize: 20,
    locale: 'en',
  })

  const response = await fetch(
    `https://api.voyantcloud.com/v1/products/search?${params}`,
    {
      headers: { Authorization: `Bearer ${process.env.VOYANT_API_KEY}` },
    }
  )

  return await response.json()
}
```

## Related endpoints

<CardGroup cols={2}>
  <Card title="List products" icon="list" href="/api-reference/products/list-products">
    Browse products with database filtering
  </Card>

  <Card title="Search facets" icon="filter" href="/api-reference/products/search-facets">
    Search for facet values
  </Card>

  <Card title="Get product" icon="book" href="/api-reference/products/get-product">
    Retrieve detailed product information
  </Card>
</CardGroup>
