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

# Minting search tokens

> Exchange an Omneo API token for a short-lived search token scoped to the Discovery indexes your application needs.

Every [Discovery](/concepts/platform-surfaces/omneo-discovery) search is authenticated with a **search token**: a short-lived credential scoped to a specific set of indexes. Mint one from the Omneo API, then use it as the bearer token on search requests.

```bash theme={null}
POST https://api.[tenant].getomneo.com/api/v3/auth/discovery
```

| Header          | Value                           |
| --------------- | ------------------------------- |
| `Authorization` | `Bearer [your Omneo API token]` |
| `Content-Type`  | `application/json`              |
| `Accept`        | `application/json`              |

The Omneo API token used for this call must carry the `read-profiles` scope. See [API tokens](/dev-guides/core-setup/api-tokens) for how to create one.

## Request body

The body is a `rules` object mapping each index you want to search to a rule. An empty rule grants unrestricted search on that index.

```json theme={null}
{
  "rules": {
    "profiles": {},
    "products": {}
  }
}
```

The resulting token can search only the indexes named in `rules`. A search against any other index is rejected, so mint a token that covers exactly what the calling application needs and no more.

## Response

```json theme={null}
{
  "data": {
    "token": "[search token]"
  }
}
```

## Scoping a token to a subset of records

A rule can carry a `filter`, which is applied to every search made with that token. This is how you hand a browser a token that can only see records the signed-in customer is allowed to see.

```json theme={null}
{
  "rules": {
    "profiles": {
      "filter": "id = 12345"
    }
  }
}
```

The filter uses the same syntax as the `filter` search parameter, described in [Running a search](/dev-guides/discovery/searching). It is enforced on the token, so a client cannot widen it by sending its own `filter`. The two combine, narrowing the result set further.

<Tip>
  For customer-facing applications, scope the token to the signed-in profile. For staff-facing applications such as an in-store lookup tool, an unrestricted rule is usually correct, because staff are expected to search the whole customer base.
</Tip>

## Keep minting server-side

<Warning>
  Your Omneo API token is a long-lived credential with broad access. Never ship it to a browser, a mobile app, or any other client you do not control.
</Warning>

Mint search tokens from your backend and return only the search token to the client. A common pattern is a small endpoint on your own server, for example `GET /api/search-token`, that mints a token, caches it, and hands it to the frontend.

## Token lifetime and caching

A search token expires 15 minutes after it is issued. The response body carries no expiry field: the expiry is in the token's `exp` claim, as Unix seconds. Decode the payload segment to read it rather than hard-coding 15 minutes, so your application keeps working if the lifetime changes.

Cache the token and reuse it across searches. Refresh at least 60 seconds before `exp` so a search already in flight does not fail against a token that expires mid-request.

Requests to the Discovery host with a missing, malformed, or expired token are rejected with `401` or `403`. Treat either as a signal to mint a fresh token and retry once.

## Minting and caching in Node

```js theme={null}
// Node 18+ (built-in fetch). Keep OMNEO_API_TOKEN in server environment variables,
// never in client code.
let cached = null;

function decodeTokenPayload(token) {
  const payload = token.split('.')[1];
  return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
}

export async function getSearchToken() {
  const now = Math.floor(Date.now() / 1000);

  // Refresh 60s before expiry so in-flight searches do not race the expiry.
  if (cached && cached.exp - 60 > now) {
    return cached.token;
  }

  const response = await fetch(
    'https://api.[tenant].getomneo.com/api/v3/auth/discovery',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.OMNEO_API_TOKEN}`,
        'Content-Type': 'application/json',
        Accept: 'application/json'
      },
      body: JSON.stringify({ rules: { profiles: {} } })
    }
  );

  if (!response.ok) {
    throw new Error(`Search token mint failed: ${response.status}`);
  }

  const { data: { token } } = await response.json();
  cached = { token, exp: decodeTokenPayload(token).exp };
  return token;
}
```

## Related

* [Running a search](/dev-guides/discovery/searching)
* [Discovery indexes](/dev-guides/discovery/indexes)
* [API tokens](/dev-guides/core-setup/api-tokens)
* [Authentication](/dev-guides/core-setup/authentication)
