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(...)andfetch_domain_coverage(previous=None)return the covered hosts, withETagrevalidation and host normalization.search_products(...)searches every active catalog by default, or only the catalogs incatalogs(1–500 keys) when it is provided. The query field isq, understood by default;understand,facets,exclude_facets, the price and brand-quality bounds,relax, anddebugrefine it (see below). Results come back asitems+next_cursor, each item carryingproduct_url, pluseffective_querywithdebug=True.more_like_this_products(...)finds products similar to a source product URL or UUID, optionally restricted tocatalogs(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_modeandon_demand_cache_policydefault toautoandprefer_cache; omittingmatch_modeleaves 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(...), andget_voyage(task_id)build a catalog for a merchant Octogen does not cover yet. Voyages are shared per domain:StartVoyageResult.createdisFalsewhen 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), anddelete_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=[...]), andlist_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])
Search controls
Each search_products keyword controls one thing. q says what to find and is
understood by default — rewritten for retrieval, with category, colour, brand,
price, and attribute filters derived from it. facets, exclude_facets, and
the bounds say what to keep; catalogs says where to look.
async with OctogenClient(base_url="https://api.storerouter.ai/v1") as client:
page = await client.search_products(
q="black hoodie",
catalogs=["warrenlotas"], # omit to search every catalog your key can access
facets=[{"name": "color", "values": ["black"]}],
exclude_facets=[{"name": "brand_name", "values": ["acme"]}],
price_min=50,
price_max=300,
brand_quality_min=4, # 1 (very poor) to 6 (exceptional)
similar_to_brands=["Warren Lotas"], # ranks, does not filter
relax=True, # drop understood filters in stages if nothing matches
debug=True,
)
if page.effective_query is not None:
for facet in page.effective_query.facets or []:
print(facet.name, facet.values, facet.source) # "caller" | "generated"
understand=Falsematchesqas written and filters only by what you send.omit_generated_facets=["fit"]drops an understood facet that is wrong when you have no value of your own for it; to replace one, send a facet of that name instead. Use the nameseffective_queryreports.debug=Trueaddseffective_query, the search that actually ran.
Contradictions the API would refuse — relax or understand=False without a
q, a facet name sent twice, price_min above price_max — raise
pydantic.ValidationError before any request.
Removed in 0.5.0 (the API now answers them with 422): catalog= → use
catalogs=[key]; text_search_query= → use q with understand=False and
your filters in facets and exclude_facets. See the
CHANGELOG for the migration.
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.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| octogen_ai_sdk-0.5.0.tar.gz | 64.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| octogen_ai_sdk-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 135.1 kB
Release files / octogen_ai_sdk-0.5.0.tar.gz
| Download URL | octogen_ai_sdk-0.5.0.tar.gz |
|---|---|
| Size | 64.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a67b6b840a7e53a7889d7336e974bfdfb38121fba73d5fec845c6c4636a9cb2
|
|
BLAKE2b-256 checksum How to use checksums |
e03cf55bb09393080cccdf598bc22522141a0346c254148eea1321bb443dd5af
|
| 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 logRelease files / octogen_ai_sdk-0.5.0-py3-none-any.whl
| Download URL | octogen_ai_sdk-0.5.0-py3-none-any.whl |
|---|---|
| Size | 70.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0aa39102f87a739d66a4951f509caa97abf47919ae7e70e9dcc28a65404dbc23
|
|
BLAKE2b-256 checksum How to use checksums |
54d69b3b2150baa4f0edc8cb0a531ac35fcb85890015e6abc15fa0f3aa3680a1
|
| 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