{
  "object": "doc",
  "title": "How do I call Cartroute from Python?",
  "description": "A small, production-ready client with retries and error handling.",
  "section": "Agents and integrations",
  "url": "https://cart-route.com/docs/guides/python",
  "updated": "2026-09-23",
  "prev": "https://cart-route.com/docs/guides/function-calling",
  "next": "https://cart-route.com/docs/guides/typescript",
  "text": "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.\n\r\n\r\n\n## What does a small production client look like?\n\r\n\n```\nimport os\r\nimport time\r\nimport uuid\r\n\r\nimport requests\r\n\r\nclass CartrouteError(Exception):\r\n    def __init__(self, status, problem):\r\n        super().__init__(problem.get(\"detail\", f\"HTTP {status}\"))\r\n        self.status = status\r\n        self.code = problem.get(\"code\")\r\n        self.problem = problem\r\n\r\n\r\nclass Cartroute:\r\n    def __init__(self, api_key=None, base_url=\"https://cart-route.com/api/v1\", timeout=15, max_retries=3):\r\n        self.base_url = base_url.rstrip(\"/\")\r\n        self.timeout = timeout\r\n        self.max_retries = max_retries\r\n        self.session = requests.Session()\r\n        self.session.headers[\"Authorization\"] = f\"Bearer {api_key or os.environ['CARTROUTE_API_KEY']}\"\r\n\r\n    def _get(self, path, params=None):\r\n        key = str(uuid.uuid4())  # same key on every retry of this call\r\n        for attempt in range(self.max_retries + 1):\r\n            try:\r\n                resp = self.session.get(f\"{self.base_url}{path}\", params=params,\r\n                                        headers={\"Idempotency-Key\": key}, timeout=self.timeout)\r\n            except requests.RequestException:\r\n                if attempt == self.max_retries:\r\n                    raise\r\n                time.sleep(2 ** attempt)\r\n                continue\r\n            if resp.status_code == 429 and attempt < self.max_retries:\r\n                time.sleep(int(resp.headers.get(\"Retry-After\", \"1\")))\r\n                continue\r\n            if resp.status_code >= 500 and attempt < self.max_retries:\r\n                time.sleep(2 ** attempt)\r\n                continue\r\n            body = resp.json()\r\n            if not resp.ok:\r\n                raise CartrouteError(resp.status_code, body)\r\n            return body\r\n\r\n    def search(self, q, **filters):\r\n        return self._get(\"/search\", {\"q\": q, **filters})\r\n\r\n    def product(self, product_id):\r\n        return self._get(f\"/products/{product_id}\")[\"product\"]\r\n\r\n    def price_history(self, product_id, days=90):\r\n        return self._get(f\"/products/{product_id}/price-history\", {\"days\": days})[\"history\"]\r\n\r\n    def coupons(self, **filters):\r\n        return self._get(\"/coupons\", filters)[\"coupons\"]\r\n\r\n    def account(self):\r\n        return self._get(\"/account\")\n```\n\r\n\r\n\n## How do I use it?\n\r\n\n```\ncr = Cartroute()\r\n\r\ntry:\r\n    result = cr.search(\"robot vacuum\", max_price=600, condition=\"new\", include_membership=\"false\")\r\nexcept CartrouteError as e:\r\n    if e.code == \"insufficient_credits\":\r\n        print(\"Out of credits:\", e.problem.get(\"top_up_url\"))\r\n    else:\r\n        raise\r\nelse:\r\n    for p in result[\"results\"]:\r\n        best = p[\"price_summary\"]\r\n        print(f'{p[\"title\"]}: {best[\"best_retailer\"]} ${best[\"best_effective_price_usd\"]:.2f}',\r\n              \"-\", p[\"price_insight\"][\"verdict\"])\r\n    print(\"credits left:\", result[\"credits\"][\"remaining\"])\n```\n\r\n\r\n\n## Why these choices?\n\r\n\r\n\n- One idempotency key per call, reused across retries, so a timeout followed by a retry is never charged twice.\r\n\n- No retry on 4xx (except 429): the answer will not change.\r\n\n- Booleans as strings in query parameters (`\"true\"`, `\"false\"`), because `requests` would send Python's `True`."
}