# Cartroute

> Cartroute is a unified retail search API for AI agents. One request returns matched offers, landed prices, 90-day price history and verified coupon codes from 12 major US retailers as normalized JSON. Free to start with 200 credits.

Cartroute is operated by Cartroute Labs, Inc. (San Francisco, California, United States). Retailers covered: Amazon, B&H Photo, Best Buy, Costco, Lowe's, Micro Center, Newegg, Sam's Club, Staples, Target, The Home Depot, Walmart.
Data source for this deployment: sandbox (synthetic prices for integration testing; every response says so in `data_source`).

## Start here

- [Quickstart](https://cart-route.com/docs/quickstart): get a key and make the first call in under a minute
- [OpenAPI 3.1 spec](https://cart-route.com/openapi.json): every endpoint, parameter and field
- [MCP server](https://cart-route.com/docs/mcp): connect Claude, Cursor or any MCP client to `https://cart-route.com/mcp`
- [Errors](https://cart-route.com/docs/errors): RFC 9457 problem+json with stable codes

## Key facts for an integrating agent

- Auth: `Authorization: Bearer cr_live_...`. New accounts get 100 credits; verifying the email adds 100.
- Cost: 1 credit per successful search, product lookup, price-history or coupon call. Validation errors are free.
- An agent can register for its user with `POST https://cart-route.com/api/v1/accounts`.
- Compare offers on `effective_price_usd` (price + shipping − best coupon needing no special eligibility), not `price_usd`.
- Costco and Sam's Club need a paid membership; pass `include_membership=false` to exclude them.
- Retries: send `Idempotency-Key` to avoid being charged twice.

## Reference

- [Retailers and coverage](https://cart-route.com/retailers)
- [Active coupons](https://cart-route.com/coupons)
- [How prices are compared](https://cart-route.com/guides/how-cartroute-compares-prices)
- [Pricing](https://cart-route.com/pricing)

## Optional

- [Full documentation as one file](https://cart-route.com/llms-full.txt)
- [Changelog](https://cart-route.com/changelog)

---

# What is the Cartroute API?

URL: https://cart-route.com/docs

The Cartroute API searches 12 US retailers (Amazon, B&H Photo, Best Buy, Costco, Lowe's, Micro Center, Newegg, Sam's Club, Staples, Target, The Home Depot, Walmart) in one call and returns every matching product with each retailer's offer, shipping, the best usable coupon, the effective price you would actually pay, stock, delivery time and a 90-day price verdict, as normalized JSON. It is built for AI agents: REST with an OpenAPI 3.1 description, a remote MCP server, stable machine-readable errors and credit-based metering.

Base URL: https://cart-route.com/api/v1

## What can I build with it?

- Shopping assistants that answer "where is this cheapest right now?" with a sourced number instead of a guess.

- Procurement agents that compare landed prices across Amazon, Walmart, Best Buy, Staples and B&H before buying hardware.

- Deal and price-drop alerts built on the 180-day daily price history and the `good_time_to_buy` verdict.

- Coupon checkers that only suggest codes with a measured success rate.

## Where should I start?

  [1 minuteQuickstartGet a key and run your first search.](https://cart-route.com/docs/quickstart)
  [AgentsMCP serverConnect Claude Code, Cursor or any MCP client.](https://cart-route.com/docs/mcp)
  [ReferenceSearch endpointEvery parameter, with examples.](https://cart-route.com/docs/api/search)
  [GuideRecommending where to buyTurn a response into a sound recommendation.](https://cart-route.com/docs/guides/recommendations)

## What does it cost?

Each successful search, product, price-history or coupon call costs one credit. New accounts receive 100 credits at signup and 100 more after confirming their email, with no card. Paid plans start at $29 a month for 5,000 credits. Details: [credits and pricing](https://cart-route.com/docs/credits).

## Which endpoints are there?

| Endpoint | Purpose | Credits |

| GETPOST[`/search`](https://cart-route.com/docs/api/search) | Search all retailers | 1 |

| GET[`/products/{id}`](https://cart-route.com/docs/api/products) | One product, every offer, specs | 1 |

| GET[`/products/{id}/price-history`](https://cart-route.com/docs/api/price-history) | Daily prices, 7 to 180 days | 1 |

| GET[`/coupons`](https://cart-route.com/docs/api/coupons) | Active coupons and promotions | 1 |

| GET[`/retailers`, `/categories`](https://cart-route.com/docs/api/reference) | Reference data | 0 |

| GET[`/account`, `/usage`](https://cart-route.com/docs/api/account) | Balance and ledger | 0 |

| POST[`/accounts`](https://cart-route.com/docs/api/accounts) | Create an account and key | 0 |

## Which conventions does every response follow?

- Money is always a number in US dollars, and every money field ends in `_usd`.

- Timestamps are ISO 8601 in UTC.

- Every response carries `request_id` and `data_source` (`live` or `sandbox`; see [sandbox data](https://cart-route.com/docs/sandbox)).

- Errors are RFC 9457 `application/problem+json` with a stable `code`; see [errors](https://cart-route.com/docs/errors).

- Metered responses report `credits.charged` and `credits.remaining`.

---

# How do I make my first Cartroute API call?

URL: https://cart-route.com/docs/quickstart

Create a free account, create an API key in the dashboard, and call `GET https://cart-route.com/api/v1/search` with the key in the `Authorization` header. The whole path takes about a minute and the first 200 searches are free.

## Step 1: How do I get an account and credits?

[Sign up](https://cart-route.com/signup) with your name, email and a password. 100 credits are added immediately. Click the link in the confirmation email and 100 more are added, once. An agent can also register on a person's behalf with [`POST /api/v1/accounts`](https://cart-route.com/docs/api/accounts).

## Step 2: How do I create an API key?

Open the [dashboard](https://cart-route.com/dashboard), name the key (for example "production agent") and press Create key. The key starts with `cr_live_` and is shown exactly once, because Cartroute stores only its hash. Put it in an environment variable:

```
export CARTROUTE_API_KEY="cr_live_..."
```

## Step 3: How do I run the first search?

  curl
  Python
  TypeScript

```
curl "https://cart-route.com/api/v1/search?q=noise+cancelling+headphones+under+%24400&in_stock=true" \
  -H "Authorization: Bearer $CARTROUTE_API_KEY"
```

```
import os
import requests

resp = requests.get(
    "https://cart-route.com/api/v1/search",
    params={"q": "noise cancelling headphones under $400", "in_stock": "true"},
    headers={"Authorization": f"Bearer {os.environ['CARTROUTE_API_KEY']}"},
    timeout=15,
)
resp.raise_for_status()
for product in resp.json()["results"]:
    s = product["price_summary"]
    print(product["title"], "-", s["best_retailer"], s["best_effective_price_usd"])
```

```
const url = new URL("https://cart-route.com/api/v1/search");
url.searchParams.set("q", "noise cancelling headphones under $400");
url.searchParams.set("in_stock", "true");

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.CARTROUTE_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail);
const body = await res.json();
for (const p of body.results) {
  console.log(p.title, p.price_summary.best_retailer, p.price_summary.best_effective_price_usd);
}
```

## What comes back?

A `search_result` with the products that matched, each carrying `price_summary`, `price_insight` and an `offers` array. This is a trimmed real response from the catalog:

```
{
  "object": "search_result",
  "data_source": "sandbox",
  "query": "sony wh-1000xm6",
  "total_results": 1,
  "results": [
    {
      "id": "prd_0dophw3",
      "title": "Sony WH-1000XM6 Wireless Noise Cancelling Headphones",
      "price_summary": {
        "best_offer_id": "off_1piefxe",
        "best_retailer": "Newegg",
        "best_effective_price_usd": 343.99,
        "lowest_price_usd": 343.99,
        "highest_price_usd": 454.99,
        "spread_usd": 111,
        "offers_count": 7,
        "in_stock_count": 6,
        "coupons_count": 4
      },
      "price_insight": {
        "verdict": "good_time_to_buy",
        "reason": "Current lowest price is at or within 1% of the 90-day low of $342.99."
      },
      "offers": [
        {
          "retailer": "Newegg",
          "price_usd": 343.99,
          "shipping_usd": 0,
          "effective_price_usd": 343.99,
          "availability": {
            "in_stock": true
          }
        }
      ]
    }
  ],
  "credits": {
    "charged": 1,
    "remaining": 199
  }
}
```

Compare offers on `effective_price_usd`. Every field is described in [response objects](https://cart-route.com/docs/objects).

## What should I read next?

- [Search reference](https://cart-route.com/docs/api/search): filters, sorting, budgets and pagination.

- [MCP server](https://cart-route.com/docs/mcp): skip the HTTP code and give an agent the tools directly.

- [Errors](https://cart-route.com/docs/errors) and [retries](https://cart-route.com/docs/idempotency): what to do when a call fails.

---

# How does Cartroute API authentication work?

URL: https://cart-route.com/docs/authentication

Every metered and account endpoint needs an API key sent as `Authorization: Bearer cr_live_...` (or as an `X-API-Key` header). Keys are created in the dashboard or by `POST /api/v1/accounts`, are shown once, and are never accepted in the URL.

## How do I send the key?

```
curl "https://cart-route.com/api/v1/account" -H "Authorization: Bearer $CARTROUTE_API_KEY"

# equivalent
curl "https://cart-route.com/api/v1/account" -H "X-API-Key: $CARTROUTE_API_KEY"
```

A key in a query string is ignored on purpose: URLs are written to proxy, browser and server logs, so a key placed there should be treated as leaked.

## What does a key look like?

`cr_live_` followed by 40 URL-safe characters. The dashboard shows only the first 16 characters afterwards (for example `cr_live_8fJ2kQ1a…`) so you can tell keys apart.

## How are keys stored?

Cartroute keeps only a SHA-256 hash of each key. A lost key cannot be recovered or displayed again; revoke it and create a new one.

## How many keys can an account have?

Ten active keys. Use one per environment or per agent so a leak can be revoked without downtime elsewhere, and so the dashboard shows usage per key.

## How do I rotate a key?

- Create a new key in the [dashboard](https://cart-route.com/dashboard).

- Deploy it to the agent or service.

- Watch the old key's Last used time stop moving.

- Revoke the old key. Requests using it immediately return `401 unauthorized`.

## What does an authentication failure return?

`401` with `code: "unauthorized"` when the key is missing, malformed or revoked, and `403` with `code: "account_suspended"` when the account has been suspended. Neither is charged. See [errors](https://cart-route.com/docs/errors).

## Which endpoints need no key?

`GET /api/v1`, `/retailers`, `/categories`, `POST /accounts`, the MCP discovery methods (`initialize`, `tools/list`, `resources/*`, `prompts/*`) and every public web page, including its [JSON form](https://cart-route.com/docs/pages-as-json).

## Is CORS enabled?

Yes, for every origin. The API authenticates with keys, never cookies, so cross-origin calls carry no ambient credentials. Do not ship a key in public browser code: anyone can read it. Call the API from your server or agent runtime.

---

# How are Cartroute credits counted?

URL: https://cart-route.com/docs/credits

One credit is spent per successful search, product, price-history or coupon call. Reference and account endpoints are free. Failed calls cost nothing. New accounts get 100 credits immediately and 100 more when the email address is confirmed; paid plans add 5,000 or 50,000 credits a month.

## What does each call cost?

| Operation | REST | MCP tool | Credits |

| Search all retailers | `GET/POST /search` | `search_products` | 1 |

| Product with every offer | `GET /products/{id}` | `get_product` | 1 |

| Price history | `GET /products/{id}/price-history` | `get_price_history` | 1 |

| Coupons | `GET /coupons` | `find_coupons` | 1 |

| Retailers, categories | `GET /retailers`, `/categories` | `list_retailers`, `list_categories` | 0 |

| Balance, ledger | `GET /account`, `/usage` | `get_account` | 0 |

| Web search on this site | `/search` page, logged in | n/a | 1 |

## When is a call not charged?

- Validation errors (`422`): the input is checked before anything is charged.

- Authentication and suspension errors (`401`, `403`).

- Rate-limit responses (`429`) and server errors (`5xx`).

- A repeat of a call with the same `Idempotency-Key` within 24 hours (see [retries](https://cart-route.com/docs/idempotency)).

A search that matches nothing is charged. "No product matches" is an answer the call paid to learn, and the response says so in `notes`.

## How do I see my balance?

Every metered response includes `credits.charged` and `credits.remaining`, repeated in the `X-Credits-Charged` and `X-Credits-Remaining` headers. `GET /api/v1/account` returns the balance and a `low_balance` flag (below 20); `GET /api/v1/usage` returns the ledger. The dashboard shows a 30-day usage chart.

## What happens at zero?

Metered calls return `402` with `code: "insufficient_credits"`, `credits_remaining`, `top_up_url` and, if the email is still unconfirmed, `verify_email_bonus`. An agent should stop and tell its user rather than retry.

## Do free credits expire?

No. Signup and verification credits stay on the account until used. Plan credits are granted monthly.

## What are the plans?

| Plan | Price | Credits | Extra 1,000 | Rate limit |

| Free | $0 | 200 once | n/a | 120/min |

| Builder | $29/mo | 5,000/mo | $6 | 120/min |

| Scale | $199/mo | 50,000/mo | $4 | 600/min |

| Enterprise | Custom | Volume | Custom | Custom |

See [pricing](https://cart-route.com/pricing), or email [sales@cart-route.com](mailto:sales@cart-route.com) for Enterprise.

---

# What does data_source "sandbox" mean?

URL: https://cart-route.com/docs/sandbox

`data_source: "sandbox"` means the response comes from the Cartroute sandbox catalog: real product identities, identifiers and retailer policies, with synthetic prices, stock levels and coupon codes that move daily like live data. Every API response, MCP tool result and JSON page states its data source, so an agent can always tell which it is looking at.

## What in the sandbox is real?

- Products, brands, models, GTINs and MPNs, and their specifications.

- Which retailers cover which categories, their membership rules, free-shipping thresholds and return windows.

- The API itself: authentication, credits, errors, rate limits, pagination, idempotency and the MCP server behave exactly as in production.

## What in the sandbox is synthetic?

- Prices and the 180-day price history, generated as realistic daily series with promotions.

- Stock levels and delivery windows.

- Coupon codes, success rates and sample sizes. Do not try sandbox codes at a real checkout.

- SKUs. `retailer_url` links open the retailer's search page for the product rather than a specific listing.

## Why does a sandbox exist?

So an agent can be built and tested end to end, including its handling of out-of-stock offers, membership-only stores, coupons needing eligibility and price verdicts, without anyone buying anything and without depending on retailer availability during development.

## Can I show sandbox prices to shoppers?

No. Present them as test data only. When your integration moves to a live key, `data_source` becomes `"live"`; check the field rather than assuming.

## How fresh is the sandbox?

Prices roll forward every day, each offer's `last_checked_at` is re-stamped every 10 minutes, and expired coupons are replaced, so freshness logic in an agent behaves as it would against live data.

---

# Search products: GET /api/v1/search

URL: https://cart-route.com/docs/api/search

`GET /api/v1/search` finds products across all 12 retailers and returns, for each, every offer with landed price, best coupon, effective price, stock and a 90-day price verdict. Plain-language budgets in `q` ("under $400", "between 200 and 500", "over 1k") become price filters. Costs 1 credit.

GET https://cart-route.com/api/v1/search

POST https://cart-route.com/api/v1/search (same parameters as a JSON body)

## Which parameters does search accept?

| Name | Type | Default | Description |

| `q` | string, ≤ 200 |  | What to find, in plain words. Required unless `category` or `brand` is set. |

| `category` | string |  | Exact category name: Appliances, Audio, Computing, Fitness, Gaming, Home, Kitchen, Laptops, Office, Outdoor, Phones, TVs, Tools, Wearables. |

| `brand` | string |  | Exact brand name, e.g. `Sony`. |

| `retailers` | csv (GET) or array (POST) | all | Retailer ids: amazon, bhphoto, bestbuy, costco, lowes, microcenter, newegg, samsclub, staples, target, homedepot, walmart. Slugs are also accepted. |

| `min_price` | number ≥ 0 |  | Minimum effective price in USD. Overrides a budget parsed from `q`. |

| `max_price` | number ≥ 0 |  | Maximum effective price in USD. Overrides a budget parsed from `q`. |

| `in_stock` | boolean | false | Drop out-of-stock and pre-order offers. |

| `include_membership` | boolean | true | Include Costco and Sam's Club, which require a paid membership to buy. |

| `sort` | enum | relevance | `relevance`, `price_asc`, `price_desc`, `savings` (widest retailer spread), `rating`. |

| `limit` | integer 1–25 | 10 | Products per page. |

| `cursor` | string |  | `next_cursor` from the previous page, unchanged. See [pagination](https://cart-route.com/docs/pagination). |

Headers: `Authorization` (required) and `Idempotency-Key` (optional, see [retries](https://cart-route.com/docs/idempotency)).

## How is the query matched?

Keywords are matched against identifiers (GTIN, MPN, model), brand, title, category and product keywords, with plurals and common synonyms handled ("tv" matches TVs, "ps5" matches PlayStation). A one- or two-word query must match every word; longer queries must match at least 60% of their words. Filler words such as "best", "cheap" and "deal" are ignored. The `interpreted` object in the response shows exactly how the query was understood.

## What does a GET request look like?

```
curl "https://cart-route.com/api/v1/search?q=65+inch+oled+tv&in_stock=true&include_membership=false&sort=price_asc&limit=5" \
  -H "Authorization: Bearer $CARTROUTE_API_KEY"
```

## What does a POST request look like?

```
curl -X POST "https://cart-route.com/api/v1/search" \
  -H "Authorization: Bearer $CARTROUTE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6c1f0e2a-quote-42" \
  -d '{"q": "cordless drill kit", "retailers": ["homedepot", "lowes", "amazon"], "max_price": 350}'
```

## What does the response contain?

| Field | Description |

| `object` | Always `search_result`. |

| `query`, `interpreted` | The input and how it was understood: keywords, parsed price bounds, filters. |

| `total_results`, `returned` | Matching products overall, and in this page. |

| `next_cursor` | Pass as `cursor` for the next page; `null` on the last page. |

| `results[]` | Products; see [the product object](https://cart-route.com/docs/objects#product). Specs are omitted in search; fetch them with [get product](https://cart-route.com/docs/api/products). |

| `notes` | Present when nothing matched, with suggestions. |

| `credits` | `charged` and `remaining`. |

```
{
  "object": "search_result",
  "api_version": "2026-09-01",
  "data_source": "sandbox",
  "request_id": "req_2m9x1q0a4b8c7f3k",
  "query": "sony wh-1000xm6",
  "interpreted": {
    "keywords": [
      "sony",
      "wh-1000xm6"
    ],
    "min_price_usd": null,
    "max_price_usd": null,
    "retailers": "all",
    "in_stock_only": false,
    "include_membership_retailers": true,
    "sort": "relevance"
  },
  "total_results": 1,
  "returned": 1,
  "next_cursor": null,
  "results": [
    {
      "object": "product",
      "id": "prd_0dophw3",
      "slug": "sony-wh-1000xm6",
      "title": "Sony WH-1000XM6 Wireless Noise Cancelling Headphones",
      "brand": "Sony",
      "category": "Audio",
      "subcategory": "Headphones",
      "identifiers": {
        "gtin": "00027242928138",
        "mpn": "WH1000XM6/B",
        "model": "WH-1000XM6"
      },
      "rating": 4.6,
      "review_count": 6720,
      "released_on": "2026-05-15",
      "summary": "The XM6 restores the folding hinge the XM5 dropped and adds a 12-microphone array. Street price settles roughly 15% below MSRP within four months of launch, and Costco bundles a hard case at the same price point.",
      "url": "https://cart-route.com/p/sony-wh-1000xm6",
      "price_summary": {
        "best_offer_id": "off_1piefxe",
        "best_retailer": "Newegg",
        "best_effective_price_usd": 343.99,
        "lowest_price_usd": 343.99,
        "highest_price_usd": 454.99,
        "spread_usd": 111,
        "offers_count": 7,
        "in_stock_count": 6,
        "coupons_count": 4
      },
      "price_insight": {
        "verdict": "good_time_to_buy",
        "reason": "Current lowest price is at or within 1% of the 90-day low of $342.99.",
        "low_90d_usd": 342.99,
        "high_90d_usd": 431.99,
        "avg_30d_usd": 372.72,
        "trend_30d_pct": -3.4,
        "is_at_90d_low": true,
        "observations": 91
      },
      "offers": [
        {
          "offer_id": "off_1piefxe",
          "retailer": {
            "id": "newegg",
            "name": "Newegg",
            "domain": "newegg.com",
            "membership_required": false,
            "returns_window_days": 30
          },
          "sku": "N82E16877620928",
          "condition": "new",
          "seller": "Newegg",
          "price_usd": 343.99,
          "list_price_usd": 449.99,
          "discount_from_list_pct": 23.6,
          "shipping_usd": 0,
          "landed_price_usd": 343.99,
          "best_coupon": null,
          "effective_price_usd": 343.99,
          "coupons": [],
          "availability": {
            "in_stock": true,
            "stock_level": "in_stock",
            "delivery_days_min": 3,
            "delivery_days_max": 6,
            "store_pickup": false
          },
          "retailer_url": "https://www.newegg.com/p/pl?d=Sony+WH-1000XM6",
          "buy_url": "https://cart-route.com/go/off_1piefxe",
          "last_checked_at": "2026-09-23T13:29:33.558Z",
          "freshness_seconds": 880
        }
      ]
    }
  ],
  "credits": {
    "charged": 1,
    "remaining": 199
  }
}
```

## Which errors can search return?

`401 unauthorized`, `402 insufficient_credits`, `422 validation_error` (the body names the `param`, e.g. an unknown retailer id or `limit` above 25) and `429 rate_limited`. See [errors](https://cart-route.com/docs/errors).

---

# Get a product: GET /api/v1/products/{id}

URL: https://cart-route.com/docs/api/products

`GET /api/v1/products/{id}` returns one product with its specifications, every retailer offer, the coupons that apply to each offer and a 90-day price verdict. `{id}` may be a product id (`prd_...`), the slug, the GTIN or the MPN. Costs 1 credit.

GET https://cart-route.com/api/v1/products/{id}

## Which identifiers can I pass?

| Form | Example |

| Product id | `prd_0dophw3` |

| Slug | `sony-wh-1000xm6` |

| GTIN | `00027242928138` |

| MPN | `WH1000XM6/B` |

Product ids are stable. Slugs are stable too, but store the id if you persist references.

## What does a request look like?

```
curl "https://cart-route.com/api/v1/products/sony-wh-1000xm6" \
  -H "Authorization: Bearer $CARTROUTE_API_KEY"
```

## What does the response contain?

`{ "object": "product", "product": { ... }, "credits": { ... } }`. The product has every field of [the product object](https://cart-route.com/docs/objects#product) plus `specs`, a flat object of display-ready strings. Offers are sorted by effective price, cheapest first.

```
{
  "object": "product",
  "data_source": "sandbox",
  "product": {
    "object": "product",
    "id": "prd_0dophw3",
    "slug": "sony-wh-1000xm6",
    "title": "Sony WH-1000XM6 Wireless Noise Cancelling Headphones",
    "brand": "Sony",
    "category": "Audio",
    "subcategory": "Headphones",
    "identifiers": {
      "gtin": "00027242928138",
      "mpn": "WH1000XM6/B",
      "model": "WH-1000XM6"
    },
    "rating": 4.6,
    "review_count": 6720,
    "released_on": "2026-05-15",
    "summary": "The XM6 restores the folding hinge the XM5 dropped and adds a 12-microphone array. Street price settles roughly 15% below MSRP within four months of launch, and Costco bundles a hard case at the same price point.",
    "url": "https://cart-route.com/p/sony-wh-1000xm6",
    "price_summary": {
      "best_offer_id": "off_1piefxe",
      "best_retailer": "Newegg",
      "best_effective_price_usd": 343.99,
      "lowest_price_usd": 343.99,
      "highest_price_usd": 454.99,
      "spread_usd": 111,
      "offers_count": 7,
      "in_stock_count": 6,
      "coupons_count": 4
    },
    "price_insight": {
      "verdict": "good_time_to_buy",
      "reason": "Current lowest price is at or within 1% of the 90-day low of $342.99.",
      "low_90d_usd": 342.99,
      "high_90d_usd": 431.99,
      "avg_30d_usd": 372.72,
      "trend_30d_pct": -3.4,
      "is_at_90d_low": true,
      "observations": 91
    },
    "offers": [
      {
        "offer_id": "off_1piefxe",
        "retailer": {
          "id": "newegg",
          "name": "Newegg",
          "domain": "newegg.com",
          "membership_required": false,
          "returns_window_days": 30
        },
        "sku": "N82E16877620928",
        "condition": "new",
        "seller": "Newegg",
        "price_usd": 343.99,
        "list_price_usd": 449.99,
        "discount_from_list_pct": 23.6,
        "shipping_usd": 0,
        "landed_price_usd": 343.99,
        "best_coupon": null,
        "effective_price_usd": 343.99,
        "coupons": [],
        "availability": {
          "in_stock": true,
          "stock_level": "in_stock",
          "delivery_days_min": 3,
          "delivery_days_max": 6,
          "store_pickup": false
        },
        "retailer_url": "https://www.newegg.com/p/pl?d=Sony+WH-1000XM6",
        "buy_url": "https://cart-route.com/go/off_1piefxe",
        "last_checked_at": "2026-09-23T13:29:33.558Z",
        "freshness_seconds": 880
      }
    ],
    "specs": {
      "Drivers": "30mm carbon fibre composite",
      "Battery": "30 hours ANC on, 3 min charge for 3 hours",
      "Weight": "254 g"
    }
  },
  "credits": {
    "charged": 1,
    "remaining": 198
  }
}
```

## When should I use this instead of search?

When you already know the product (a user pasted a model number or a previous search returned its id) and you want the full offer list and specs. Search returns up to every offer too, but omits specs and costs the same, so fetching by id is the precise choice.

## Which errors can it return?

`404 not_found` when no product matches the identifier (not charged), plus the common `401`, `402` and `429`.

---

# Price history: GET /api/v1/products/{id}/price-history

URL: https://cart-route.com/docs/api/price-history

`GET /api/v1/products/{id}/price-history` returns the daily lowest price across all retailers and a daily series per retailer, for the last 7 to 180 days (default 90). Use it to decide whether today's price is a genuine low. Costs 1 credit.


GET https://cart-route.com/api/v1/products/{id}/price-history?days=90



## Which parameters does it take?


  

| Name | Type | Default | Description | 
    

| `id` (path) | string |  | Product id, slug, GTIN or MPN. | 

| `days` | integer 7–180 | 90 | Window length, ending today. | 
    






## What does the response look like?


```
{
  "object": "price_history",
  "data_source": "sandbox",
  "history": {
    "object": "price_history",
    "product_id": "prd_0dophw3",
    "title": "Sony WH-1000XM6 Wireless Noise Cancelling Headphones",
    "days": 30,
    "currency": "USD",
    "lowest_by_day": [
      {
        "date": "2026-08-25",
        "price_usd": 369.99
      },
      {
        "date": "2026-08-26",
        "price_usd": 364.99
      }
    ],
    "by_retailer": [
      {
        "retailer_id": "amazon",
        "retailer_name": "Amazon",
        "points": [
          {
            "date": "2026-08-25",
            "price_usd": 379.99
          }
        ]
      }
    ]
  },
  "credits": {
    "charged": 1,
    "remaining": 197
  }
}
```

Truncated: a 30-day response has 30 entries in `lowest_by_day` and in each retailer's `points`.



## How should an agent read it?



- Is today a low? Compare today's `lowest_by_day` value with the minimum of the series. Search and product responses already do this for 90 days in `price_insight.verdict`; call this endpoint when the user wants the evidence or a different window.

- Which retailer discounts? `by_retailer` shows who ran promotions and how deep they went.

- Should I wait? A series that dipped 15 to 20% several times in 90 days suggests another promotion is likely; a flat series suggests the price is structural.




## Which errors can it return?

`404 not_found` for an unknown product and `422 validation_error` for `days` outside 7–180 (neither is charged), plus `401`, `402` and `429`.

---

# Find coupons: GET /api/v1/coupons

URL: https://cart-route.com/docs/api/coupons

`GET /api/v1/coupons` lists active coupon codes and automatic promotions, filterable by retailer, category or product, each with its redemption success rate, sample size, terms and expiry. Costs 1 credit. Coupons that apply to a product are also embedded in every search and product offer.

GET https://cart-route.com/api/v1/coupons?retailer=target

## Which filters does it take?

| Name | Type | Description |

| `retailer` | string | Retailer id or slug. |

| `category` | string | Category name. Returns category coupons and sitewide ones. |

| `product` | string | Product id, slug, GTIN or MPN. Returns coupons that apply to it. |

| `limit` | integer 1–100 | Default 50. Sorted by success rate, then sample size. |

## What does a coupon look like?

```
{
  "coupon_id": "cpn_05pgl3b",
  "retailer_id": "target",
  "retailer_name": "Target",
  "code": "CIRCLE20KIT",
  "requires_code": true,
  "kind": "percent",
  "value": 20,
  "value_unit": "percent",
  "title": "Target Circle 20% off one kitchen item",
  "terms": "Requires a Target Circle account. Must be activated in the app before checkout.",
  "min_spend_usd": null,
  "applies_to": "category:Kitchen",
  "stacks_with_sale": true,
  "requires_eligibility": false,
  "success_rate": 0.68,
  "sample_size": 3301,
  "last_verified_at": "2026-09-23T07:12:40.000Z",
  "expires_at": "2026-09-30T10:12:40.000Z",
  "source": "loyalty_program"
}
```

## What do the coupon fields mean?

| Field | Meaning |

| `code`, `requires_code` | The code to enter, or `null` when the promotion applies automatically. |

| `kind` | `percent`, `amount` (USD off), `shipping` (free shipping) or `gift_card` (a perk; never subtracted from price). |

| `applies_to` | `product`, `category:<name>` or `sitewide`. |

| `min_spend_usd` | Minimum order value, or `null`. |

| `requires_eligibility` | Student, education or similar. Listed, never assumed in `effective_price_usd`. |

| `success_rate`, `sample_size` | Share of recent redemption attempts that applied, and how many attempts that is based on. |

| `stacks_with_sale` | Whether it applies on top of a sale price. |

| `last_verified_at`, `expires_at` | When it was last checked, and when it ends. Expired coupons never appear. |

| `estimated_savings_usd` | Only inside an offer: the saving on that specific offer. |

## How much should an agent trust a coupon?

Weigh `success_rate` by `sample_size`: 0.9 over 2,000 attempts is dependable, 0.9 over 40 is a hint. Automatic promotions (`requires_code: false`) are the most reliable because the retailer applies them only when the cart qualifies. Tell the shopper about `terms` that need action, such as activating a loyalty offer.

---

# Reference data: retailers and categories

URL: https://cart-route.com/docs/api/reference

`GET /api/v1/retailers` and `GET /api/v1/categories` are free and need no key. They return the valid values for the `retailers`, `category` and `brand` search filters, plus each retailer's membership rule, free-shipping threshold, return window and refresh interval.

GET https://cart-route.com/api/v1/retailers

GET https://cart-route.com/api/v1/categories

## Which retailers are covered?

| id | Name | Membership | Free shipping over | Returns | Refresh |

| `amazon` | Amazon | No | $35 | 30 days | 10 min |

| `bhphoto` | B&H Photo | No | $49 | 30 days | 30 min |

| `bestbuy` | Best Buy | No | $35 | 15 days | 15 min |

| `costco` | Costco | Required | always | 90 days | 30 min |

| `lowes` | Lowe's | No | $45 | 90 days | 30 min |

| `microcenter` | Micro Center | No | pickup only | 30 days | 10 min |

| `newegg` | Newegg | No | $25 | 30 days | 10 min |

| `samsclub` | Sam's Club | Required | always | 90 days | 30 min |

| `staples` | Staples | No | $35 | 14 days | 30 min |

| `target` | Target | No | $35 | 90 days | 15 min |

| `homedepot` | The Home Depot | No | $45 | 90 days | 30 min |

| `walmart` | Walmart | No | $35 | 90 days | 15 min |

## What does /retailers return?

```
{
  "object": "retailer_list",
  "count": 12,
  "retailers": [
    {
      "id": "costco",
      "name": "Costco",
      "domain": "costco.com",
      "membership_required": true,
      "free_shipping_over_usd": 0,
      "returns_window_days": 90,
      "refresh_interval_seconds": 1800,
      "coverage_note": "Membership required to purchase. Bundle contents often differ from the equivalent SKU elsewhere.",
      "url": "https://cart-route.com/retailers/costco"
    }
  ]
}
```

## Which categories exist?

Appliances (2), Audio (4), Computing (4), Fitness (1), Gaming (5), Home (3), Kitchen (4), Laptops (4), Office (1), Outdoor (2), Phones (3), TVs (3), Tools (2), Wearables (2). `/categories` also lists every brand with its product count.

## How often should I call these?

They change rarely. Cache them for a day, or read them once when an agent session starts. They are also available as MCP resources (`cartroute://retailers`, `cartroute://categories`) and as the free tools `list_retailers` and `list_categories`.

---

# Account and usage: GET /api/v1/account, /usage

URL: https://cart-route.com/docs/api/account

`GET /api/v1/account` returns the caller's credit balance, verification state and key details; `GET /api/v1/usage` returns the credit ledger. Both need a key and cost nothing, so an agent can check its budget before a batch of searches.

GET https://cart-route.com/api/v1/account

GET https://cart-route.com/api/v1/usage?limit=50

## What does /account return?

```
{
  "object": "account",
  "request_id": "req_7f3k2m9x1q0a4b8c",
  "id": "usr_4k2m9x1q0a4b8c7f3k",
  "email": "sam@example.com",
  "name": "Sam Rivera",
  "email_verified": false,
  "credits_remaining": 96,
  "low_balance": false,
  "verification_bonus_available": 100,
  "api_key": {
    "id": "key_9x1q0a4b8c7f3k2m",
    "name": "production agent",
    "scopes": [
      "search:read",
      "catalog:read",
      "coupons:read"
    ]
  },
  "dashboard": "https://cart-route.com/dashboard"
}
```

`low_balance` is true below 20 credits. `verification_bonus_available` is the number of credits the account will receive when its email is confirmed (0 once granted).

## What does /usage return?

```
{
  "object": "usage",
  "credits_remaining": 96,
  "entries": [
    {
      "id": "led_1q0a4b8c7f3k2m9x",
      "at": "2026-09-23T10:41:07.512Z",
      "delta": -1,
      "balance_after": 96,
      "reason": "search",
      "description": "Product search"
    },
    {
      "id": "led_0a4b8c7f3k2m9x1q",
      "at": "2026-09-23T10:02:55.103Z",
      "delta": 100,
      "balance_after": 100,
      "reason": "signup_grant",
      "description": "Welcome grant"
    }
  ]
}
```

Entries are newest first. `limit` is 1–200, default 50. Reasons: `signup_grant`, `verify_grant`, `search`, `product_lookup`, `price_history`, `coupon_lookup`, `plan_builder`, `plan_scale`, `admin_adjustment`, `refund`.

---

# Create an account: POST /api/v1/accounts

URL: https://cart-route.com/docs/api/accounts

`POST /api/v1/accounts` lets an agent create a Cartroute account for the person it works for and receive a working API key with 100 credits in the response. A confirmation email goes to the address; when the person clicks it, the account gets 100 more. No key is needed to call it.


POST https://cart-route.com/api/v1/accounts



## What must the agent do before calling it?

Ask the person. The request must set `accept_terms: true`, which states that the account holder has agreed to the [Terms of Service](https://cart-route.com/legal/terms). Use the person's own email address; the confirmation link goes there and the bonus credits depend on it.



## Which fields does it take?


  

| Field | Required | Rules | 
    

| `email` | yes | Valid address, not already registered. | 

| `full_name` | yes | 2–80 characters. | 

| `password` | yes | At least 10 characters, with a letter and a number. Used for dashboard login. | 

| `accept_terms` | yes | Must be `true`. | 

| `company` | no | Up to 80 characters. | 

| `use_case` | no | Up to 300 characters. | 

| `agent_framework` | no | `mcp`, `openai-agents`, `langchain`, `llamaindex`, `crewai`, `n8n`, `custom` or `none`. | 

| `key_name` | no | Label for the key created with the account. | 
    






## What does a request look like?


```
curl -X POST "https://cart-route.com/api/v1/accounts" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "sam@example.com",
    "full_name": "Sam Rivera",
    "password": "a-long-passphrase-42",
    "accept_terms": true,
    "agent_framework": "mcp",
    "key_name": "shopping assistant"
  }'
```



## What does it return?


```
{
  "object": "account_created",
  "request_id": "req_0a4b8c7f3k2m9x1q",
  "account": {
    "id": "usr_4k2m9x1q0a4b8c7f3k",
    "email": "sam@example.com",
    "name": "Sam Rivera",
    "email_verified": false
  },
  "api_key": "cr_live_…",
  "api_key_notice": "Store this key now. It is shown once and only its hash is kept.",
  "credits_remaining": 100,
  "next_step": "A verification link was emailed to sam@example.com. The account holder must click it to unlock 100 more credits."
}
```

Status `201`. Store `api_key` immediately; it cannot be retrieved later.



## What are the limits?

Five accounts per IP address per hour; the sixth returns `429 rate_limited`. A validation failure returns `422` with the first problem in `detail` and every problem in `errors`, keyed by field.

---

# Which fields does a Cartroute response contain?

URL: https://cart-route.com/docs/objects

Every result is a product with a `price_summary`, a `price_insight` and an array of offers; each offer carries its own coupons. Money fields end in `_usd` and are plain numbers. The field to rank on is `offer.effective_price_usd`.

## What is in a product?

| Field | Type | Description |

| `id` | string | Stable id, `prd_…`. |

| `slug`, `url` | string | Readable identifier and the product's page on cart-route.com. |

| `title`, `brand`, `category`, `subcategory` | string | Descriptive fields. |

| `identifiers` | object | `gtin`, `mpn`, `model`. Offers are matched across retailers on these, never on title. |

| `rating`, `review_count` | number | Aggregate rating out of 5. |

| `summary` | string | An editorial note on buying this product, often about how its price behaves. |

| `specs` | object | Display-ready specifications. Only on [get product](https://cart-route.com/docs/api/products). |

| `price_summary` | object | See [below](#price-summary). |

| `price_insight` | object | See [below](#price-insight). |

| `offers` | array | Sorted by effective price, cheapest first. |

## What does price_summary tell me?

| Field | Description |

| `best_offer_id`, `best_retailer` | The in-stock offer with the lowest effective price (falls back to any offer if none is in stock). |

| `best_effective_price_usd` | That offer's effective price. |

| `lowest_price_usd`, `highest_price_usd`, `spread_usd` | The range of listed prices across offers. |

| `offers_count`, `in_stock_count`, `coupons_count` | Counts after your filters are applied. |

## What does price_insight tell me?

| Field | Description |

| `verdict` | `good_time_to_buy` (within 1% of the 90-day low), `above_recent_average` (more than 5% above the 30-day average), `typical_price`, or `insufficient_history`. |

| `reason` | One sentence with the numbers behind the verdict, suitable to quote. |

| `low_90d_usd`, `high_90d_usd`, `avg_30d_usd` | Computed from the daily lowest price across retailers. |

| `trend_30d_pct`, `is_at_90d_low`, `observations` | Direction, low flag, and number of daily data points. |

## What is in an offer?

| Field | Description |

| `offer_id`, `sku` | Offer id and the retailer's SKU. |

| `retailer` | `id`, `name`, `domain`, `membership_required`, `returns_window_days`. |

| `condition` | `new` or `open_box`. Open-box offers are separate offers, never mixed with new. |

| `price_usd` | Listed price. |

| `list_price_usd`, `discount_from_list_pct` | Manufacturer list price and the discount from it. |

| `shipping_usd` | Standard shipping; 0 above the retailer's free-shipping threshold. |

| `landed_price_usd` | Price plus shipping, before tax. |

| `best_coupon` | The applicable coupon with the largest saving that needs no special eligibility, or `null`. |

| `effective_price_usd` | Landed price minus `best_coupon.estimated_savings_usd`. Rank on this. |

| `coupons` | Every applicable coupon, including ones needing eligibility. See [coupons](https://cart-route.com/docs/api/coupons). |

| `availability` | `in_stock`, `stock_level` (`in_stock`, `low_stock`, `out_of_stock`, `preorder`), `delivery_days_min`, `delivery_days_max` (null when pickup-only), `store_pickup`. |

| `retailer_url` | Direct link to the retailer. |

| `buy_url` | Tracked redirect to the same place, via cart-route.com. |

| `last_checked_at`, `freshness_seconds` | When the price was last observed and how long ago. |

## How exactly is effective price calculated?

- Start from `price_usd`.

- Add `shipping_usd` to get `landed_price_usd`. Tax is excluded because it depends on the delivery address.

- From the coupons that apply to this product and retailer, meet their minimum spend and need no eligibility, take the single largest saving. Coupons are never stacked. A `percent` coupon saves that share of the price; `amount` saves a fixed sum; `shipping` saves the shipping charge; `gift_card` saves nothing up front.

- Subtract it to get `effective_price_usd`.

The full method, with its limits, is in [how Cartroute compares prices](https://cart-route.com/guides/how-cartroute-compares-prices).

## What is in every response envelope?

`object` (the response type), `api_version`, `data_source`, `request_id` (quote it to support), `generated_at`, and on metered calls `credits`. A replayed idempotent call adds `idempotent_replay: true`.

---

# How does the Cartroute API report errors?

URL: https://cart-route.com/docs/errors

Every API error is an RFC 9457 `application/problem+json` body with a stable machine `code`, an HTTP status, a human `detail` and the `request_id`. Branch on `code`, never on the wording of `detail`. No error is ever charged.

## What does an error body look like?

```
{
  "type": "https://cart-route.com/docs/errors#validation_error",
  "title": "Request validation failed",
  "status": 422,
  "detail": "`limit` must be an integer between 1 and 25.",
  "code": "validation_error",
  "instance": "/api/v1/search?q=tv&limit=500",
  "request_id": "req_2m9x1q0a4b8c7f3k",
  "param": "limit"
}
```

Extra fields appear where they help: `param` and sometimes `valid_values` for validation errors, `credits_remaining` and `top_up_url` for credit errors, `retry_after_seconds` for rate limits.

## Which error codes exist and what should an agent do?

| HTTP | code | Meaning | Agent action | Retry? |

| 401 | `unauthorized` | Key missing, malformed or revoked. | Stop; ask the user for a valid key. | No |

| 402 | `insufficient_credits` | Balance below the call's cost. | Stop; tell the user, cite `top_up_url` and any `verify_email_bonus`. | No |

| 403 | `account_suspended` | Account suspended by an operator. | Stop; contact support@cart-route.com. | No |

| 403 | `forbidden` | Not allowed for this key. | Stop. | No |

| 404 | `not_found` | Unknown product or path. | Search instead, or check the id. | No |

| 405 | `method_not_allowed` | Wrong HTTP method. | Fix the request. | No |

| 422 | `validation_error` | A parameter is invalid; `param` names it. | Fix that parameter and resend. | After fixing |

| 429 | `rate_limited` | Too many requests in the window. | Wait `Retry-After` seconds. | Yes |

| 500 | `internal_error` | Failure on our side. | Retry with backoff and the same `Idempotency-Key`. | Yes |

## How do errors appear over MCP?

A tool that fails returns a normal result with `isError: true`, and its JSON content carries the same `code` values, for example `{"error": "...", "code": "insufficient_credits", "credits_remaining": 0}`. That lets the model read the problem and explain it. Protocol-level problems (unknown method, malformed JSON) use JSON-RPC error codes: `-32700` parse error, `-32600` invalid request, `-32601` method not found, `-32602` invalid params, `-32001` invalid API key.

## What about web pages?

Pages requested as JSON return `{"object": "error", "code": ..., "detail": ...}` with the matching status, for example `404 not_found` or `402 preview_limit_reached` when an anonymous visitor exceeds five searches a day.

---

# What are the Cartroute API rate limits?

URL: https://cart-route.com/docs/rate-limits

The API allows 120 requests per minute per API key and 30 per minute per IP address for calls without a key. Every response reports the current state in `RateLimit-*` headers, and a `429` adds `Retry-After`. Rate-limited calls are never charged.

## Which headers report the limit?

| Header | Meaning |

| `RateLimit-Limit` | Requests allowed in the window. |

| `RateLimit-Remaining` | Requests left in the current window. |

| `RateLimit-Reset` | Seconds until the window resets. |

| `RateLimit-Policy` | The policy, e.g. `120;w=60`. |

| `Retry-After` | On 429 only: seconds to wait before retrying. |

## What should an agent do when limited?

Sleep for `Retry-After` seconds and resend the same request with the same `Idempotency-Key`. Do not retry immediately in a loop: every retry inside the window is itself rejected. For planned bursts, pace requests using `RateLimit-Remaining` instead of waiting to be rejected.

## Are there other limits?

- `POST /api/v1/accounts`: five accounts per IP per hour.

- Anonymous web searches on cart-route.com: five per visitor per day; logged-in searches use credits instead.

- Request bodies: 32 KB for the REST API, 64 KB for MCP.

## Can I get a higher limit?

The Scale plan allows 600 requests per minute and Enterprise is set per contract. Email [sales@cart-route.com](mailto:sales@cart-route.com).

---

# How do I retry a Cartroute call safely?

URL: https://cart-route.com/docs/idempotency

Send an `Idempotency-Key` header on metered calls. If the same key is used again for the same operation within 24 hours, Cartroute returns the first response unchanged, with `idempotent_replay: true`, and charges nothing. That makes it safe to retry after a timeout without paying twice.

## How do I use an idempotency key?

```
curl "https://cart-route.com/api/v1/search?q=robot+vacuum" \
  -H "Authorization: Bearer $CARTROUTE_API_KEY" \
  -H "Idempotency-Key: 4b1d7c1e-3f2a-4a4e-9a57-1b1a2f6c0e91"
```

Use a fresh random value (a UUID) per logical request, and reuse it only when retrying that same request. Keys are scoped to your account and the operation, up to 200 characters.

## Which failures should be retried?

| Outcome | Retry? | How |

| Network error or timeout | Yes | Same request, same `Idempotency-Key`. |

| `429 rate_limited` | Yes | After `Retry-After` seconds. |

| `500 internal_error`, `502`, `503`, `504` | Yes | Exponential backoff: 1 s, 2 s, 4 s, up to 3 attempts. |

| `401`, `402`, `403`, `404` | No | The answer will not change. |

| `422` | Only after fixing | Change the parameter named in `param`. |

## What does a replayed response look like?

Identical to the original body, plus `"idempotent_replay": true` and `"credits": {"charged": 0, "remaining": ...}` showing the current balance.

## Does the MCP server support idempotency?

Not per call. MCP clients do not send an equivalent key, so each successful `tools/call` is charged. Agents that need retry-safety for expensive loops should call the REST API.

---

# How does pagination work?

URL: https://cart-route.com/docs/pagination

Search returns at most `limit` products (1 to 25, default 10) and a `next_cursor`. Pass that value back as `cursor`, unchanged and with the same other parameters, to get the next page. On the last page `next_cursor` is `null`. Each page is a separate metered call.

## How do I fetch the next page?

```
curl "https://cart-route.com/api/v1/search?q=laptop&limit=5" -H "Authorization: Bearer $CARTROUTE_API_KEY"
# ... "next_cursor": "eyJvIjo1fQ", "links": {"next": "https://cart-route.com/api/v1/search?q=laptop&cursor=eyJvIjo1fQ"}

curl "https://cart-route.com/api/v1/search?q=laptop&limit=5&cursor=eyJvIjo1fQ" -H "Authorization: Bearer $CARTROUTE_API_KEY"
```

## How do I iterate every page?

```
import os
import requests

def search_all(q, **filters):
    params = {"q": q, "limit": 25, **filters}
    headers = {"Authorization": f"Bearer {os.environ['CARTROUTE_API_KEY']}"}
    while True:
        body = requests.get("https://cart-route.com/api/v1/search", params=params, headers=headers, timeout=15).json()
        yield from body["results"]
        if not body.get("next_cursor"):
            return
        params["cursor"] = body["next_cursor"]
```

## Should I paginate at all?

Usually not. Results are ranked, so the first page holds the best matches; an agent answering "where should I buy X" rarely needs more than 5. Narrow the query or add filters before paging, which costs fewer credits than paging through broad results.

## What makes a cursor invalid?

Editing it, or using a value from another source. An invalid cursor returns `422 validation_error` with `param: "cursor"` and is not charged. Cursors do not expire, but results can shift between pages as prices change, because ranking is recomputed on every call.

---

# Can I read any Cartroute page as JSON?

URL: https://cart-route.com/docs/pages-as-json

Every public page on cart-route.com (products, categories, retailers, coupons, guides, pricing, status and these docs) returns structured JSON when requested with `Accept: application/json` or `?format=json`. Page views are free, need no key, and are the cheapest way for a browsing agent to read the catalog.

## How do I request JSON?

```
curl -H "Accept: application/json" https://cart-route.com/p/sony-wh-1000xm6
curl "https://cart-route.com/c/tvs?format=json"
curl "https://cart-route.com/docs/errors?format=json"
```

HTML pages also advertise their JSON form with `Link: <…?format=json>; rel="alternate"; type="application/json"`, and every response carries `Link` headers pointing to the OpenAPI description and the MCP server card.

## Which pages have a JSON form?

| Page | JSON object |

| `/p/{slug}` | `product` with offers, specs and 90-day `price_history` |

| `/c/{category}` | `category` with every product in it |

| `/retailers`, `/retailers/{slug}` | `retailer_list`, `retailer` |

| `/coupons` | `coupon_list` |

| `/search?q=` | `search_result` (anonymous preview: 5 a day) |

| `/guides`, `/guides/{slug}` | `guide_list`, `guide` with live figures |

| `/docs/…` | `doc` with the page text |

| `/pricing`, `/about`, `/status`, `/` | `pricing`, `about`, status, `page` |

## When should an agent use the API instead?

When it needs filters, sorting, pagination, idempotent retries or more than five searches a day. Page JSON is for reading known URLs, for example a product link a user pasted.

---

# How do I connect an AI agent to Cartroute with MCP?

URL: https://cart-route.com/docs/mcp

Add `https://cart-route.com/mcp` as a remote MCP server with the header `Authorization: Bearer cr_live_...`. The server speaks Streamable HTTP, exposes 7 tools, and works with Claude Code, Claude Desktop, Cursor and any client that supports remote servers. Discovery needs no key; the four data tools cost one credit per call.

POST https://cart-route.com/mcp

## How do I add it to Claude Code?

```
claude mcp add --transport http cartroute https://cart-route.com/mcp \
  --header "Authorization: Bearer $CARTROUTE_API_KEY"
```

## How do I add it to Cursor or another JSON-configured client?

```
{
  "mcpServers": {
    "cartroute": {
      "url": "https://cart-route.com/mcp",
      "headers": {
        "Authorization": "Bearer cr_live_..."
      }
    }
  }
}
```

## What if my client only supports local (stdio) servers?

Bridge it with `mcp-remote`, which runs locally and forwards to the remote server:

```
{
  "mcpServers": {
    "cartroute": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://cart-route.com/mcp",
        "--header",
        "Authorization:${CARTROUTE_AUTH}"
      ],
      "env": {
        "CARTROUTE_AUTH": "Bearer cr_live_..."
      }
    }
  }
}
```

## Which tools does the server expose?

| Tool | What it does |

| `search_products` | Search 12 US retailers (Amazon, Walmart, Best Buy, Costco, Target, Newegg, B&H, Home Depot, Lowe's, Sam's Club, Staples, Micro Center) in one call. Returns matched products, each with every retailer offer, landed price (price + shipping), the best applicable coupon, effective price after that coupon, and a 90-day price verdict. Natural-language price bounds in the query (&#34;under $400&#34;) are understood. Costs 1 credit. |
| `get_product` | Full product record: specs, every retailer offer, applicable coupons and price verdict. Accepts a product id (prd_...), slug, GTIN or MPN. Costs 1 credit. |
| `get_price_history` | Daily lowest price and per-retailer series for one product. Use it to judge whether today's price is a real deal. Costs 1 credit. |
| `find_coupons` | Active coupon codes and automatic promotions, with redemption success rate and sample size. Filter by retailer, category or product. Costs 1 credit. |
| `list_retailers` | Retailer ids, membership requirements, free-shipping thresholds, return windows and refresh intervals. Free. |
| `list_categories` | Category names with product counts, for use as the `category` filter. Free. |
| `get_account` | Remaining credits and whether the email-verification bonus is still available. Free; needs an API key. |

Every tool is read-only and annotated `readOnlyHint: true`, so clients that confirm side-effecting tools can run these without prompting. Full input schemas are returned by `tools/list` and published in the [server card](https://cart-route.com/.well-known/mcp.json).

## Which resources and prompts are there?

- `cartroute://retailers`, `cartroute://categories`, `cartroute://pricing`: reference data as JSON resources.

- Prompt `find_best_deal` (arguments `product`, optional `budget_usd`): instructs the model to search, compare on effective price, flag membership and eligibility requirements, and recommend one retailer with its reasoning.

## What does a raw call look like?

```
curl https://cart-route.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $CARTROUTE_API_KEY" \
  -d '{&#34;jsonrpc&#34;:&#34;2.0&#34;,&#34;id&#34;:1,&#34;method&#34;:&#34;tools/call&#34;,&#34;params&#34;:{&#34;name&#34;:&#34;search_products&#34;,&#34;arguments&#34;:{&#34;query&#34;:&#34;robot vacuum under $600&#34;,&#34;in_stock&#34;:true,&#34;limit&#34;:3}}}'
```

The result's `structuredContent` is the same envelope the REST search returns, and `content[0].text` carries it as JSON text for clients that only read text.

## What happens when a tool fails?

It returns a normal result with `isError: true` and a JSON body using the REST error `code` values (`insufficient_credits`, `validation_error`, `not_found`). A missing key produces an `isError` result that says how to get one, so the model can explain the next step to its user. See [errors](https://cart-route.com/docs/errors).

## Which protocol details should client authors know?

- Transport: Streamable HTTP, stateless. `POST` carries JSON-RPC; responses are `application/json`. `GET /mcp` returns 405 because the server opens no server-initiated streams.

- Protocol versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05. The server echoes the client's requested version when supported, otherwise the newest.

- Notifications (for example `notifications/initialized`) receive `202 Accepted` with no body. JSON-RPC batches are answered for older clients.

- Authentication: a bearer API key in the `Authorization` header. An invalid key returns HTTP 401 with a `WWW-Authenticate` header.

- Rate limits are shared with the REST API: 120 requests per minute per key.

---

# How do I give Claude a Cartroute search tool?

URL: https://cart-route.com/docs/guides/claude

Two ways: connect Claude Code or Claude Desktop to the [Cartroute MCP server](https://cart-route.com/docs/mcp) with no code, or, in your own application, define a `search_products` tool with the Anthropic Python SDK and let the SDK's tool runner call the Cartroute API whenever Claude decides to search. The example below is complete and runs as-is with two environment variables.

## What do I need?

```
pip install anthropic requests
export ANTHROPIC_API_KEY="sk-ant-..."
export CARTROUTE_API_KEY="cr_live_..."
```

## How do I define the tool with the tool runner?

The tool runner turns a decorated Python function into a tool, sends it to Claude, runs the function whenever Claude calls it, and loops until Claude has a final answer.

```
import json
import os

import anthropic
import requests
from anthropic import beta_tool

CARTROUTE = "https://cart-route.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CARTROUTE_API_KEY']}"}

client = anthropic.Anthropic()

@beta_tool
def search_products(query: str, max_price: float | None = None, in_stock: bool = True,
                    include_membership: bool = True) -> str:
    """Search 12 US retailers for a product and return every offer.

    Each offer has price_usd, shipping_usd, the best usable coupon and
    effective_price_usd (what the shopper pays before tax). Rank on
    effective_price_usd. price_insight.verdict says whether today's price is a
    90-day low.

    Args:
        query: What to find, in plain words, e.g. "sony noise cancelling headphones".
        max_price: Optional maximum effective price in USD.
        in_stock: Only return offers that are in stock.
        include_membership: Include Costco and Sam's Club, which need a paid membership.
    """
    params = {"q": query, "limit": 3, "in_stock": str(in_stock).lower(),
              "include_membership": str(include_membership).lower()}
    if max_price is not None:
        params["max_price"] = max_price
    resp = requests.get(f"{CARTROUTE}/search", params=params, headers=HEADERS, timeout=20)
    # Return errors to Claude as text so it can explain them (e.g. out of credits).
    return json.dumps(resp.json())

runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=16000,
    tools=[search_products],
    messages=[{
        "role": "user",
        "content": "Where should I buy Sony WH-1000XM6 headphones? I don't have a Costco membership.",
    }],
)

for message in runner:
    for block in message.content:
        if block.type == "text":
            print(block.text)
```

Claude reads the docstring as the tool description, so the facts that matter for ranking (effective price, membership) are stated there.

## How do I write the loop myself, with refusal fallbacks?

If you need full control, or want server-side refusal fallbacks on the request, use a manual loop. `fallbacks: "default"` lets the API re-run a request that Claude Opus 5's safety classifiers decline on Anthropic's recommended fallback model, inside the same call.

```
import json
import os

import anthropic
import requests

CARTROUTE = "https://cart-route.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CARTROUTE_API_KEY']}"}
client = anthropic.Anthropic()

tools = [{
    "name": "search_products",
    "description": (
        "Search 12 US retailers for a product. Returns every offer with "
        "effective_price_usd (price + shipping - best usable coupon). Rank on it."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "What to find, in plain words."},
            "max_price": {"type": "number", "description": "Maximum effective price in USD."},
            "include_membership": {"type": "boolean", "description": "Include Costco and Sam's Club."},
        },
        "required": ["query"],
    },
}]

def run_tool(name, args):
    if name != "search_products":
        return json.dumps({"error": f"unknown tool {name}"}), True
    params = {"q": args["query"], "limit": 3,
              "include_membership": str(args.get("include_membership", True)).lower()}
    if "max_price" in args:
        params["max_price"] = args["max_price"]
    resp = requests.get(f"{CARTROUTE}/search", params=params, headers=HEADERS, timeout=20)
    return json.dumps(resp.json()), not resp.ok

messages = [{"role": "user", "content": "Find me a 65 inch OLED TV under $2,000."}]

while True:
    response = client.beta.messages.create(
        model="claude-opus-5",
        max_tokens=16000,
        betas=["server-side-fallback-2026-07-01"],
        fallbacks="default",
        tools=tools,
        messages=messages,
    )
    if response.stop_reason == "refusal":
        print("The request was declined.")
        break
    if response.stop_reason != "tool_use":
        break

    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type == "tool_use":
            output, is_error = run_tool(block.name, block.input)
            results.append({"type": "tool_result", "tool_use_id": block.id,
                            "content": output, "is_error": is_error})
    # All results for one turn go back in a single user message.
    messages.append({"role": "user", "content": results})

print(next((b.text for b in response.content if b.type == "text"), ""))
```

## What should the tool return to Claude?

The Cartroute JSON as-is. It is compact, every money field is labelled `_usd`, and `price_insight.reason` is written to be quoted. On errors, return the problem body with `is_error: true`; its `detail` is written for the model to relay, for example that the account is out of credits.

## How do I keep costs down?

- Ask for `limit: 3`; the first results are the best matches.

- Keep the tool definitions and system prompt identical between requests so Anthropic's prompt caching can reuse them.

- Each tool call is one Cartroute credit; the [`/account`](https://cart-route.com/docs/api/account) endpoint is free if the agent should check its budget first.

---

# How do I use Cartroute from any function-calling framework?

URL: https://cart-route.com/docs/guides/function-calling

Any framework that supports function calling can use Cartroute: register the JSON Schema below as a tool, and when the model calls it, send the arguments to `GET /api/v1/search` and return the JSON response as the tool result. The OpenAPI 3.1 description at [`/openapi.json`](https://cart-route.com/openapi.json) can also be imported directly by frameworks that build tools from OpenAPI.

## What is the portable tool definition?

This schema is framework-neutral. Most SDKs accept it as the tool's parameters object; some call the field `parameters`, others `input_schema`.

```
{
  "name": "search_products",
  "description": "Search 12 US retailers (Amazon, Walmart, Best Buy, Costco, Target and more) for a product. Returns every offer with price_usd, shipping_usd, best_coupon and effective_price_usd (price + shipping - best usable coupon). Rank offers on effective_price_usd. price_insight.verdict says whether today's price is a 90-day low. Costco and Sam's Club need a paid membership.",
  "parameters": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "What to find, in plain words. Budgets like \"under $400\" are understood."
      },
      "max_price": {
        "type": "number",
        "description": "Maximum effective price in USD."
      },
      "in_stock": {
        "type": "boolean",
        "description": "Only in-stock offers. Default true."
      },
      "include_membership": {
        "type": "boolean",
        "description": "Include Costco and Sam's Club. Default true."
      },
      "retailers": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "amazon",
            "bhphoto",
            "bestbuy",
            "costco",
            "lowes",
            "microcenter",
            "newegg",
            "samsclub",
            "staples",
            "target",
            "homedepot",
            "walmart"
          ]
        },
        "description": "Limit to these retailers."
      }
    },
    "required": [
      "query"
    ]
  }
}
```

## What should the handler do?

```
import json
import os
import requests

def search_products(query, max_price=None, in_stock=True, include_membership=True, retailers=None):
    params = {"q": query, "limit": 3, "in_stock": str(in_stock).lower(),
              "include_membership": str(include_membership).lower()}
    if max_price is not None:
        params["max_price"] = max_price
    if retailers:
        params["retailers"] = ",".join(retailers)
    resp = requests.get(
        "https://cart-route.com/api/v1/search",
        params=params,
        headers={"Authorization": f"Bearer {os.environ['CARTROUTE_API_KEY']}"},
        timeout=20,
    )
    # Hand the body back either way: error bodies explain themselves to the model.
    return json.dumps(resp.json())
```

## Which other tools are worth adding?

| Tool | Endpoint | When the model should use it |

| `get_product` | `GET /products/{id}` | The user names an exact model, GTIN or a product from an earlier search. |

| `get_price_history` | `GET /products/{id}/price-history` | The user asks whether to buy now or wait. |

| `find_coupons` | `GET /coupons` | The user asks for codes at a specific store. |

## Is there a shortcut if my framework speaks MCP?

Yes. If the framework or runtime can connect to remote MCP servers, point it at [`https://cart-route.com/mcp`](https://cart-route.com/docs/mcp) and all seven tools arrive with their schemas, no handler code needed.

---

# How do I call Cartroute from Python?

URL: https://cart-route.com/docs/guides/python

Use `requests` (or `httpx`) with the key in an `Authorization` header, a timeout, an `Idempotency-Key` per logical request, and a retry loop that retries only rate limits, server errors and network failures. The client below does all of that in about 50 lines.

## What does a small production client look like?

```
import os
import time
import uuid

import requests

class CartrouteError(Exception):
    def __init__(self, status, problem):
        super().__init__(problem.get("detail", f"HTTP {status}"))
        self.status = status
        self.code = problem.get("code")
        self.problem = problem

class Cartroute:
    def __init__(self, api_key=None, base_url="https://cart-route.com/api/v1", timeout=15, max_retries=3):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.max_retries = max_retries
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {api_key or os.environ['CARTROUTE_API_KEY']}"

    def _get(self, path, params=None):
        key = str(uuid.uuid4())  # same key on every retry of this call
        for attempt in range(self.max_retries + 1):
            try:
                resp = self.session.get(f"{self.base_url}{path}", params=params,
                                        headers={"Idempotency-Key": key}, timeout=self.timeout)
            except requests.RequestException:
                if attempt == self.max_retries:
                    raise
                time.sleep(2 ** attempt)
                continue
            if resp.status_code == 429 and attempt < self.max_retries:
                time.sleep(int(resp.headers.get("Retry-After", "1")))
                continue
            if resp.status_code >= 500 and attempt < self.max_retries:
                time.sleep(2 ** attempt)
                continue
            body = resp.json()
            if not resp.ok:
                raise CartrouteError(resp.status_code, body)
            return body

    def search(self, q, **filters):
        return self._get("/search", {"q": q, **filters})

    def product(self, product_id):
        return self._get(f"/products/{product_id}")["product"]

    def price_history(self, product_id, days=90):
        return self._get(f"/products/{product_id}/price-history", {"days": days})["history"]

    def coupons(self, **filters):
        return self._get("/coupons", filters)["coupons"]

    def account(self):
        return self._get("/account")
```

## How do I use it?

```
cr = Cartroute()

try:
    result = cr.search("robot vacuum", max_price=600, in_stock="true", include_membership="false")
except CartrouteError as e:
    if e.code == "insufficient_credits":
        print("Out of credits:", e.problem.get("top_up_url"))
    else:
        raise
else:
    for p in result["results"]:
        best = p["price_summary"]
        print(f'{p["title"]}: {best["best_retailer"]} ${best["best_effective_price_usd"]:.2f}',
              "-", p["price_insight"]["verdict"])
    print("credits left:", result["credits"]["remaining"])
```

## Why these choices?

- One idempotency key per call, reused across retries, so a timeout followed by a retry is never charged twice.

- No retry on 4xx (except 429): the answer will not change.

- Booleans as strings in query parameters (`"true"`, `"false"`), because `requests` would send Python's `True`.

---

# How do I call Cartroute from TypeScript?

URL: https://cart-route.com/docs/guides/typescript

Use the built-in `fetch` (Node.js 18+, Deno, Bun, edge runtimes) with the key in an `Authorization` header, a timeout via `AbortSignal.timeout`, one `Idempotency-Key` per logical request and retries only for 429, 5xx and network errors. Types for the response come straight from the OpenAPI description.

## What does a small typed client look like?

```
type Offer = {
  offer_id: string;
  retailer: { id: string; name: string; membership_required: boolean };
  price_usd: number;
  shipping_usd: number;
  effective_price_usd: number;
  availability: { in_stock: boolean; stock_level: string };
  buy_url: string;
};
type Product = {
  id: string;
  title: string;
  price_summary: { best_retailer: string | null; best_effective_price_usd: number | null };
  price_insight: { verdict: string; reason: string };
  offers: Offer[];
};
type SearchResult = { results: Product[]; next_cursor: string | null; credits: { charged: number; remaining: number } };

export class CartrouteError extends Error {
  constructor(public status: number, public code: string, public problem: Record<string, unknown>) {
    super(String(problem.detail ?? `HTTP ${status}`));
  }
}

const BASE = "https://cart-route.com/api/v1";

async function get<T>(path: string, params: Record<string, string> = {}, retries = 3): Promise<T> {
  const url = new URL(BASE + path);
  Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  const idempotencyKey = crypto.randomUUID();
  for (let attempt = 0; ; attempt++) {
    let res: Response;
    try {
      res = await fetch(url, {
        headers: { Authorization: `Bearer ${process.env.CARTROUTE_API_KEY}`, "Idempotency-Key": idempotencyKey },
        signal: AbortSignal.timeout(15_000),
      });
    } catch (err) {
      if (attempt >= retries) throw err;
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
      continue;
    }
    if ((res.status === 429 || res.status >= 500) && attempt < retries) {
      const wait = res.status === 429 ? Number(res.headers.get("Retry-After") ?? 1) : 2 ** attempt;
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    const body = await res.json();
    if (!res.ok) throw new CartrouteError(res.status, body.code, body);
    return body as T;
  }
}

export const cartroute = {
  search: (q: string, filters: Record<string, string> = {}) => get<SearchResult>("/search", { q, ...filters }),
  product: (id: string) => get<{ product: Product }>(`/products/${encodeURIComponent(id)}`),
  account: () => get<{ credits_remaining: number }>("/account"),
};
```

## How do I use it?

```
const { results } = await cartroute.search("espresso machine", { max_price: "700", in_stock: "true" });
const cheapest = results[0]?.offers[0];
if (cheapest) {
  console.log(`${cheapest.retailer.name}: $${cheapest.effective_price_usd.toFixed(2)} → ${cheapest.buy_url}`);
}
```

## Can I generate the types instead?

Yes. Point any OpenAPI 3.1 code generator at `https://cart-route.com/openapi.json`. The hand-written types above cover the fields most agents use.

---

# How do I use Cartroute from n8n, Zapier or Make?

URL: https://cart-route.com/docs/guides/no-code

In n8n, Zapier or Make, add a generic HTTP request step: method `GET`, URL `https://cart-route.com/api/v1/search`, a query parameter `q`, and a header `Authorization` with the value `Bearer cr_live_...`. The JSON response can be mapped into later steps directly; the fields most workflows need are listed below.

## Which settings does the HTTP step need?

| Setting | Value |

| Method | `GET` |

| URL | `https://cart-route.com/api/v1/search` |

| Query parameters | `q` = the product, optionally `max_price`, `in_stock` = `true`, `limit` = `3` |

| Header | `Authorization` = `Bearer cr_live_...` (store it as a credential, not in the step text) |

| Response format | JSON |

## Which response fields should I map?

| Path | Meaning |

| `results[0].title` | Best-matching product. |

| `results[0].price_summary.best_retailer` | Where it is cheapest. |

| `results[0].price_summary.best_effective_price_usd` | What it costs there, shipping and coupon included. |

| `results[0].price_insight.verdict` | `good_time_to_buy`, `typical_price` or `above_recent_average`. |

| `results[0].offers[0].buy_url` | Link to buy. |

| `credits.remaining` | Balance, for a low-credit alert. |

## What is a useful first workflow?

A daily price-drop alert: a schedule trigger, the HTTP step with a fixed query, a filter that continues only when `price_insight.verdict` equals `good_time_to_buy`, and an email or Slack step with the retailer, price and `buy_url`. It costs one credit a day.

## How do I handle errors in a workflow?

Turn on the step's "continue on error" option and branch on the HTTP status: `402` means out of credits, `429` means retry later. The body's `detail` is readable enough to send as-is in a notification.

---

# How should an agent decide where to buy?

URL: https://cart-route.com/docs/guides/recommendations

Rank in-stock offers by `effective_price_usd`, drop members-only stores unless the shopper has that membership, treat coupons that need a code as conditional on their success rate, and use `price_insight.verdict` to say whether to buy now. Then recommend one retailer and state the numbers behind the choice. The procedure below turns a Cartroute response into a recommendation a shopper can trust.

## What is the decision procedure?

- Pick the product. If several results match, confirm the exact model with the user before comparing prices. Offers are matched by GTIN, so every offer under one product is the same item.

- Filter offers. Keep `availability.in_stock` offers. Drop `retailer.membership_required` unless the shopper has that membership (or search with `include_membership=false`). Drop `condition: "open_box"` unless the shopper accepts open-box.

- Rank on effective price. Sort by `effective_price_usd`, never `price_usd`: shipping and coupons change the winner on a meaningful share of products.

- Check the coupon. If the winner's `best_coupon.requires_code` is true, give the code and mention `success_rate` when it is below about 0.85. If the next offer is within that coupon's saving, it may be the safer pick.

- Check timing. `good_time_to_buy`: say so. `above_recent_average`: suggest waiting and quote `price_insight.reason`. `typical_price`: no strong signal either way.

- Check delivery and returns when the user cares: `delivery_days_max` and `retailer.returns_window_days` can justify a slightly higher price.

- Recommend one retailer, with the effective price, what it includes (shipping, coupon) and a `buy_url`.

## What does a good answer sound like?

"Buy it at Newegg for $343.99, shipping included and in stock (2–3 day delivery). That's the lowest price across 7 retailers and matches its 90-day low. Costco is $4 cheaper but needs a membership."

## Which mistakes should an agent avoid?

- Quoting `price_usd` as "the price" when shipping or a coupon changes the ranking.

- Recommending a Costco or Sam's Club offer to someone without a membership.

- Promising a coupon with `requires_eligibility: true`, such as a student discount.

- Presenting prices as current when `freshness_seconds` is large, or presenting `data_source: "sandbox"` prices as real.

- Calling the API once per retailer. One search already covers all 12.

## Is there a ready-made prompt?

Yes. The MCP server's `find_best_deal` prompt encodes this procedure; see [MCP server](https://cart-route.com/docs/mcp).

---

# Which machine-readable files does Cartroute publish?

URL: https://cart-route.com/docs/machine-readable

Cartroute publishes an OpenAPI 3.1 description, an MCP server card, an RFC 9727 API catalog, llms.txt and llms-full.txt, robots.txt, a sitemap and security.txt, and every response carries `Link` headers pointing to the API description. An agent that lands on any URL can discover the whole API from there.

## Which files are published?

| File | Format | Purpose |

| [`/openapi.json`](https://cart-route.com/openapi.json) | OpenAPI 3.1 | Every endpoint, parameter, schema and error. Import into code generators and tool builders. |

| [`/.well-known/mcp.json`](https://cart-route.com/.well-known/mcp.json) | JSON | MCP server card: endpoint, transport, protocol versions, authentication, tools. |

| [`/.well-known/api-catalog`](https://cart-route.com/.well-known/api-catalog) | RFC 9727 linkset | Points to the API's description, documentation and status. |

| [`/llms.txt`](https://cart-route.com/llms.txt) | Markdown | A short index for coding agents integrating the API. |

| [`/llms-full.txt`](https://cart-route.com/llms-full.txt) | Markdown | All of this documentation in one file. |

| [`/robots.txt`](https://cart-route.com/robots.txt) | robots.txt | Allows search, user-fetch and training crawlers everywhere except private account pages. |

| [`/sitemap.xml`](https://cart-route.com/sitemap.xml) | Sitemap | Every indexable URL, with last-modified dates. |

| [`/.well-known/security.txt`](https://cart-route.com/.well-known/security.txt) | RFC 9116 | Security contact. |

## Which Link headers are sent?

```
Link: </openapi.json>; rel="service-desc"; type="application/json"
Link: </docs>; rel="service-doc"; type="text/html"
Link: </.well-known/mcp.json>; rel="mcp-server"; type="application/json"
```

## Is structured data embedded in pages?

Yes, as server-rendered JSON-LD: `Organization` and `WebSite` on every page, `Product` with `AggregateOffer` on product pages, `ItemList` on category pages, `Article` on guides, `TechArticle` on docs, and `BreadcrumbList` throughout.

## Which crawlers are allowed?

All search and user-triggered AI fetchers (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, Googlebot, Bingbot, Applebot and others) and training crawlers are allowed on every public page. `/admin`, `/dashboard`, `/go/` and account paths are disallowed.

---

# How do I get help with the Cartroute API?

URL: https://cart-route.com/docs/support

Email [support@cart-route.com](mailto:support@cart-route.com) with the `request_id` from the response you are asking about. Live system status is at [/status](https://cart-route.com/status), API changes are in the [changelog](https://cart-route.com/changelog), and breaking changes only ship under a new dated API version.

## What should I include in a support request?

- The `request_id` (in every response body and in the `X-Request-Id` header).

- The endpoint, the parameters and the time in UTC.

- The error `code` if there was one. Never send your API key.

## How is the API versioned?

The current version is `2026-09-01`, reported as `api_version` in every metered response. Adding fields, endpoints, error codes or enum values is not a breaking change, so clients should ignore unknown fields. Removing or renaming a field, or changing its meaning, ships under a new dated version announced at least 90 days ahead.

## Where do I check whether something is down?

[/status](https://cart-route.com/status) shows the API, MCP server, website and price pipeline, with last-hour request counts, latency and server errors. [`/status?format=json`](https://cart-route.com/status?format=json) is suitable for monitoring.

## How do I report a security issue?

Email [security@cart-route.com](mailto:security@cart-route.com); see [security.txt](https://cart-route.com/.well-known/security.txt). Good-faith research that respects user privacy and service availability is welcome.

## Who do I talk to about volume pricing?

[sales@cart-route.com](mailto:sales@cart-route.com).
