Python SDK
The official Python client for Crossmart lets you manage your catalog and stock, publish products to your Shopify, eBay and Google Merchant Center stores, set per-store prices and overrides, read orders and sales, and work with Crossmart’s AI writing tools from Python scripts and AI agents.
- Typed end to end: models and enums are generated from the API’s OpenAPI document, and the client passes
mypy --strict. - Synchronous (
Crossmart) and asynchronous (AsyncCrossmart) clients built on httpx. - Automatic pagination, typed exceptions, and retries with backoff that respect
Retry-After.
Requires Python 3.11 or later.
Install
Section titled “Install”-
Clone the repository:
Terminal window git clone https://github.com/crossmart-io/crossmart.git -
Install the SDK from its folder, preferably in a virtual environment:
Terminal window python -m venv .venv && source .venv/bin/activatepip install ./crossmart/sdks/python
Once it is published, installing will be:
pip install crossmartAuthentication
Section titled “Authentication”Create an API key in the Crossmart web app under Settings → API keys. The secret starts with
cm_ and is shown only once. A key belongs to one workspace and has a role:
| Role | Can |
|---|---|
| Admin | Everything below, plus manage stores, pricing rules, members and keys |
| Editor | Read everything; create and edit products and stock; publish and remove listings; ask the AI to write |
| Approver | Read everything; approve or reject AI suggestions |
| Viewer | Read everything |
Keys can’t be Owners, and a key never has more access than the member who created it. Custom roles work too. See Roles and permissions for each permission.
from crossmart import Crossmart
client = Crossmart(api_key="cm_...")Every setting can come from the environment instead:
| Argument | Environment variable | Default |
|---|---|---|
api_key |
CROSSMART_API_KEY |
|
access_token |
CROSSMART_ACCESS_TOKEN |
|
base_url |
CROSSMART_BASE_URL |
https://api.crossmart.io |
workspace_id |
CROSSMART_WORKSPACE_ID |
the API key’s workspace |
With an API key you don’t need workspace_id: the client asks the API which workspace the key
belongs to (GET /api/v1/api-key) the first time it needs it.
AI agents that connect to Crossmart through OAuth (coming soon) get an access token instead of an
API key. Pass it as access_token, together with workspace_id; the client sends it as
Authorization: Bearer <token>.
A self-hosted Crossmart works the same way: set base_url to your API’s address, with or without
/api/v1.
Quick start
Section titled “Quick start”from crossmart import Crossmartfrom crossmart.models import CreateProductDto, CreateVariantDto, OrderStatus, ProductStatus
with Crossmart() as client: # reads CROSSMART_API_KEY # List products, one page at a time... page = client.products.list(status=ProductStatus.ACTIVE, limit=20) print(page.total, "active products")
# ...or iterate over all of them; pages are fetched as you go. for product in client.products.iter(search="apron"): print(product.id, product.title)
# Create a product with one variant and opening stock. created = client.products.create( CreateProductDto( title="Linen apron", status=ProductStatus.ACTIVE, variants=[CreateVariantDto(sku="APR-NAT", price="24.00", quantity=15)], ) )
# Publish it to a connected store. store = client.stores.list()[0] listing = client.listings.publish(created.id, store.id) print(listing.connection.display_name, listing.listing.status if listing.listing else "queued")
# Read open orders across every store. for order in client.orders.iter(status=OrderStatus.OPEN): print(order.name, order.store_name, order.total, order.currency_code)Request bodies accept either a generated model or a plain mapping in the API’s JSON shape (camelCase keys), which is handy for agents that build requests as JSON:
client.products.create({"title": "Linen apron", "variants": [{"price": "24.00"}]})Resources
Section titled “Resources”| Attribute | Methods |
|---|---|
products |
list, iter, get, create, update (replaces the product; a variant without quantity keeps its stock), delete |
inventory |
list, iter, adjust, movements, iter_movements |
stores |
list, get, set_pricing, preview_pricing, sync, list_products, iter_products, list_listings, iter_listings |
listings |
for_product, publish, retry, remove, set_override, clear_override, publish_many, remove_many |
orders |
list, iter, get |
reports |
sales, inventory |
ai |
status, usage, generate_content, draft_content |
suggestions |
list, iter, get, approve, reject, approve_many |
workspaces |
get, update, current_api_key |
Models and enums live in crossmart.models. A few more examples:
from crossmart.models import ( InventoryMovementReason, ListingOverrideDto, PriceRounding, CompareAtMode, PricingRuleDto, SalesRange, StockLevel, SuggestionStatus,)
# Restock everything that runs low.for item in client.inventory.iter(stock_level=StockLevel.LOW_STOCK): client.inventory.adjust(item.inventory_item_id, 10, InventoryMovementReason.RECEIVED)
# Price one store 10% above the catalog, ending in .99. `None` removes the rule.client.stores.set_pricing( store_id, PricingRuleDto( adjustment_percent="10", fixed_amount="0", rounding=PriceRounding.ENDING_99, floor=None, compare_at=CompareAtMode.KEEP, ),)
# Show a different title in one store.client.listings.set_override( product_id, store_id, ListingOverrideDto(title="Linen apron | Free shipping"))
# Sales of the last 7 days, bucketed in a time zone.report = client.reports.sales(range_=SalesRange.LAST_7_DAYS, time_zone="America/New_York")print(report.current.sales, report.currency_code)
# Ask the AI to write store-specific titles and descriptions, then review them.client.ai.generate_content(store_id, [product_id])for suggestion in client.suggestions.iter(status=SuggestionStatus.PENDING): print(suggestion.current_value, "->", suggestion.proposed_value) client.suggestions.approve(suggestion.id)Async usage
Section titled “Async usage”AsyncCrossmart takes the same arguments and has the same resources. Methods are coroutines, and
iter methods return async iterators:
import asyncio
from crossmart import AsyncCrossmart
async def main() -> None: async with AsyncCrossmart() as client: stores = await client.stores.list() async for product in client.products.iter(): print(product.title)
asyncio.run(main())Pagination
Section titled “Pagination”List endpoints use cursors. list() returns one Page with items, next_cursor, total (how
many match across all pages) and has_more. Pass next_cursor back as cursor to get the next
page. iter() does that for you and yields items lazily. limit sets the page size (at most 100).
page = client.orders.list(limit=50)while True: for order in page.items: ... if not page.has_more: break page = client.orders.list(limit=50, cursor=page.next_cursor)Errors
Section titled “Errors”Every error status raises a CrossmartAPIError with status, message, code, error, body
and headers. Subclasses let you catch the cases you handle differently:
| Exception | Status | When |
|---|---|---|
BadRequestError |
400 | Validation failed; message joins the API’s messages with "; " |
AuthenticationError |
401 | The key or token is missing, wrong, revoked or expired |
PlanLimitError |
402 | The plan doesn’t allow it; see code, feature, limit, usage |
PermissionDeniedError |
403 | The key’s role lacks the permission |
NotFoundError |
404 | Not found, or not in the key’s workspace |
ConflictError |
409 | Conflicts with the current state, such as a suggestion already reviewed |
RateLimitError |
429 | Too many requests, after retries |
ServerError |
5xx | The API or an upstream provider failed |
CrossmartConnectionError means the API could not be reached, and CrossmartConfigurationError
means the client is missing credentials or a workspace. All of them derive from CrossmartError.
from crossmart import PlanLimitErrorfrom crossmart.models import EntitlementErrorCode
try: client.listings.publish(product_id, store_id)except PlanLimitError as error: if error.code == EntitlementErrorCode.PLAN_LIMIT_REACHED: print(f"Your {error.plan} plan allows {error.limit} {error.feature}. Upgrade to add more.")Retries and timeouts
Section titled “Retries and timeouts”Idempotent requests (GET, PUT, DELETE) are retried twice by default after a 429, a 5xx or a
refused connection, with exponential backoff and jitter. When the API sends Retry-After, the
client waits that long (at most 60 seconds). POST requests are never retried, so nothing is
created or published twice.
client = Crossmart(max_retries=5, timeout=60.0) # max_retries=0 turns retries offYou can pass your own httpx transport, for example for a proxy or for tests:
Crossmart(transport=httpx.HTTPTransport(proxy="http://proxy:8080")).
Contributing
Section titled “Contributing”The SDK lives in the Crossmart repository. Its models and low-level client are generated from the API’s OpenAPI document with openapi-python-client, and CI fails when the committed code is out of date. See the SDK’s README there for how to regenerate it and run its checks.
License
Section titled “License”AGPL-3.0, like the rest of Crossmart.