Skip to main content

Octogen Python SDK

Async Python SDK for the Octogen AI commerce API.

Install For Development

From this directory:

uv sync

Authentication

The client reads the API key from OCTOGEN_PLATFORM_API_KEY by default — the same variable the Octogen agent-onboarding skill and CLI use — and sends it as:

Authorization: Bearer <api-key>

You can also pass api_key explicitly when constructing the client.

OCTO_API_KEY is still read as a deprecated fallback: it raises a DeprecationWarning once and will be removed. Set OCTOGEN_PLATFORM_API_KEY.

Usage

import asyncio

from octogen_ai_sdk import OctogenClient


async def main() -> None:
    async with OctogenClient(base_url="https://api.storerouter.ai/v1") as client:
        results = await client.search_products(q="paisley jackets", limit=3)
        for product in results.items:
            print(product.title, product.product_url)


asyncio.run(main())

Start with coverage

Octogen does not cover every merchant, and the coverage check is the cheapest call you can make. GET /v1/domains returns hosts normalized by the server — lowercased, with a leading www. stripped — so it reports macys.com and never www.macys.com. Real product URLs usually do carry www., so comparing a raw URL host against that list reports covered merchants as uncovered. fetch_domain_coverage normalizes both sides for you:

async with OctogenClient(base_url="https://api.storerouter.ai/v1") as client:
    coverage = await client.fetch_domain_coverage()

    url = "https://www.macys.com/shop/product/some-dress"
    if coverage.is_host_covered(url):
        found = await client.lookup_product(url)
        print(found.product.title)
    else:
        # Not covered yet — this is what `start_voyage` is for.
        started = await client.start_voyage(url)
        print(started.task.task_id, "dispatched" if started.created else "joined")

The endpoint is Cache-Control: max-age=300 behind a strong ETag, and clients are expected to revalidate rather than refetch. Pass the previous snapshot back and a 304 returns it untouched:

coverage = await client.fetch_domain_coverage()
# …later…
coverage = await client.fetch_domain_coverage(coverage)  # sends If-None-Match

print(len(coverage.hosts), "covered hosts")
print(coverage.catalogs_for("https://www.macys.com/x"))  # ("macys",)

Use list_domains(if_none_match=...) directly if you manage the cache yourself; it reports not_modified, etag, and max_age_seconds and leaves the decision to you. normalize_host and is_host_covered are exported for callers with their own storage.

API

Every published /v1 operation has a method. tests/contract fails the build if that stops being true — see Contract conformance.

  • get_me() returns the calling organization, key id and provenance, quotas, and rate-limit posture. Read-only and side-effect free: safe on startup and in CI.
  • list_domains(...) and fetch_domain_coverage(previous=None) return the covered hosts, with ETag revalidation and host normalization.
  • search_products(...) searches every active catalog by default, or only the catalogs in catalogs (1–500 keys) when it is provided. The query field is q; results come back as items + next_cursor, each item carrying product_url. catalog and text_search_query are deprecated (see below).
  • more_like_this_products(...) finds products similar to a source product URL or UUID, optionally restricted to catalogs (1–100 keys).
  • lookup_product(url, match_mode=..., resolution_mode=..., on_demand_cache_policy=...) resolves a product URL from the index or on demand. resolution_mode and on_demand_cache_policy default to auto and prefer_cache; omitting match_mode leaves the server on its own default (loose).
  • resolve_product_from_html(html=..., url=...) resolves a product from page HTML you already have — no index read, no outbound fetch.
  • start_voyage(domain), list_voyages(...), and get_voyage(task_id) build a catalog for a merchant Octogen does not cover yet. Voyages are shared per domain: StartVoyageResult.created is False when you joined one already running, which consumes no quota.
  • create_coverage_url_list(name=...), list_coverage_url_lists(...), get_coverage_url_list(url_list_id), and delete_coverage_url_list(url_list_id) manage coverage URL lists — named sets of product URLs that Octogen continuously joins against its crawled index and shares back as a BigQuery listing.
  • add_coverage_url_list_urls(url_list_id, urls=[...]), remove_coverage_url_list_urls(url_list_id, urls=[...]), check_coverage_url_list_urls(url_list_id, urls=[...]), and list_coverage_url_list_urls(url_list_id, ...) mutate and inspect a list's membership with per-URL accepted/rejected outcomes.
async with OctogenClient(base_url="https://api.storerouter.ai/v1") as client:
    similar = await client.more_like_this_products(
        source_url="https://warrenlotas.com/products/black-hoodie",
        price_preference="any",
        limit=12,
    )
    for product in similar.items:
        print(product.title, product.product_url)
