# Published catalog APIs

Find an API that CleanedWeb has published, read its contract and saved sample, and learn what you need before you run it. Anyone can browse the catalog. Running an API needs an account or API key and uses workspace credits; see [run a catalog API](/docs/run-catalog-api/).

The examples use the Gymshark Products API, which returns product records from collection pages on `uk.gymshark.com`. Identifiers, versions and records on this page are copied from a real response of the live catalog on 2026-11-04; check the live catalog for the current values. Fields the catalog page does not publish are left out of the records.

## Find a published API

Browse the [API catalog](/catalog/) or read the published list as JSON (the page can leave out an API that is still published here). Reading needs no credential:

```bash
export CLEANEDWEB_API_BASE="https://app.cleanedweb.com"

curl --fail-with-body --max-time 30 \
  "${CLEANEDWEB_API_BASE}/api/catalog/apis?limit=20&offset=0"
```

`limit` is 1–100 (default 20) and `offset` starts at 0. `q` searches text and `category` filters by category; both apply to the whole published inventory before paging. Offset paging reflects current content and is not a snapshot across requests. An unknown or repeated parameter, or a value out of range, answers `422` with `invalid_catalog_query`.

`sort` (`updated` or `name`) orders the list. An unknown `sort` value answers `422 invalid_catalog_query`.

Each item has `api_id`, `name`, `short_description`, `category`, `record_types`, `countries`, `languages`, `tags`, `limitations`, `site` (`name`, `domain`) and `endpoints`. An endpoint lists `endpoint_id`, `name`, `description` and `status` (`available` when you can run it). Read one API with `GET /api/catalog/apis/{api_id}`; it adds each endpoint's `active_version`.

```json
{
  "api_id": "generated_21ac18f87ad65090f8480c76",
  "name": "Gymshark Products",
  "category": "uncategorized",
  "record_types": [],
  "countries": [],
  "languages": [],
  "limitations": [],
  "site": { "name": "Gymshark Products", "domain": "uk.gymshark.com" },
  "endpoints": [
    {
      "endpoint_id": "generated_21ac18f87ad65090f8480c76_search",
      "name": "Extract records",
      "status": "available"
    }
  ]
}
```

Abbreviated. Empty `countries` and `languages` arrays mean the coverage is unspecified, not worldwide, and an empty `limitations` array means none were stated, not that there are none. The [catalog page](/catalog/) for an API adds reviewed coverage, limitations and the fields it returns, so read it before you rely on an API for a whole site. Record types and tags describe the listing; they are not extraction promises.

Responses are not cached (`Cache-Control: no-store`).

## Machine-readable copies

Every listing has a Markdown copy and a contract at its own address, and the whole catalog has an index and an OpenAPI file. They are written from the same reviewed data as the pages and need no credential:

