Metadata-Version: 2.5
Name: omnicommerce
Version: 0.14.0
Summary: Python SDK for the OmniCommerce API. Generated from the public OpenAPI spec.
Project-URL: Homepage, https://docs.omnicommerce.sg/sdk/python
Project-URL: Repository, https://github.com/rmdort/omnicommerce
License: MIT
License-File: LICENSE
Keywords: api,omni,omnicommerce,sdk
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Description-Content-Type: text/markdown

# omnicommerce

Python client for the [OmniCommerce API](https://docs.omnicommerce.sg/api-reference).

The client is generated from the public OpenAPI spec, the same operation map as `@omni-commerce/sdk`. Sync and async clients take snake_case keyword arguments. Responses are parsed JSON.

## Install

Install from PyPI:

```bash
python3 -m pip install omnicommerce
```

Python 3.10+.

## Usage

```python
import os
from omnicommerce import Omni

with Omni(api_key=os.environ["OMNI_API_KEY"]) as client:
    products = client.products.list(q="lamp")
    job = client.products.create(
        product={
            "sku": "SKU-100",
            "title": "Sample product",
            "price": 29.9,
            "currency": "SGD",
            "images": ["https://cdn.example.com/product.jpg"],
        }
    )
    created = client.jobs.wait(job_id=job["jobId"])
```

API keys are organization-scoped. Do not send `organization_id` on API-key requests.

```python
from omnicommerce import AsyncOmni, Omni, OmniEnvironment

client = Omni(
    api_key="omni_sk_...",
    environment=OmniEnvironment.LOCAL,  # http://localhost:3000
    timeout_in_seconds=60,
    max_retries=2,
)

async with AsyncOmni(api_key=os.environ["OMNI_API_KEY"]) as client:
    products = await client.products.list(q="lamp")
```

`client.with_raw_response.products.get(product_id="prod_123")` returns `data`, `status_code`, `headers`, and `url`.

## Review actions

Use `client.reviews.reply(review_id="review-uuid", reply="Thank you!")` to submit supplied text. Set `generate_with_ai=True` instead of `reply` for an AI-generated response; `submit=False` returns a draft, while `submit=True` sends it. Optional `instructions` guide the AI. The review ID goes in the URL; inspect `data.submitted` for confirmation. Both sync and async clients use this endpoint; the separate `draft_reply` and `actions` methods are removed.

## Batch market price comparison

Version 0.6.1 adds sync and async `products.compare_prices` and `products.get_price_comparison`. Submit up to 50 simple product or variant IDs with a `catalog:read` credential:

```python
queued = client.products.compare_prices(
    product_ids=["cc87d195-097a-4a5a-8229-cd989ce98d0f"],
)
client.jobs.wait(job_id=queued["jobId"], timeout_in_seconds=1800)
completed = client.products.get_price_comparison(job_id=queued["jobId"])
print(completed["result"]["success"], completed["result"]["results"])
```

Omit `gl` to compare all recorded listing markets; `gl="sg"` restricts research to Singapore. Global Zalora prices stay separate by country/currency. An India Shopify store selling in SGD is researched in Singapore. Read each product's `markets` array for price evidence, research, comparison, and errors. Missing published price evidence is an explicit failure; a catalog fallback is never a verified listing price. A wait timeout leaves the durable job running. [Full comparison guide](https://docs.omnicommerce.sg/sdk/python#market-price-comparison).

## Order fulfillment and picking lists

Available in SDK 0.13.0. Read choices and submit reviewed operations for 1–50 distinct orders. Reading choices and submitting require `orders:write`; job status and picking lists require `orders:read`. `AsyncOmni` exposes the same methods with `await`.

```python
from uuid import uuid4
from omnicommerce import RequestOptions

options = client.orders.list_batch_action_options(order_ids=order_ids, action="ship")
# Review choices and supply required fields in reviewed_operations:
# [{"orderId": ..., "action": "ship", "payload": chosen_payload}]
request_id = str(uuid4())
queued = client.orders.process_actions(request_id=request_id, operations=reviewed_operations)
progress = client.orders.get_action_jobs(job_ids=queued["data"]["jobIds"])
for job in progress["data"]["results"]:
    for label in job["labels"]:
        print(label["url"])
picking = client.orders.create_picking_list(
    order_ids=order_ids,
    time_zone="Asia/Singapore",
    request_options=RequestOptions(timeout_in_seconds=120),
)
print(picking["data"]["downloadUrl"], picking["data"]["expiresAt"])
return_requests = client.returns.list_for_orders(order_ids=order_ids)
```

Operations support `ship`, `cancel`, `labels`, and `update_tracking`; nested keys retain their JSON names. Generate labels after shipment completes when required. Check all returned job IDs with backoff while `pendingCount` is positive. Submission success means accepted; job `data.success` means every requested job succeeded. Preserve the request key and unchanged operations for retries. Unsupported marketplace actions remain explicit per-order failures. Picking lists accept up to 500 orders and return a private PDF link valid for five minutes; missing or inaccessible orders fail the entire list. The API generates files, not physical printer jobs. See [Order fulfillment](https://docs.omnicommerce.sg/guides/order-fulfillment).

## Return processing

Available in SDK 0.13.0. Use one submission for a single return or up to 50 reviewed decisions. Checking and submitting require `orders:write`; job status requires `orders:read`. `AsyncOmni` exposes the same methods with `await`.

```python
from uuid import uuid4

checked = client.returns.list_actions(return_ids=return_ids)
# selected_decisions comes from your review UI: [{"returnId": ..., "actionId": ...}].
by_id = {item["returnId"]: item for item in checked["data"]["results"]}
operations = []
for selected in selected_decisions:
    result = by_id[selected["returnId"]]
    if not result["success"] or not any(
        action["id"] == selected["actionId"] for action in result["actions"]
    ):
        raise ValueError("Refresh and review this return before processing it")
    operations.append({**selected, "version": result["version"]})
request_id = str(uuid4())  # retain this key for safe retries
queued = client.returns.process(request_id=request_id, operations=operations)
progress = client.returns.get_action_jobs(job_ids=queued["data"]["jobIds"])
print(progress["data"])
```

Nested operation keys retain their JSON wire names (`returnId`, `actionId`, `version`). An empty actions array means no current seller decision, including completed or cancelled returns. Read `limitation` for unsupported operations. Retry transport failures with the same key and operations. Check **every** returned job ID with backoff until `pendingCount` is zero; only `data.success: true` confirms all selected decisions succeeded. `needs_review` requires checking marketplace state before another decision. See [Return processing](https://docs.omnicommerce.sg/guides/return-processing).

## Batch publish, delist, and update

```python
published = client.products.batch_publish(
    product_ids=["prod_123", "prod_456"], visibility="live", concurrency=3,
)
delisted = client.products.batch_delist(product_ids=["prod_123", "prod_456"])
updated = client.products.batch_update(updates=[
    {"product_id": "prod_123", "name": "Desk lamp"},
    {"product_id": "prod_456", "price": 29.9, "currency": "SGD"},
])
```

Both sync and async clients process 1–100 products through the existing single-product APIs, with concurrency 1–10 (default 3). Publish/delist trim and deduplicate IDs; updates reject duplicate product IDs. Results include ordered per-product responses/errors, success/failure counts, and every returned handle in `jobIds`. Await those jobs separately. All marketplaces and selected-field publish options are supported. `batch_update` edits the catalog; use `batch_publish` with `update_fields` to send saved fields to existing listings. Submission requires the SDK process to stay running; queued server jobs continue independently. [Batch guide](https://docs.omnicommerce.sg/sdk/python#batch-publish-delist-and-update).

## Publish selected fields

Choose marketplaces and publish only saved fields on existing listings, matching the publish popover:

```python
publish = client.products.publish(
    product_id="prod_123",
    marketplaces=["shopee", "lazada"],
    update_fields=["name", "description", "images"],
)
```

Omit marketplaces to use every enabled or listed marketplace; omit fields for full publishing. Selected-field updates preserve other fields and listing visibility. Inspect every target result for capability/connection errors and await every returned job ID and inventory correlation. See the [publishing guide](https://docs.omnicommerce.sg/sdk/python#publish-selected-fields).

## Product counts and resumable scans

Use `products.count()` for a total instead of downloading the catalog. It uses the Products page filters and counts catalog parent products once. It does not count variants or verify live marketplace listing status. `status` selects the catalog lifecycle/derived view. All supported marketplaces are accepted. Combine `marketplace` with `store`, or pass exact `marketplace:country:storeId` keys using `store_keys`.

```python
filters = {"marketplace": "lazada", "store": "actual store ID", "status": "active"}
count = client.products.count(**filters)
print(count["count"], count["unit"])  # catalog_products

def checkpoint(progress, items):
    # Persist this page's items and progress["next"] together.
    print(progress["pagesRead"], progress["itemsRead"], progress["complete"], flush=True)

chunk = client.products.scan(**filters, max_pages=1, on_progress=checkpoint)
# A later invocation can resume with client.products.scan(**chunk["next"])
# Only resume when chunk["complete"] is False and chunk["next"] is not None.
```

Sync `Omni` and `AsyncOmni` expose both methods; async calls use `await`, and async progress callbacks are awaited. Scans default to one page (up to 50 products), allow 1–100 pages per chunk, and request `include_summary=False` to skip repeated summary classification. Results contain `items`, `pagesRead`, `itemsRead`, `complete`, and `next`. Counts are per chunk. Unlike API response keys, `next` contains SDK keyword arguments in snake_case and can be passed unchanged with `**`.

Save each successful page through `on_progress`; a capped chunk stays explicitly incomplete. `ProductScanError.partial` retains completed items and the first unprocessed request, with the original error in `__cause__`. Never label a failed/capped scan as complete. Continuations preserve filters and use a server cursor when supported (`created_desc` without search), otherwise an offset. Resume with the same organization and filters; changing records can affect offset paging, and scans are not immutable snapshots. Persist records and continuation together, without credentials. Normal per-request timeout/retry options still apply.

For manual paging, `products.list(**filters, include_summary=False)` omits `summary` and returns `pagination.total=None`. Call `products.count(**filters)` separately if needed. Default list behavior keeps the summary and total.

## Resources

| Client                         | Examples                                                                        |
| ------------------------------ | ------------------------------------------------------------------------------- |
| `client.products`              | `list`, `create`, `get`, `update`, `delete`, `publish`, `bulk_import`, `enrich` |
| `client.orders`                | `list`, `create`, `get`, `update`                                               |
| `client.promotions`            | `list`, `create`, `get`, `update`, `delete`, `sync`                             |
| `client.price_books`           | `list`, `create`, `get`, `update`, `delete`, `preview`                          |
| `client.aop`                   | `create`, `execute`, `get_config`, `update_config`, `retry`                     |
| `client.jobs`                  | `get`, `cancel`, `wait`                                                         |
| `client.webhooks`              | `list`, `create`                                                                |
| `client.checkout_sessions`     | `create`, `get`, `update`, `complete`, `cancel`                                 |
| `client.ucp.checkout_sessions` | Universal Commerce Protocol checkout                                            |
| `client.oauth`                 | `token`, `revoke`                                                               |

Every other documented tag is exposed the same way.

## Errors

```python
from omnicommerce import NotFoundError, Omni, OmniError

try:
    client.products.get(product_id="missing")
except NotFoundError:
    pass
except OmniError as error:
    print(error.status_code, error.body)
```

## Webhooks

```python
from omnicommerce import construct_event

event = construct_event(raw_body, headers, os.environ["OMNI_WEBHOOK_SECRET"])
```

Signature: HMAC-SHA256 over `{timestamp}.{raw_body}` compared to `X-Omni-Signature`.

## Regenerate from OpenAPI

From the monorepo root:

```bash
yarn sdk:generate
```

That refreshes `src/omnicommerce/_generated/resources.py` from `docs/openapi.json`.

## Commerce jobs and verification

SDK 0.14.0 adds the shared waiter and fresh state verifier, also available on AsyncOmni. Pass every returned order/return correlation together.

```python
correlations = submitted["data"]["correlations"]
progress = client.commerce.await_jobs(correlations=correlations, timeout_ms=30000)
verified = client.commerce.verify_state(correlations=correlations)
```

Inspect every result and pending/failed counts. Acceptance alone does not establish marketplace state; uncertain outcomes must not be automatically resubmitted. See [Commerce verification](https://docs.omnicommerce.sg/guides/commerce-verification).