async with OctogenClient(base_url="https://api.storerouter.ai/v1") as client:
    # Poll a voyage until its catalog is live. Voyages run for hours to days.
    started = await client.start_voyage("shop.example")
    progress = await client.get_voyage(started.task.task_id)
    print(progress.phase_label, progress.progress_percent)
async with OctogenClient(base_url="https://api.storerouter.ai/v1") as client:
    url_list = await client.create_coverage_url_list(name="q3-campaign")
    result = await client.add_coverage_url_list_urls(
        url_list.url_list_id,
        urls=["https://warrenlotas.com/products/black-hoodie"],
    )
    print(result.url_count, [r.code for r in result.rejected])

Narrowing to catalogs

Pass catalogs — a list of catalog keys — to restrict results. Omit it to search everything; an empty list is refused rather than sent.

async with OctogenClient(base_url="https://api.storerouter.ai/v1") as client:
    dresses = await client.search_products(catalogs=["macys", "jcrew"], q="linen dress")

Deprecated in 0.4.0, still sent as-is with a DeprecationWarning:

  • catalog="macys" on search_products → use catalogs=["macys"]. Passing both raises ValueError.
  • text_search_query=... on search_products → use q with facets, price_min, and price_max.

The API is retiring both fields on search, so they will be removed from a later SDK release. more_like_this_products already takes only catalogs: the endpoint stopped accepting catalog, so 0.4.0 removed the keyword.

Coverage URL lists CLI

octogen-url-lists manages coverage URL lists from the terminal, so the whole workflow — build a list, then subscribe to its BigQuery listing with octogen-bq-subscribe — stays on the command line. It uses OCTOGEN_PLATFORM_API_KEY (or --api-key), not Google credentials.

export OCTOGEN_PLATFORM_API_KEY=octo_live_...
uv run --project sdks/python octogen-url-lists create --base-url https://api.storerouter.ai/v1 --name q3-campaign --apply

Mutating commands are dry-run by default — matching octogen-bq-subscribe and octogen-bq-autosubscribe — so nothing changes until you pass --apply:

uv run --project sdks/python octogen-url-lists add-urls --base-url https://api.storerouter.ai/v1 cul_01... --file urls.txt
uv run --project sdks/python octogen-url-lists add-urls --base-url https://api.storerouter.ai/v1 cul_01... --file urls.txt --apply

--file reads one URL per line, skipping blank lines and # comments (pass - to read standard input), removes exact duplicates, and splits the result into requests of 1,000 URLs automatically. --url takes a single URL and can be repeated.

Command Purpose
create --name NAME Create a list
list Show every list, following pagination
get URL_LIST_ID Show one list, including its BigQuery listing and last export
urls URL_LIST_ID [--limit N] Print member URLs in insertion order
add-urls URL_LIST_ID Add URLs (batched)
remove-urls URL_LIST_ID Remove URLs (batched)
contains URL_LIST_ID Report which URLs are members
delete URL_LIST_ID Permanently delete the list and its BigQuery resources

Add --json to any command for machine-readable output; dry-run JSON uses the same keys as the applied output so automation can parse both with one schema.

Deletion is permanent and has no restore, so delete --apply first shows what the list holds and then asks you to retype its name. Pass --yes to skip that prompt; without a TTY, --yes is required rather than assumed.

Exit code Meaning
0 Success, or a dry run
1 The API call failed
2 Usage error, missing API key, or an unconfirmed delete
3 The change was applied but some URLs were rejected

Because a large file becomes several requests, a failure partway through leaves the earlier requests applied. In that case the command exits 1 but still reports how many URLs landed — completedRequests and urlCount in JSON, and a stopped after N/M request(s) line on stderr — so automation never reads a partial update as a no-op. Re-running the same input is safe: adds and removes are idempotent.

BigQuery subscribe (optional)

Once Octogen grants your organization access to a catalog's BigQuery Analytics Hub listing, subscribe to it — creating the linked dataset in your GCP project — without leaving the terminal. Install the extra and authenticate with Application Default Credentials:

pip install "octogen-ai-sdk[bigquery]"
gcloud auth application-default login

CLI (dry-run by default; --apply to subscribe):

octogen-bq-subscribe --listing <listing-resource> --project my-gcp-project --apply

Or programmatically:

from octogen_ai_sdk import subscribe_to_listing

result = subscribe_to_listing(
    listing_resource="projects/octogen-prod/locations/us/dataExchanges/oneoff/listings/farfetch",
    destination_project="my-gcp-project",
)
print(result.linked_dataset, result.state)

Copy <listing-resource> from the Platform UI BigQuery sharing page or from the Catalog Partner MCP list_bigquery_listing_resources tool. It's idempotent, and raises OctogenBigQueryAccessPendingError if Octogen's IAM grant hasn't landed yet (it's asynchronous — retry in a few minutes).

