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