Skip to content
cresvaDevelopers

A Python quickstart

GET/api/storefront/sdk/python
No keyCallable anonymously. Rate limited by IP.10 requests a minute

Request

bash
curl "https://cresva.ai/api/storefront/sdk/python"

Response

Captured from production on 2026-09-16: 200 in 1433ms. This is what the endpoint returned, not an example of what it might.

JSON
"""
Cresva Storefront SDK for Python
pip install requests

Usage:
    from cresva import CresvaClient
    client = CresvaClient(api_key="pk_live_...", brand_id="your-brand-id")
    results = client.search("organic cotton tshirt")
"""
import requests
import uuid
from datetime import datetime, timezone
from typing import Optional, Dict, List, Any


class CresvaError(Exception):
    """Base error for Cresva API responses."""

    def __init__(self, status_code: int, message: str, request_id: str = None):
        self.status_code = status_code
        self.request_id = request_id
        super().__init__(message)


class RateLimitError(CresvaError):
    """Raised when rate-limited (HTTP 429)."""

    def __init__(self, retry_after: int, request_id: str = None):
        self.retry_after = retry_after
        super().__init__(429, f"Rate limited. Retry after {retry_after} seconds.", request_id)


class AuthError(CresvaError):
    """Raised on authentication failure (HTTP 401)."""

    def __init__(self, message: str = "Invalid or missing API key."):
        super().__init__(401, message)


class CresvaClient:
    """Cresva Storefront API client.

    Args:
        api_key: Your Cresva API key (pk_live_, sk_live_, pk_test_, or sk_test_).
        brand_id: The brand identifier for your storefront.
        base_url: Override the default API base URL.
        timeout: Request timeout in seconds (default 30).
    """

    def __init__(
        self,
        api_key: str,
        brand_id: str,
        base_url: str = "https://cresva.ai",
        timeout: int = 30,
    ):
        if not api_key:
            raise ValueError("api_key is required")
        if not brand_id:
            raise ValueError("brand_id is required")

        self.api_key = api_key
        self.brand_id = brand_id
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    @property
    def _storefront_base(self) -> str:
        return f"{self.base_url}/api/storefront/{self.brand_id}"

    def _request(
        self,
        path: str,
        method: str = "GET",
        json_body: Optional[dict] = None,
        retries: int = 0,
    ) -> dict:
        url = f"{self._storefront_base}{path}"
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }

        r = requests.request(
            method, url, headers=headers, json=json_body, timeout=self.timeout
        )

        if r.status_code == 429:
            retry_after = int(r.headers.get("Retry-After", 60))
            if retries < 2:
                import time
                time.sleep(retry_after)
                return self._request(path, method, json_body, retries + 1)
            raise RateLimitError(retry_after)

        if r.status_code == 401:
            raise AuthError()

        if not r.ok:
            body = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
            raise CresvaError(
                r.status_code,
                body.get("error", f"HTTP {r.status_code}"),
                body.get("request_id"),
            )

        return r.json()

    def search(self, query: str, limit: int = None) -> dict:
        """Search products by natural language query.

        Returns {"query", "products", "meta"}. For filtered search
        (price, category, stock) use query() with query.filters:
        the /search endpoint accepts only q and limit.

        Args:
            limit: Max results (1 to 30, default 10).
        """
        params = [f"q={requests.utils.quote(query)}"]
        if limit is not None:
            params.append(f"limit={limit}")
        return self._request(f"/search?{'&'.join(params)}")

    def get_product(self, product_id: str) -> dict:
        """Get full product details by ID."""
        res = self._request(f"/products/{product_id}")
        return res.get("product", res)

    def list_products(self, limit: int = 20, page: int = 1) ->
… truncated at 4000 characters

Response codes

200Text.
string
401The key does not match any active key.
erroranyTwo shapes exist across this API and that is deliberate rather than untidy. Each route kept the error shape it already used, so an existing client's error handling keeps working. See x-cresva-error-shapes.
403The key is valid but belongs to a different brand than the one in the path. Keys are scoped to one brand and do not travel.
erroranyTwo shapes exist across this API and that is deliberate rather than untidy. Each route kept the error shape it already used, so an existing client's error handling keeps working. See x-cresva-error-shapes.
404No such brand.
erroranyTwo shapes exist across this API and that is deliberate rather than untidy. Each route kept the error shape it already used, so an existing client's error handling keeps working. See x-cresva-error-shapes.
429Over the rate limit.
erroranyTwo shapes exist across this API and that is deliberate rather than untidy. Each route kept the error shape it already used, so an existing client's error handling keeps working. See x-cresva-error-shapes.
503The rate limiter could not be reached, so the request was refused rather than served unmetered. Deliberately not a 429: the caller has done nothing wrong and the fault is ours.
erroranyTwo shapes exist across this API and that is deliberate rather than untidy. Each route kept the error shape it already used, so an existing client's error handling keeps working. See x-cresva-error-shapes.

Generated from the storefront OpenAPI document at growthagents 269d7898b, sha256 047fe4d301258100. Nothing on this page was typed by hand.