| Address | Content |
| --- | --- |
| `https://cleanedweb.com/catalog/{slug}/index.md` | The listing as Markdown: description, source, key fields with sample values, inputs and a working request. |
| `https://cleanedweb.com/catalog/{slug}/contract.json` | The run request (`method`, `path`, full `url`, headers including `Idempotency-Key`), `input_schema`, `output_schema`, saved examples and links. Its `output_schema` lists only the fields the catalog publishes; see [Output fields](#output-fields). |
| `https://cleanedweb.com/catalog/index.json` | Every listed API with its address, source, key fields and links, and the live list address. |
| `https://cleanedweb.com/catalog/openapi.json` | OpenAPI 3.1 for the catalog routes and the run routes. |
| `https://cleanedweb.com/.well-known/api-catalog` | The same APIs as an [RFC 9727](https://www.rfc-editor.org/info/rfc9727) linkset. |

The `{slug}` of the Gymshark example is `gymshark-products-api`. The catalog page for each API links its two files.

## Read the contract

```http
GET /api/catalog/apis/{api_id}/endpoints/{endpoint_id}/contract
```

```bash
curl --fail-with-body --max-time 30 \
  "${CLEANEDWEB_API_BASE}/api/catalog/apis/${CLEANEDWEB_CATALOG_API_ID}/endpoints/${CLEANEDWEB_CATALOG_ENDPOINT_ID}/contract"
```

The contract is generated from the exact saved definition that runs:

| Field | Meaning |
| --- | --- |
| `definition_version` | The immutable 64-character version a run uses. Runs on a public API always use the endpoint's published version; you cannot select another. |
| `method`, `path` | The run request. Send it to `https://broker.cleanedweb.com` + `path`, for example `POST https://broker.cleanedweb.com/v1/catalog/public/apis/{api_id}/endpoints/{endpoint_id}/run`. |
| `input_schema` | JSON Schema for `input`. Its `required` array lists the inputs you must send. |
| `parameters` | The same inputs as a list with `location`, `required`, `type`, `default`, `enum` and a description. |
| `output_schema` | JSON Schema for each record. Its `required` array lists fields every record has. |
| `examples` | Saved validation samples. See [the saved sample](#the-saved-sample). |
| `authentication` | The credential the run needs. |
| `limits`, `pagination`, `billing` | Build-time metadata. `pagination` and `billing` can be `null`; billing for a run follows [Units and failure behavior](/docs/run-catalog-api/#errors-and-billing-codes). Do not treat `limits` as the bounds of a run. |

### Inputs and which are required

`input_schema.required` decides what you must send. For the Gymshark Products API it is empty, so `{"input": {}}` is a valid request and returns the default collection. Its one input is optional:

| Input | Type | Meaning |
| --- | --- | --- |
| `collection` | one of `New Product Drops`, `All Accessories`, `All Socks`, `All Equipment` | Chooses one of the site's collections. Omit it for the default collection. |

Some APIs also take `enrich_records` (boolean, default `true`), which reads each returned record's detail page and merges its fields; `summary.record_enrichment` then reports how many records were enriched and why the rest were not. This API has no detail pages, so it does not take it.

The contract has `additionalProperties: false`: an input that is not listed is rejected. Send only inputs you need.

A run is bounded by the run options `max_items` and `timeout_seconds`, sent beside `input`, and by money; they are not inputs. Omit them to let the source's records or your budget end the run; see [stop reasons](/docs/run-catalog-api/#stop-reasons).

### Output fields

`output_schema` lists the fields of each record; validate records against it rather than assuming a value. Which schema you read matters. The live contract lists every field the saved definition declares, and for the Gymshark Products API it allows no other field (`additionalProperties: false`). The `contract.json` on the catalog site lists only the fields the catalog publishes, and where it leaves fields out it sets `additionalProperties: true` and says so in `output_schema_note`, so a record may carry a field it does not name. Here `title` and `url` are required.

| Fields | Meaning |
| --- | --- |
| `title` | The product name. |
| `url` | The product page. |
| `image_url` | The product image. |

The live contract also lists `star_rating`, `plp_product_tag_new_read` and `plp_product_tag_sale_read`. The catalog site leaves them out of `contract.json`: in the saved sample `star_rating` holds the text `reviews.star_rating`, not a rating. Do not build on them. The API does not return prices, colours or ratings.

Dates and times inside a record, such as `updated_at`, are values the website reported. They are not the time of your request.

## The saved sample

`examples` holds records saved when the version was bound to the endpoint:

```json
{
  "kind": "saved_validation_records",
  "definition_version": "6eca2b7936228943b822a185df1e7e14a7c5280e92a1361b900a37251bcf7e17",
  "saved_at": 1793022782.7293062,
  "request": { "input": {} },
  "records": [
    {
      "title": "Seamless Running Waistbelt",
      "url": "https://uk.gymshark.com/products/gymshark-seamless-running-waistbelt-equipment-black-aw26",
      "image_url": "https://cdn.shopify.com/s/files/1/0098/8822/files/Seamless_Running_Waistbelt_GS_Black_I2C9H_BB2J_3_3840x.jpg?v=1790783631"
    },
    {
      "title": "Lifting Belt Keyring",
      "url": "https://uk.gymshark.com/products/gymshark-lifting-belt-keyring-misc-black-ss26",
      "image_url": "https://cdn.shopify.com/s/files/1/0098/8822/files/images-MiniLiftingBeltKeyringGSBlackI2C5M_BB2J_3862_V1_3840x.jpg?v=1775483915"
    }
  ],
  "live_verified": false
}
```

The sample holds two records; `star_rating` is left out above. How to read it:

- **It is a validation sample, not a live run.** It shows the shape and typical values of the records that validated this version. `live_verified: false` means nobody checked it against the website when you loaded the page. It is not a response to your request and not a count or price you can rely on today.
- **`saved_at` is when the sample was saved**, as Unix epoch seconds (UTC). Here `1793022782` is 2026-10-26 13:53 UTC. It does not change when you read the contract and is not the time of your next run. Catalog pages use it as the content date.
- **`request` is the input that produced it.** The Gymshark Products sample was saved with `{"input": {}}`.
- **`definition_version` ties it to a version.** A new version gets a new sample; the old one stays with the old version.

Prices, availability and other record values change on the website. Run the API to get current data.

## What is not promised

- **No live verification.** An API's `health` is `unknown` and `last_verified_at` is `null` until a health contract reports evidence. A listing is not a guarantee that the website is reachable or unchanged right now.
- **Coverage is only what the listing states.** Empty `countries` or `languages` arrays mean unspecified coverage. Read `limitations`. A result that is not marked complete does not show that the whole site was collected; check `summary.catalog_complete` and `run.stop_reason`.
- **Changes return a listing to review.** Changing an API's metadata or endpoints, or activating a new version, takes the listing offline until it is published again. An API that fails its checks is unpublished. A run already admitted can finish, and a delivered result stays readable after unpublishing.

## Authentication and access

Reading the catalog, contracts and samples is anonymous. Running needs a CleanedWeb account:

- **Public means available to every account.** There is no subscription and no per-API adoption step. A published API is not anonymous or free to run: each run uses the credits of the workspace that makes it.
- **Server integrations use an API key** with the `run` scope to start a run and read `/result`. Add `read` to read connection details and `results` to retrieve recorded results. See [authentication](/docs/authentication/). Public catalog access is available to a key whether or not it is restricted to selected private APIs.
- **The console uses your signed-in session.** Open the catalog in the [console](https://app.cleanedweb.com/?catalog_api=generated_21ac18f87ad65090f8480c76#catalog) to run an API from the browser.

Without a credential, a run answers `401`. A credential that is not tied to a workspace account answers `403`.

## Next

- [Run a catalog API](/docs/run-catalog-api/): request, polling, records, stop reasons and errors.
- [Idempotency and recovery](/docs/idempotency/): what a key identifies.
- [Billing and Units](/docs/billing/): balances and charge review.
