---
title: Build a UI on the catalog
description: The /v2 catalog routes are for building UIs that keep the whole catalog in memory, with ETag revalidation and an ids-only search. Agents and API users should use the /v1 routes.
---

The `/v2/catalog` routes are for building UIs: a web page or app that downloads the whole catalog
once, keeps it, and draws lists, filters and search results from its own copy. The looot dashboard
works this way.

Agents and API users should use the /v1 routes: `GET /v1/catalog/search` to find endpoints and
`GET /v1/operations/{endpointId}` to read one endpoint's input and price before a run. The `looot`
CLI and the MCP server use them too. See [GET /v1/catalog/search](/reference/catalog/get-v1-catalog-search)
in the API reference.

## Which one to call

| You want to | Agent or script | UI that keeps the catalog |
| --- | --- | --- |
| Find endpoints for a job | `GET /v1/catalog/search` | `GET /v2/catalog/search` |
| Read one endpoint before running it | `GET /v1/operations/{endpointId}` | `GET /v2/catalog/inputs/{endpointId}` |
| List the catalog | `GET /v1/catalog/endpoints`, one page at a time | `GET /v2/catalog`, all of it |

Why an agent should not use /v2:

- `GET /v2/catalog/search` returns ids and scores only. Without the catalog document in memory, an
  agent needs one more call per row to learn the price and whether it can run. Each
  `GET /v1/catalog/search` row already carries the price, whether you can run it now, and recent
  success rates.
- `GET /v2/catalog` is the whole catalog in one response, about 6 MB before compression, and it
  ignores `limit`.
- `GET /v2/catalog/inputs/{endpointId}` has the input schema but no price and no estimated maximum
  cost. `GET /v1/operations/{endpointId}` has both.

## Sign in

Every /v2 route needs a token in the `Authorization: Bearer` header, like the /v1 routes. There
is no signed-out /v2; signed-out browsing uses `GET /v1/public-catalog`. Any customer token can read
`/v2/catalog`, `/v2/catalog/version`, `/v2/catalog/search` and `/v2/catalog/inputs/{endpointId}`.
`GET /v2/catalog/inputs` (every endpoint's inputs in one call) also needs the
`provider-registry:read` scope. Without it you get 403 [`forbidden`](/errors/rest-errors#forbidden).

## The catalog document

```bash
curl -si "https://api.looot.ai/v2/catalog" \
  -H "Authorization: Bearer $LOOOT_TOKEN" \
  -H "Accept-Encoding: gzip" --compressed
```

The body has `schemaVersion` (2), `revision`, `builtAt`, `endpoints`, `providers`, `jobs`,
`platforms`, `categories` and `counts`. Each endpoint row names its job by `jobId` (an entry of
`jobs`) and its provider by `providerId` (an entry of `providers`), so neither is repeated per row.

The document is the same for every caller. It leaves out everything that depends on your
workspace (`connected`, `readyToRun`, `runnableNow`, `access`, `unavailableReason`) and the live
`stats` and `verified` fields. For those, read the /v1 routes.

## Keep it up to date: ETag and 304

looot rebuilds the document only when the catalog changes. There is no time expiry. Every
response carries an `ETag` and `Cache-Control: private, no-cache`, so keep the body with its ETag
and revalidate before you use it again:

```bash
curl -si "https://api.looot.ai/v2/catalog" \
  -H "Authorization: Bearer $LOOOT_TOKEN" \
  -H 'If-None-Match: "<the ETag you hold>"'
```

- **304** with an empty body: your copy is current.
- **200**: a new document and a new ETag. Replace your copy.

`GET /v2/catalog/version` is a small check you can poll. It returns `revision`, `builtAt` and
`etag`. When its `etag` differs from the one you hold, fetch `/v2/catalog` again. `builtAt` and
`etag` are `null` while no document is built for the current revision yet.

`GET /v2/catalog/inputs/{endpointId}` works the same way, with its own ETag per endpoint. It
returns `endpointId`, `inputSchema`, `requestBinding`, `examples`, `method`, `path`, and
`outputSchema` when one is stored.

## Search: ids that join onto the document

```bash
curl "https://api.looot.ai/v2/catalog/search?q=verify+an+email&limit=20" \
  -H "Authorization: Bearer $LOOOT_TOKEN"
```

- `q` is required, at most 500 characters.
- `limit` is 1 to 1000 (default 50). `offset` is 0 to 100000 (default 0). A value out of range is a
  400 [`validation_error`](/errors/rest-errors#validation_error), not a shorter page.

The response has `query`, `documentEtag`, `total`, `offset`, `nextOffset`, `expandedCapabilities`
and `items`. Each item has four fields: endpointId, jobId, score, matchedBy. Look each
`endpointId` up in the document you hold to show its name and price.

`documentEtag` is the ETag of the document the search ran against. If it differs from the ETag
you hold, fetch `/v2/catalog` again first, or some ids may be missing from your copy.

`score` is 1 for the top row of the whole ranked list and falls toward 0 down the list
(1 - position/total), so a page after the first starts below 1. Sort by it descending, or keep the
order of `items`. It is not a relevance measure, so do not compare scores across queries. Search responses are not cached
(`Cache-Control: no-store`).

When no endpoint in the catalog does the job you searched for, `items` is empty and the response
adds `unsuppliedJobs` and a `warnings` entry with code [`no_supply_for_job`](/errors/job-refusals#no_supply_for_job).

## While the catalog loads

Just after a deploy, a /v2 route can answer 503 with code [`catalog_loading`](/errors/rest-errors#catalog_loading) (or
`catalog_v2_unavailable` if a build failed) and a `Retry-After` header in seconds. Wait that long and
retry. Errors use the same body as the rest of the REST API; see [Errors](/errors).

<Related />
