# The client

Call the Query API from any storefront with typed requests and answers, and read and write a listing's URL.

`@orbsearch/client` calls the [Query API](/docs/reference/glossary#query-api) with typed requests and answers. It needs no framework, and the [React components](/docs/react) build on it:

```ts twoslash title="app/search.ts"
import { createSearch } from "@orbsearch/client";

export const search = createSearch({ endpoint: "http://127.0.0.1:7800" });

const answer = await search.query("graues Sofa");
const sofa = await search.product({ id: "SOFA-ASKA-2" });
const page = await search.resolve("/wohnen/sofas?color=grey");
```

The calls search for "graues Sofa" (grey sofa), fetch one sofa and ask what the sofa page `/wohnen/sofas` (wohnen: living) with grey chosen is. Hover a call to read what it answers.

The packages are not on npm yet. A shop inside this repository's pnpm workspace depends on them as `workspace:*`, as the demo storefront in `apps/demo` does, and its bundler compiles their TypeScript source:

```json title="package.json (trimmed)"
{
    "dependencies": {
        "@orbsearch/client": "workspace:*",
        "@orbsearch/react": "workspace:*"
    }
}
```

## The calls

Each call is one request. The second argument is the request without its text, with `filters` in the shape a listing's URL holds, and the third cancels the call when the shopper moves on:

```ts twoslash title="app/search.ts"
import { createSearch } from "@orbsearch/client";
const search = createSearch({ endpoint: "http://127.0.0.1:7800" });
// ---cut---
const navigation = new AbortController();
const sofas = await search.query(
    "",
    { on: "category_page", category: "wohnen/sofas", filters: { color: ["grey"] }, sort: "price_asc" },
    { signal: navigation.signal },
);
```

- `query` searches or browses a category ([Search and browse](/docs/search)).
- `product` fetches one product by `id` or `url`, or `null` when the channel sells none ([Product pages](/docs/product-pages)).
- `resolve` says what one of the merchant's URLs is: a listing with its results, a product, a redirect or nothing ([URLs and redirects](/docs/urls)).
- `facets` counts named [facets](/docs/reference/glossary#facet) in full, without products, for a facet that [loads when opened](/docs/react#facets-that-load-when-opened).
- `suggestions` returns the store a search box runs on ([Autocomplete](/docs/autocomplete)).

In the browser, `registerAgentTools` offers the same client to agents as WebMCP tools ([Agents](/docs/agents)).

**Reference:** [`POST /v1/search`](/docs/reference/query-api#search) · [`GET /v1/product`](/docs/reference/query-api#product) · [`GET /v1/resolve`](/docs/reference/query-api#resolve)

## One client per page load

A client's searches name one page view, which Insights counts queries by. Make one client per page the shopper loads: once in the browser, and once per request on a server, with the shopper's `User-Agent`:

```ts twoslash title="app/search.server.ts"
import { createSearch } from "@orbsearch/client";

export function searchFor(request: Request) {
    const userAgent = request.headers.get("user-agent");

    return createSearch({ endpoint: "http://127.0.0.1:7800", ...(userAgent && { userAgent }) });
}
```

The user agent tells a crawler from a shopper, which the server's own would hide. The page view lives in memory only: pass the server's to the browser's client as `pageView`, and a page load counts once. `traffic: "test"` or `"bench"` marks searches the merchant's reports leave out.

<Callout type="warn">
    The management key never belongs in a storefront. The Query API needs no key and allows every origin, so the browser
    calls it directly ([Conventions](/docs/reference/conventions#query-api)).
</Callout>

**Reference:** [`page_view`](/docs/reference/query-api#search.page_view) · [`traffic`](/docs/reference/query-api#search.traffic)

## Errors

A call the Query API refuses throws a `SearchError`, whose `problem` says why in words a person can read:

```ts twoslash title="app/search.ts"
import { createSearch, SearchError } from "@orbsearch/client";

const search = createSearch({ endpoint: "http://127.0.0.1:7800" });

try {
    await search.query("sofa", { sort: "cheapest" });
} catch (error) {
    if (!(error instanceof SearchError)) throw error;
    console.error(error.problem.status, error.problem.detail);
}
```

- **400** means the request cannot be answered as sent, such as the unknown sort above, and `problem.violations` names each wrong field.
- **503** means nothing is searchable yet or the engine is unreachable. Offer the shopper the same page again.

**Reference:** [Problems](/docs/reference/problems)

## Types from the contract

Every request, answer, release file and catalog entity is a type generated from the contract, such as `SearchResponse`, `FacetResult` or `PageResponse`. Hover one to read its fields:

```ts twoslash title="app/hits.ts"
import type { SearchResponse } from "@orbsearch/client";

export function entitiesOf(answer: SearchResponse): string[] {
    return (answer.sections ?? []).flatMap((section) => section.hits.map((hit) => hit.entity));
}
```

The contract's values come as constants, such as `SEARCH_PATH`, `RELEVANCE_SORT` and `LATENCY_BUDGETS`. Mind one name: `Product` from the client is a product page's product, `Product` from `@orbsearch/react` a tile's.

**Reference:** [Query API](/docs/reference/query-api)

## A listing's URL

A search, category or brand page keeps its whole state in its URL, so a link, the back button and a crawler see what the shopper sees. `listingHref` writes it:

```ts twoslash title="app/links.ts"
import { listingHref } from "@orbsearch/client";

listingHref("/suche", {
    query: "sofa",
    filters: { color: ["grey", "green"], price: { gte: 100, lte: 300 }, on_sale: true },
    sort: "price_asc",
    page: 2,
});
// "/suche?q=sofa&color=green&color=grey&on_sale=1&price.gte=100&price.lte=300&sort=price_asc&page=2"
```

Fields stand in alphabetical order and values sorted, so one choice has one URL. Leave `page` out after a change of filters or sort, and the page starts at 1. Three helpers read a listing back:

- `listingOf(page)` reads it from `resolve`'s answer, since the merchant may rename parameters.
- `keptParams(listing)` holds what a form that changes filters or sort keeps as hidden fields, such as `q`.
- `filtersOf(url, facets)` reads filters for facets you hold yourself. It needs the facets, since `seats=1` is a value where `on_sale=1` is a toggle.

The helpers write each parameter by its code, as a [release](/docs/reference/glossary#release) without `parameters` in `routes.json` does; [URLs and redirects](/docs/urls) says what renamed parameters change.

**Reference:** [`routes.json`](/docs/reference/release-files/routes)

## Next

- [React components](/docs/react): the search box and the pages built on this client.
- [Your storefront](/docs/your-storefront): a whole shop on the client in a few minutes.
- [Clicks and views](/docs/clicks-and-views): report what shoppers open and see.
