# Search

> Hybrid keyword and semantic search across apps, skills, knowledge, agents, flows, MCP servers, and published pages.

**URL:** https://inference.sh/docs/api/rest/search
**Last updated:** 2026-10-11

---

Hybrid keyword and semantic search across apps, skills, knowledge, agents, flows, MCP servers, and published pages.

→ [Search concept](/docs/concepts/search): collections, suggest vs search, and discovery in the web app

---

## Suggest

`GET /suggest` or `POST /suggest`

Unified search for UI autocomplete and discovery. Returns actionable results ranked globally by relevance score across apps, skills, knowledge, agents, MCP servers, and published public pages.

Authentication is optional. Without a token, results are limited to public resources. With a token, your workspace's content is included according to each entry's visibility. See [Access control](#access-control).

### Query Parameters (GET)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `q` | string | Yes | Search query |

`GET` does not accept `limit`, `category`, `context`, or `scope` — use `POST` for those fields.

### Request Body (POST)

`GET` only reads `q`. Use `POST` for `limit`, `category`, `context`, and `scope`.

```json
{
  "query": "flux",
  "context": "We were comparing image models for logos.",
  "scope": ["git:github.com/inference-sh/cli", "lang:go"],
  "limit": 10,
  "category": "app"
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `query` | string | — | Search query (required). Used for keyword matching |
| `context` | string | empty | Optional conversation background (for example the assistant's previous reply from belt hooks). Keyword search uses `query` only. When present, semantic ranking embeds `query` + `\n\n` + `context` so the prompt leads and longer context does not dominate the vector |
| `scope` | string[] | empty | Environment signals for project-scoped ranking — for example `git:github.com/user/repo`, `lang:go`. Appended after `query` and optional `context` in the embedding text; does not affect keyword matching. Knowledge entries with matching `version.scope` tags rank higher; entries without scope are global. `belt suggest` auto-collects scope from the working directory. See [Knowledge API — project scope](/docs/api/rest/knowledge#project-scope) |
| `limit` | integer | `3` (all categories) or `10` (single category) | Per-category cap after global ranking and dedup. When `category` is empty, each suggest collection (apps, skills, knowledge, agents, MCP servers, pages) may contribute up to `limit` hits; the response is still one globally ranked list |
| `category` | string | empty = all | Filter to `app`, `skill`, `knowledge`, `agent`, `mcp`, or `doc` (pages). Invalid values return `400` |
| `agent` | boolean | `false` | **Deprecated**: accepted and ignored. Result descriptions are always clipped to [200 characters](/docs/api/rest/overview#description-length-limits) |

When neither `context` nor `scope` is set, semantic ranking embeds `query` alone. When only `scope` is set, it embeds `query` + `\n\n` + the scope tags, so scoped calls still match what you asked for.

### Response

Response:

```json
{
  "query": "flux",
  "results": [
    {
      "type": "app",
      "name": "pruna/flux-dev",
      "description": "Advanced text-to-image generation...",
      "command": "belt app run pruna/flux-dev",
      "score": 0.92
    },
    {
      "type": "skill",
      "name": "myteam/code-review",
      "description": "Guidelines for reviewing pull requests",
      "command": "belt skill use myteam/code-review",
      "score": 0.85
    },
    {
      "type": "knowledge",
      "tag": "reference",
      "name": "myteam/stripe-webhooks",
      "description": "Stripe webhook handling patterns",
      "command": "belt knowledge get myteam/stripe-webhooks",
      "score": 0.81
    }
  ],
  "impression_id": "01JABC1234567890ABCDEFGHIJK"
}
```

| Field | Description |
|-------|-------------|
| `impression_id` | When results are non-empty, the id of the stored suggest impression. Pass it as `ref` to [`POST /suggest/convert`](#suggest-convert-attribution) when a listed resource is loaded (the belt CLI does this via `--ref` on run/use/get commands). Omitted when there are no results. |
| `type` | Result category: `app`, `skill`, `knowledge`, `agent`, `mcp`, or `doc` (pages) |
| `tag` | Optional sub-type when the indexed entity type differs from `type` — for example `reference`, `observation`, or `blog` on a knowledge or page hit. Omitted when redundant with `type`. |
| `name` | Display name or `namespace/name` for entities |
| `description` | Short summary, clipped to 200 characters (whitespace collapsed, overflow ends with `...`) |
| `command` | Suggested CLI command or doc URL |
| `score` | Relevance score (0–1) |

Results below the relevance floor (default **0.8**) are omitted from `/suggest` responses. Forks with identical content collapse to the highest-scoring hit via content-hash dedup; when the same base name appears under multiple namespaces with different content, only the highest-scoring variant is returned. When searching all collections, each type is hard-capped at `limit` results so a single collection cannot dominate the list. Lower-scoring hits may still appear in `GET /search` when you need broader recall.

See [REST overview — response format](/docs/api/rest/overview#response-format).

### Errors

| Status | Code | Description |
|--------|------|-------------|
| 400 | `invalid_request` | Missing `query`, invalid JSON body, or unknown `category` |
| 502 | `search_error` | Search backend failure (Meilisearch or embedding service unavailable) |
| 503 | `search_unavailable` | Search is not configured on this deployment |

### Example

```bash
curl "https://api.inference.sh/suggest?q=flux"
```

```bash
curl -X POST https://api.inference.sh/suggest \
  -H "Content-Type: application/json" \
  -d '{"query": "flux", "limit": 5}'
```

---

## Suggest convert (attribution)

`POST /suggest/convert`

Records that a resource from a prior [`/suggest`](#suggest) impression was loaded — for example after `belt suggest` surfaces a skill and you run `belt skill use` with the `--ref` the CLI captured from `impression_id`. The belt CLI sends this asynchronously; callers should not block user workflows on the response.

Authentication is optional. A conversion row is stored only when `ref` matches an impression recorded for the caller's workspace. Unknown refs and refs from other workspaces still return success and record nothing.

### Request body

```json
{
  "ref": "01JABC1234567890ABCDEFGHIJK",
  "resource_name": "myteam/code-review",
  "resource_type": "skill"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `ref` | string | **Required.** Full impression id from `impression_id` (exact match; not a prefix). |
| `resource_name` | string | **Required.** Loaded resource address (for example `namespace/name` or MCP server id). |
| `resource_type` | string | Optional category hint (`app`, `skill`, `knowledge`, `agent`, `mcp`, …). |

### Response

| Status | Description |
|--------|-------------|
| `200` | Success with the standard envelope and empty `data` (`{"data":{},"messages":[]}`). The belt CLI treats only `200` as success. Storage failures are logged server-side but still return `200`. |
| `400` | `invalid_request` — malformed JSON or missing `ref` / `resource_name` |

### Example

```bash
curl -X POST https://api.inference.sh/suggest/convert \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "ref": "01JABC1234567890ABCDEFGHIJK",
    "resource_name": "myteam/code-review",
    "resource_type": "skill"
  }'
```

---

## Session end (belt plugin telemetry)

`POST /session/end`

Records **aggregate telemetry** when a belt plugin agent session ends. The belt CLI calls this from [`belt plugin session-end`](/docs/extend/coding-agents#claude-code-plugin) on the **SessionEnd** hook. This is unrelated to [app execution sessions](/docs/api/rest/tasks) (`GET /sessions` under the apps API).

The route accepts optional authentication. A row is stored only when the caller presents a valid Bearer token (API key or session) for a signed-in user with an active workspace. Anonymous requests still return **200** and record nothing.

### Request body

JSON body, maximum **64 KiB**.

```json
{
  "session_id": "sess_abc123",
  "agent": "claude-code",
  "messages": 12,
  "user_turns": 4,
  "files_edited": 2,
  "tools": ["Bash", "Read", "mcp__belt__search"]
}
```

| Field | Type | Description |
|-------|------|-------------|
| `session_id` | string | Harness session identifier. Required to persist a row (max 128 bytes). |
| `agent` | string | Agent runtime name when known (for example `claude-code`, `codex`). Max 64 bytes. |
| `messages` | integer | Conversation messages counted from the transcript (human prompts plus assistant replies with text), not every JSONL line. Negative values are stored as `0`. |
| `user_turns` | integer | User turns in the session. Must be **greater than zero** to persist; otherwise the request is ignored. |
| `files_edited` | integer | Number of files touched. Negative values are stored as `0`. |
| `tools` | string[] | Tool names used (belt and MCP tools). Empty names are dropped. At most **64** tools; each name is capped at **128** bytes. |

Older belt plugins may still send `cwd`, `summary`, or `tags`. Those fields are accepted and **ignored** — only the counts and `tools` above are stored. No file paths, prompts, replies, or summary text are saved.

When `belt plugin session-end` runs on **SessionEnd**, it reads the agent transcript JSONL locally and derives these numbers before calling the API. **Claude Code** transcripts store the API payload under `message`; file edits appear in `file-history-snapshot` entries (`snapshot.trackedFileBackups`). Counts include human prompts and assistant replies with text, plus tool names from assistant turns — not tool results, injected reminders, attachments, mode changes, sub-agent turns, or other non-conversation lines. **Codex** rollouts use `event_msg` / `response_item` payload types with the same goal (user messages and assistant/tool activity, not noise lines). If there are zero user turns, the CLI skips the request.

One row is kept per **(workspace, session_id)**. If the same session ends again after a resume, the existing row is updated.

### Response

| Status | Description |
|--------|-------------|
| `200` | Success with the standard envelope and empty `data` (`{"data":{},"messages":[]}`). The belt CLI treats only `200` as success. Storage failures are logged server-side but still return `200` so session-close hooks are not blocked. |
| `400` | `invalid_request` — malformed JSON body |

### Example

```bash
curl -X POST https://api.inference.sh/session/end \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "sess_abc123",
    "agent": "claude-code",
    "messages": 12,
    "user_turns": 4,
    "files_edited": 2,
    "tools": ["Bash", "Read"]
  }'
```

---

## Search

`GET /search` or `POST /search`

Direct search with collection and filter control. Returns scored hits with entity metadata.

### Query Parameters (GET)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `q` | string | Yes | Search query |
| `collection` | string | No | One collection: `apps`, `skills`, `knowledge`, `agents`, `flows`, `mcp_servers`, or `pages`. Omit to search all collections. |

`GET` reads only `q` and `collection`, and returns up to 20 results per collection. Use `POST` to search several collections or set `limit`.

### Request Body (POST)

```json
{
  "query": "payment processing",
  "collections": ["knowledge", "skills"],
  "limit": 20
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `query` | string | Yes | Search query |
| `collections` | string[] | No | Collections to search. Omit to search all collections. |
| `limit` | integer | No | Max results per collection. Default `20`. |

### Response

Response:

```json
{
  "query": "payment processing",
  "results": [
    {
      "collection": "knowledge",
      "id": "know_abc123",
      "score": 0.87,
      "fields": {
        "name": "stripe-webhooks",
        "namespace": "myteam",
        "description": "Stripe webhook handling patterns",
        "type": "reference",
        "parent_id": "know_abc123",
        "updated_at": 1770732019
      }
    }
  ]
}
```

See [REST overview — response format](/docs/api/rest/overview#response-format).

| Field | Description |
|-------|-------------|
| `collection` | Source collection (`apps`, `skills`, `knowledge`, `agents`, `flows`, `mcp_servers`, `pages`) |
| `id` | Entity ID |
| `score` | Relevance score (0–1) |
| `fields` | Collection-specific metadata (name, namespace, description, tags, etc.) |

### Errors

| Status | Code | Description |
|--------|------|-------------|
| 400 | `invalid_request` | Missing `query` or invalid JSON body |
| 500 | `search_error` | Search backend failure |
| 503 | `search_unavailable` | Search is not configured on this deployment |

### Example

```bash
curl "https://api.inference.sh/search?q=flux&collection=apps"
```

```bash
curl -X POST https://api.inference.sh/search \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "payment processing",
    "collections": ["knowledge"],
    "limit": 20
  }'
```

Use an API key to search your workspace's content. Public resources are searchable without authentication.

---

## Access control

Search applies the same visibility rules as the REST list endpoints. The index filters by your workspace, user, and organization, and Postgres re-checks the final hits. An out-of-date index can hide a result but never exposes private content.

Marketplace **takedowns** use the same re-check: if a public app, skill, agent, flow, or MCP server has a store listing that is **delisted** or **suspended**, it does not appear in `/search` or `/suggest` for other callers (including anonymous users). The owning workspace still finds it in search. Listing status is not stored in the search index, so a takedown takes effect on the next query without reindexing. Store browse endpoints (`GET /store/apps`, `GET /store/skills`) already exclude non-live listings.

| Visibility | Who sees it in search |
|------------|----------------------|
| `public` | Everyone, no auth required |
| `team` | Members of the owning workspace |
| `private` | Only the creator, within the owning workspace |
| `org` | Callers whose workspace belongs to the same organization |
| `unlisted` | Members of the owning workspace. Other callers do not get it from search. |

Search never returns other members' `private` entries to workspace admins. [`include_private`](/docs/api/rest/overview#workspace-admin-visibility) is a listing option and does not apply to search.

Pages are in the `pages` collection only when they are published and `public`. Workspace-only and private pages are not indexed.

---

## Collections

| Collection | Contents |
|------------|----------|
| `apps` | Apps in the grid (retired apps are filtered out of results) |
| `skills` | Skills from the registry and your workspace |
| `knowledge` | Observations, references, preferences (requires auth for non-public entries) |
| `agents` | Agents in your workspace and public agents |
| `flows` | Flow definitions. Returned by `/search`, not by `/suggest`. |
| `mcp_servers` | MCP servers configured for your workspace |
| `pages` | Published public documentation and blog posts |

Search combines keyword matching with semantic similarity. Newly created entities are keyword-searchable immediately; semantic ranking improves after background embedding completes.

If query embedding fails or times out, the API returns **keyword-only** results for that request (no semantic re-ranking).

---

## Related APIs

- [Search concept](/docs/concepts/search) — when to use suggest vs search and what each collection contains
- [Skills](/docs/api/rest/skills) — list, create, and manage skills
- [Knowledge](/docs/api/rest/knowledge) — manage knowledge entries that appear in search
