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())
Product lookup
Look up a product URL directly. A missing product does not prove that its whole
merchant domain is unsupported. Use get_me() to check credentials.
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. -
get_domain(url_or_host)returns one store's coverage snapshot (GET /v1/domains/{host}): every catalog serving the host, with product counts, populated facets and a price histogram. A URL is normalized to its host first. An uncovered host raisesOctogenNotFoundErrorwithdetail == "domain_not_covered". There is no list of covered hosts. -
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, or to a set of 1–20 product UUIDs passed assources, optionally restricted tocatalogs(1–100 keys).facets,exclude_facetsand the price and brand-quality bounds work as on search.relaxdefaults toTrueon the server: a short page 1 is retried with Platform's generated facets dropped, never yours, and reported ineffective_query.relaxationwithdebug=True.facetsreplacedinclude_facetsin 0.8.0.omit_generated_facetsand the embedding-column lists tune what is generated and how candidates are retrieved and ranked. -
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. -
list_taxonomy_categories(parent=...),list_taxonomy_attributes(category)andlist_taxonomy_attribute_values(handle)discover categories, their attributes and the attributes' value handles, paged withoffset/limit. A category'sfacetsand an attribute'sfacet_nameplus valuehandlego back intosearch_productsunchanged. -
count_product_facets(...)counts the productssearch_productswould filter to — bycategories,attributesandfacet_names— under the sameq, facets, bounds andcatalogs. -
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:
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])
async with OctogenClient() as client:
children = await client.list_taxonomy_categories(
parent="Apparel & Accessories > Clothing"
)
dresses = next(c for c in children.categories if c.name == "Dresses")
counts = await client.count_product_facets(
q="red",
categories=[{"key": dresses.id, "facets": dresses.facets}],
attributes=["neckline"],
)
print(counts.categories[0].count, counts.attributes[0].values[:3])
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.
Prebuilt requests, timeouts, and errors
Every operation with a request body also takes request=, a prebuilt request
model sent as is. A subclass's extra fields go out too, which bridges a field
the API accepts before this SDK declares it. Passing request= together with
the keyword form is a TypeError.
class MySearchRequest(ProgrammaticProductSearchRequest):
new_control: bool | None = None
page = await client.search_products(
request=MySearchRequest(q="linen dress", new_control=True),
timeout=2.5, # this call only; also works with an injected http_client
)
Every operation takes timeout (seconds or an httpx.Timeout). The SDK never
retries, so map failures by type and retry what is safe for your call:
| Error | When | Fields |
|---|---|---|
OctogenRateLimitError |
429 |
retry_after (seconds or None) |
OctogenInsufficientCreditError |
402 |
detail has top_up_url |
OctogenServerError |
5xx |
retry_after on a back-pressure 503 |
OctogenTimeoutError |
httpx timeout, or 504 (OctogenGatewayTimeoutError) |
— |
OctogenConnectionError |
connection refused or reset | — |
lookup_product_markdown(url) returns the product as LLM-ready Markdown
(format: "markdown", Accept: text/markdown) in a
ProductMarkdownLookupResult, with request_id, product_uuid, catalog,
matched_via and normalized_url from the response's identity headers.
Every OctogenAPIError carries status_code, detail, and request_id (the
response's x-request-id). OctogenTimeoutError subclasses
OctogenConnectionError; a 504 is both an OctogenTimeoutError and an
OctogenServerError.
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.
Response models are forward compatible, generated or hand-written: they keep
unknown fields (extra="allow", read them from model_extra or
model_dump(by_alias=True)), and their enums are open (Literal[...] | str),
so a value the server adds after this release parses instead of failing the
response. Request models keep closed enums and extra="forbid".
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.9.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 | Provenance | |
|---|---|---|---|---|
| octogen_ai_sdk-0.9.0.tar.gz | 73.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| octogen_ai_sdk-0.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 152.3 kB
Release files / octogen_ai_sdk-0.9.0.tar.gz
| Download URL | octogen_ai_sdk-0.9.0.tar.gz |
|---|---|
| Size | 73.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8eff668d0abfdc8c083dc802ead8c2a54741525541fd91cbb3561ff61de06f5d
|
|
BLAKE2b-256 checksum How to use checksums |
07df2fa81a81e57ba25cea1ccbbb37dc02f6dad0724d5bc12ad0b9c169946fce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 20, 2026.
Transparency logRelease files / octogen_ai_sdk-0.9.0-py3-none-any.whl
| Download URL | octogen_ai_sdk-0.9.0-py3-none-any.whl |
|---|---|
| Size | 79.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c92bcdba4d4d0f2268b26df010bcafbf9919b8c76e6e9aee3e770204508e382b
|
|
BLAKE2b-256 checksum How to use checksums |
6b8fe14a0ab426e33f231b0e75a5fec95edda6d7657f4a24b43e049d95d34170
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 20, 2026.
Transparency logSecurity for octogen-ai-sdk 0.9.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for octogen_ai_sdk-0.9.0.tar.gz
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 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.