Auto-subscribe new listings

For cron jobs, use the higher-level autosubscribe command. It calls Octogen MCP to register your Reader if needed, finds every ready listing that is not active yet, creates linked datasets in your GCP project, then refreshes Octogen's status.

Bootstrap Octogen MCP credentials once from an interactive terminal:

uv run --project sdks/python octogen-mcp-login \
  --authkit-domain https://auth.storerouter.ai \
  --resource https://mcp.storerouter.ai \
  --client-id-file /secure/octogen-mcp.client-id \
  --refresh-token-file /secure/octogen-mcp.refresh

Dry-run first:

OCTOGEN_MCP_CLIENT_ID_FILE=/secure/octogen-mcp.client-id \
OCTOGEN_MCP_REFRESH_TOKEN_FILE=/secure/octogen-mcp.refresh \
uv run --project sdks/python --extra bigquery \
  octogen-bq-autosubscribe \
  --mcp-url https://mcp.storerouter.ai/mcp/ \
  --mcp-token-endpoint https://auth.storerouter.ai/oauth2/token \
  --project my-gcp-project \
  --principal serviceAccount:bq-reader@my-gcp-project.iam.gserviceaccount.com \
  --json

Apply from cron:

*/15 * * * * cd /path/to/octogen-dev && OCTOGEN_MCP_CLIENT_ID_FILE=/secure/octogen-mcp.client-id OCTOGEN_MCP_REFRESH_TOKEN_FILE=/secure/octogen-mcp.refresh uv run --project sdks/python --extra bigquery octogen-bq-autosubscribe --mcp-url https://mcp.storerouter.ai/mcp/ --mcp-token-endpoint https://auth.storerouter.ai/oauth2/token --project my-gcp-project --principal serviceAccount:bq-reader@my-gcp-project.iam.gserviceaccount.com --apply --json

OCTOGEN_MCP_REFRESH_TOKEN_FILE should contain an Octogen MCP OAuth refresh token. If WorkOS rotates that refresh token during exchange, the command writes the replacement back to the same file. The command also accepts OCTOGEN_MCP_CLIENT_ID instead of OCTOGEN_MCP_CLIENT_ID_FILE, OCTOGEN_MCP_TOKEN_COMMAND for custom token brokers, or OCTOGEN_MCP_ACCESS_TOKEN for short-lived manual runs. BigQuery subscription still uses your local Google Application Default Credentials.

Tests

Run the mocked SDK tests:

uv run pytest

Run linting:

uv run ruff check

Run type checking:

uv run ty check

Run all configured git hooks:

uv run --project sdks/python prek run --all-files

Example

From the repository root:

OCTOGEN_PLATFORM_API_KEY=... uv run --project sdks/python python examples/python/search_clothes.py

Generated versus hand-written

src/octogen_ai_sdk/generated/models.py is emitted from the published contract by npm run codegen (at the repository root) and committed, so a contract change arrives as a reviewable diff. Response models are re-exported from it under the contract's own names.

Request models stay hand-written in models.py, for two reasons: rules the contract cannot express (extra="forbid" catches a misspelled key, but not "exactly one of url or uuid"), and constructor ergonomics — the generator turns a length-constrained string into a root-model alias, so a generated request body would not accept a plain str.

operations.py is the routing table; tests/contract fails when it and the published contract disagree in either direction.

Metadata

Release files for octogen-ai-sdk 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for octogen-ai-sdk 0.4.0
File Size Uploaded
octogen_ai_sdk-0.4.0.tar.gz 63.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for octogen-ai-sdk 0.4.0
File Interpreter ABI Platform
octogen_ai_sdk-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 133.5 kB

Release files / octogen_ai_sdk-0.4.0.tar.gz

Download URL octogen_ai_sdk-0.4.0.tar.gz
Size 63.5 kB
Tags Source
SHA-256 checksum
How to use checksums
8be94882c8181ce6900bd85ac5904bbd4c8ef1425d1275448925383fbb4e6c82
BLAKE2b-256 checksum
How to use checksums
a13085935b4f22824a992d84efd11db1b4c91d7d6bdae9e451f6783aa48c4196
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / octogen_ai_sdk-0.4.0-py3-none-any.whl

Download URL octogen_ai_sdk-0.4.0-py3-none-any.whl
Size 70.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c82e31482d930c3bec17db7086f78e75a37419dfaab334619df0223047eb224a
BLAKE2b-256 checksum
How to use checksums
25f4739425518f4b3309a0618113ec18998e1527851e9a3fe77a4e6224d82934
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

2 release files

This release

0.4.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page