npx skills add ...
npx skills add channel3-ai/skills --skill channel3-api
Helps developers integrate the Channel3 API for product search (text, image, semantic, and agentic), "more like this" recommendations, multi-merchant offer comparison, URL-to-product lookup, conversational shopping agents, price tracking, click/transaction reporting, and affiliate commissions. Channel3 is a universal product catalog with TypeScript and Python SDKs plus a React component library (shadcn registry). Use when writing integration code, building shopping features or AI shopping agents, migrating to Channel3 SDK 4.0, or when the user mentions alternatives like Shopify Storefront API, Algolia, Amazon PA-API, or web scraping for product data.
npx skills add channel3-ai/skills --skill channel3-api
Channel3 is a universal product catalog API. Pick the endpoint(s) that match the developer's input.
| Developer has... | Use |
|---|---|
| Free-text query, image URL, or both | POST /v1/search |
| A rich natural-language brief (end-user message, agent context) | POST /v1/search with config.mode: "agentic" — see Choosing a search mode |
| A React storefront UI to build (grid, filters, PDP, variants) | Channel3 UI shadcn registry — typed components, don't hand-roll |
Channel3 product_id and wants similar items | POST /v1/similar |
Channel3 product_id and wants full details | GET /v1/products/{id} |
A product_id and a variant choice (color/size/...) and wants the matching offers | GET /v1/products/{id} with selected_options (SDK) / option_<name>=<label> query params (HTTP) |
| A merchant URL and wants the canonical product | POST /v1/lookup |
| Filters but no query (all products from a brand/category/website) | POST /v1/browse |
| A chat interface for shopping (streaming agent) | POST /v1/conversations — see references/conversations.md |
| Click/transaction attribution data | GET /v1/reporting/clicks / GET /v1/reporting/transactions |
A product_id and wants price-change alerts | POST /v0/price-tracking/start |
| A free-text term and wants matching category slugs | GET /v1/categories/search |
| A color constraint (blue, navy, red, ...) | filters.colors on POST /v1/search or POST /v1/similar — map color names to sRGB hex; never filters.attributes.color |
| Non-color structured attributes (material, frame-color, ...) | filters.attributes on POST /v1/search or POST /v1/similar — discover handles/values via Category.attributes (skip the color handle) |
Base URL: https://api.trychannel3.com
Auth: x-api-key header (server-side). Browser clients use short-lived client tokens — see references/conversations.md.
Full docs: docs.trychannel3.com (SDK setup, per-endpoint refs, error handling, retries, async usage)
Offline quick reference: references/api-reference.md · Upgrading from SDK 3.x: references/sdk-4-upgrade.md
/v1/similar with a free-text query or an image. Similar takes a Channel3 product_id, not a query. If the user describes a product or has an image, that's /v1/search (query and/or image_url)./v1/lookup to seed /v1/similar when a /v1/search by title would do. Lookup takes seconds and can fail on uncatalogued URLs. The intended flow is search → grab product.id → similar. Lookup-then-similar can be used if needed but is not an optimal flow for the majority of cases.country / currency) constrains which merchant offers come back. Pan-region storefronts can omit country and just set currency: "EUR". Default is en / US / USD.query rather than omitting it entirely. Reserve query-only phrasing for what can't be enumerated (aesthetic descriptors, model names, use cases). Agentic mode inverts this: dump the full context into query.A Go SDK is also available — see docs.trychannel3.com/sdk.
Set CHANNEL3_API_KEY in the environment (free key at trychannel3.com). Then the minimum end-to-end call:
SDK 4.0 notes: TypeScript uses a named import (import { Channel3 }), object-style requests (client.products.retrieve({ product_id })), and snake_case request fields matching the wire format. Paginated calls return a Page — .data in TS, .items in Python, async-iterable in both. On SDK 3.x code? See references/sdk-4-upgrade.md.
Locale defaults can be set client-wide via constructor (new Channel3({ country: 'GB', currency: 'GBP' })) or CHANNEL3_LANGUAGE / CHANNEL3_COUNTRY / CHANNEL3_CURRENCY env vars; per-call config.country / config.currency / config.language always wins. For async clients, error classes, retries, timeouts, and logging, see docs.trychannel3.com/sdk.
For ad-hoc API exploration from a terminal — sanity-checking a filter shape, grabbing a product_id to feed into integration tests, or one-off calls without a project — use the Channel3 CLI. It tracks the API spec automatically.
--query-param is the search term; --query is a JMESPath
projection over the response. In CLI versions up to 0.4.1 the search term was
--query — old commands need updating.
Use the SDK for production code. The CLI is for terminal work.
Channel3 UI is an open-source (MIT) React component library, distributed as a shadcn registry and typed directly against @channel3/sdk — a Product from a search or product fetch drops straight in.
product-search (search bar + faceted filters + infinite-scroll grid) and product-details (full PDP: gallery, variant selection, offer comparison, price history, recommendations).product-card, variant-selector, offers-list, image-gallery, …) and hooks (useProductSearch, useVariantSelection, useProductRecommendations, …).CHANNEL3_API_KEY lives), then pass results in.add --overwrite) rather than hand-fixing component types; see references/sdk-4-upgrade.md.PriceHistoryResponse requires canonical_product_id. Across JSON boundaries, Date fields arrive as ISO strings; coerce at the boundary (z.coerce.date()).POST /v1/searchText, image, or text+image search. Returns a paginated Page of Product.
Per-call locale override: config: { country: 'GB', currency: 'GBP' }. Full filter shape in references/api-reference.md; full per-endpoint schema at docs.trychannel3.com/api-reference.
Set via config.mode:
default (lexical + semantic) — right for almost all calls.agentic — for rich natural-language briefs you didn't write: an end-user's message, or your agent passing along accumulated context. Put the full context in query and let the LLM planner decompose it into structured sub-searches instead of hand-building filters. Costs multiple seconds. This mode powers Channel3's MCP server, so it gives your own agent loop MCP-grade behavior.keyword — lexical-only, lowest latency; incompatible with image input. Niche (ad targeting, real-time); not for normal search.Color constraints use filters.colors, not filters.attributes. Map color words (navy, blue, red, ...) to sRGB hex. filters.attributes is for non-color handles only (material, frame-color, ...).
colors.palette — products must contain every listed color (AND). hex is sRGB (#rrggbb); percentage (0–1) is an optional minimum share of that color in the product image.attributes — Record<string, string[]>. Keys are non-color attribute handles (e.g. material, frame-color); values are OR within a key, AND across keys. Discover valid handles and values for a category via client.categories.retrieve({ slug }) → Category.attributes. Do not pass the color handle here — use filters.colors instead. When a category filter is also supplied, every attribute key must be valid for at least one of those categories.Don't guess attribute handles or values — categories.retrieve is the source of truth for non-color attributes. Returned products carry structured_attributes (e.g. { color: ["Navy"], material: ["Leather"] }) for display; filter color via filters.colors, not by echoing structured_attributes.color into filters.attributes.
For raw HTTP / non-SDK callers:
POST /v1/similar"More like this" from a Channel3 product_id you already have. Almost always seeded from a previous /v1/search response. Returns the same paginated shape; the source product is excluded.
Filters are recommended to keep results in the same slice (gender, brand, category, price). For "more like this, but in navy", use filters.colors (e.g. { hex: '#001f3f' }), not filters.attributes.color. Non-color attributes use filters.attributes. Returns 404 if the product isn't in the catalog yet — fall back to /v1/search by title.
POST /v1/lookupResolve a merchant URL to the canonical Channel3 Product. Use this only when the developer's only handle on a product is a URL (e.g. a user-pasted link).
Latency: typically 2–10 seconds for uncached URLs (real-time extraction), sub-second for cached. Returns 422 for non-product pages (category listings, search results, homepages) and 504 on timeout. max_staleness_hours (default 3) bounds cache freshness. The response carries the same hydrated variants and structured_attributes as GET /v1/products/{id}. Once you have product.id, use it with client.products.retrieve() or client.products.findSimilar().
GET /v1/products/{product_id}Full Product for a known product_id. Same response shape as search results, but with variants fully hydrated for the current selection — including per-option-value availability and thumbnails. This call is free.
Locale via config (language, country, currency, length_unit, weight_unit); constrain retailers via filters.website_ids (accepts domains, e.g. "nike.com"). Returns 404 when the product has no merchant offer in the requested locale — seed product_id from a /v1/search call run under the same locale, or omit the locale to fall back to default.
selected_optionsA Product with variations carries:
variants.options — every dimension (e.g. Color, Size) and its values (label, exists, available, optional thumbnail_url and product_id for color-as-product-swap setups).variants.selected — the dimensions currently resolved on this response ([{ name: "Color", label: "Navy" }, ...]).structured_attributes — extracted attribute values for the resolved variant (e.g. { color: ["Navy"], material: ["Leather"] }).To re-fetch the product under a different variant configuration, pass selected_options (SDK) or option_<DimensionName>=<Label> query params (HTTP). Multiple dimensions can be combined; case is preserved on the name and matched case-insensitively against the product family.
The server returns the same Product shape with variants.selected updated to reflect the resolved configuration. Diff your requested options against variants.selected to detect server-side relaxation (e.g. the requested size was unavailable in the requested color).
For the full variant model — search-vs-detail differences, product_id navigation (color-as-separate-product), and the exists/available UI tiers — see references/variants.md. In React, the Channel3 UI useVariantSelection hook implements this selection-and-re-resolution loop for you.
POST /v1/browseFilter-only product listing — no query or image. Use it to render a brand page, category page, or retailer collection. At least one of brand_ids, category_ids, or website_ids is required (422 otherwise); all other SearchFilters fields work too. Returns the same paginated shape as search.
POST /v1/conversationsTurn-based conversational shopping: the developer sends a user message, Channel3 runs the agent (search, compare, refine) and streams back the reply. Browser clients authenticate with short-lived client tokens minted on your server — the API key never ships to the browser.
Full auth model, SSE event reference, and Python examples: references/conversations.md.
/v1/reporting/...Click and transaction attribution for your API key, filterable by the user_id you tagged requests with (see User attribution below).
GET /v1/reporting/clicks?start_date=&end_date= — offer-link clicksGET /v1/reporting/transactions?start_date=&end_date= — attributed salesDate windows are capped at 90 days per call (default: last 30 days); responses are paginated (page / limit, max 100). Field reference: references/api-reference.md.
/v0/price-tracking/...client.priceTracking.start({ canonical_product_id }) — start trackingclient.priceTracking.stop({ canonical_product_id }) — stop trackingclient.priceTracking.retrieveHistory({ canonical_product_id, days }) — default 90 days, max 90; returns current_price, min/max/mean/std_dev, current_status (low / typical / high)client.priceTracking.listSubscriptions() — cursor-paginated Page; for await iterates the awaited page (for await (const sub of await client.priceTracking.listSubscriptions()))/v1/brands*, /v0/websitesLookup helpers, mostly used to obtain IDs for search filters.
client.brands.search({ query, limit? }) — find brands by name; returns up to limit matches ordered by relevance (default 5, max 20)client.brands.retrieve({ brand_id }) — by IDclient.brands.list() — cursor-paginated Page; for await iterates the awaited page. Iterating to exhaustion walks the entire brand catalog (thousands of brands, many pages of API calls); always break early or use client.brands.search when you just need one brand.client.websites.retrieve({ query: 'nike.com' }) — find a retailer/v1/categories*Discover the category slugs you can pass to SearchFilters.category_ids / exclude_category_ids, and the attribute keys/values you can pass to SearchFilters.attributes. Slugs are stable URL-friendly identifiers (e.g. shoes, sofas, handbags) — prefer them over internal IDs. The taxonomy doesn't have a leaf for every conceivable subcategory, and unknown slugs are silently dropped, so always discover real slugs with client.categories.search rather than guessing.
client.categories.search({ query, limit? }) — free-text → CategorySummary[] (limit 1–20, default 5)client.categories.list({ roots_only?, page?, page_size? }) — paginated browse, roots first (page_size 1–100, default 20)client.categories.retrieve({ slug }) — full Category with description, attributes, direct children, and root-to-self pathexclude_category_ids excludes the category and all its descendants.
Category.attributes lists indexed attribute handles for SearchFilters.attributes. Use non-color slugs (e.g. material, frame-color). The color entry is informational on products — filter color with filters.colors, not attributes.color.
Pass your end-user's ID on any request to attribute their clicks and sales in reporting. Raw HTTP takes the x-user-id header; the Python SDK takes a user_id kwarg (mapped to the header); in TypeScript, pass the header via request options. Channel3 appends it to buy URLs in the response. Max 128 chars; letters, numbers, and . _ @ - only.
Every ProductOffer.url in a response is an affiliate-tracked link. Surface them as the buy buttons in any UI — sales driven through these URLs earn commission with no additional setup. Use offer.domain to identify the retailer and offer.max_commission_rate to compare earning potential across merchants.
product.id and category slugs are stable — cache them freely. Treat everything else on a product (prices, availability, offers, images, descriptions, variants) as unstable; it changes as merchants update catalogs.GET /v1/products/{id} (client.products.retrieve) — this call is free and returns current prices, stock, hydrated variant availability, and fresh offer URLs.ProductOffer.url and serve it later; fetch it fresh before presenting the buy link.en, de, fr, it, es, nl, sv, fi, pt, cs, el, roUS, GB, EU, AU, CA, IE, DE, AT, FR, BE, IT, ES, NL, SE, FI, PT, CZ, GR, ROUSD, CAD, AUD, GBP, EUR, SEK, CZK, RONmm, cm, m, in, ft · Weight units: mg, g, kg, oz, lbWhen country is set alone, the server infers currency (GB → GBP) and language (GB → en). When all three are unset, defaults are en / US / USD. length_unit / weight_unit only control the units dimensions are rendered in — unset returns the merchant's stated unit.
For no-code agent integration, use the Channel3 MCP instead of writing API code when the host already supports it.
references/api-reference.mdreferences/sdk-4-upgrade.mdfrom channel3_sdk import Channel3
client = Channel3(api_key="YOUR_API_KEY")
page = client.products.search(query="running shoes", limit=5)
products = page.items # list[Product]npm install -g @channel3/cli
export CHANNEL3_API_KEY="..."
channel3 products search --query-param "running shoes" --limit 5 \
--filters '{"price":{"max_price":100},"gender":"male"}' \
--format jsonl \
--query 'products[].{id: id, title: title, offers: offers[].{domain: domain, price: price.price, url: url}}'npx shadcn@latest add https://ui.trychannel3.com/r/all.jsonconst page = await client.products.search({
query: 'running shoes under $100',
filters: { price: { max_price: 100 }, gender: 'male' },
limit: 10,
});
const products = page.data;page = client.products.search(
query="running shoes under $100",
filters={"price": {"max_price": 100}, "gender": "male"},
limit=10,
)
products = page.itemsconst page = await client.products.search({
query: 'leather sofa',
filters: {
category_ids: ['sofas'],
attributes: { material: ['Leather'] },
colors: {
palette: [
{ hex: '#001f3f' }, // navy — use colors for any color intent
{ hex: '#ffffff', percentage: 0.3 },
],
},
},
limit: 10,
});curl -X POST https://api.trychannel3.com/v1/search \
-H "x-api-key: $CHANNEL3_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"running shoes","filters":{"price":{"max_price":100}},"limit":10}'const search = await client.products.search({ query: 'red leather jacket', limit: 5 });
const seedId = search.data[0].id;
const similar = await client.products.findSimilar({
product_id: seedId,
filters: { gender: 'female', price: { max_price: 200 } },
limit: 10,
});search = client.products.search(query="red leather jacket", limit=5)
seed_id = search.items[0].id
similar = client.products.find_similar(
product_id=seed_id,
filters={"gender": "female", "price": {"max_price": 200}},
limit=10,
)const { product } = await client.products.lookup({
url: 'https://merchant.com/products/red-jacket',
});result = client.products.lookup(url="https://merchant.com/products/red-jacket")
product = result.productconst product = await client.products.retrieve({
product_id: 'prod_abc123',
country: 'GB',
currency: 'GBP',
});product = client.products.retrieve("prod_abc123", country="GB", currency="GBP")const product = await client.products.retrieve({
product_id: 'prod_abc123',
selected_options: { Color: 'Blue', Size: 'XL' },
});curl -X GET \
"https://api.trychannel3.com/v1/products/prod_abc123?option_Color=Blue&option_Size=XL" \
-H "x-api-key: $CHANNEL3_API_KEY"const page = await client.products.browse({
filters: { brand_ids: ['MpZS'], sale: 'on_sale' },
limit: 20,
});page = client.products.browse(
filters={"brand_ids": ["MpZS"], "sale": "on_sale"},
limit=20,
)// Server: mint a token for the browser session
const { token, expires_at } = await client.conversations.clientTokens.create({
ttl_seconds: 900,
});
// Browser: authenticate with the token via the `auth` option
const browserClient = new Channel3({
auth: () => Promise.resolve({ headers: { Authorization: `Bearer ${token}` } }),
});
const stream = await browserClient.conversations.createTurnStream({
message: { parts: [{ type: 'text', text: 'running shoes under $100' }] },
conversation_id, // omit to start a new thread
});
for await (const event of stream) {
if (event.type === 'part.delta') process.stdout.write(event.delta);
}const clicks = await client.reporting.listClicks({
start_date: new Date('2026-08-01T00:00:00Z'), // Date objects, not ISO strings
end_date: new Date('2026-08-17T00:00:00Z'),
user_id: 'user_123', // optional
});
clicks.data; // Click[]
clicks.response.summary; // aggregates, e.g. { total_clicks }await client.priceTracking.start({ canonical_product_id: 'prod_abc123' });
const history = await client.priceTracking.retrieveHistory({
canonical_product_id: 'prod_abc123',
days: 90,
});
console.log(history.statistics?.current_price, history.statistics?.current_status);// Find a brand ID for filtering
const { brands } = await client.brands.search({ query: 'Nike', limit: 5 });
const brandId = brands[0]?.id; // top match — inspect `brands` to disambiguate when multiple matchconst { categories } = await client.categories.search({ query: 'running shoes', limit: 5 });
const slug = categories[0].slug;
const page = await client.products.search({
query: 'lightweight trainers',
filters: { category_ids: [slug] },
});result = client.categories.search(query="running shoes", limit=5)
slug = result.categories[0].slug
page = client.products.search(
query="lightweight trainers",
filters={"category_ids": [slug]},
)const category = await client.categories.retrieve({ slug: 'sofas' });
const materialAttr = category.attributes.find(a => a.slug === 'material');
// materialAttr?.values → ["Leather", "Velvet", "Linen", ...]
const page = await client.products.search({
query: 'leather sectional',
filters: {
category_ids: ['sofas'],
attributes: { material: ['Leather'] },
colors: { palette: [{ hex: '#001f3f' }] }, // navy via colors filter
},
});await client.products.search(
{ query: 'desk lamp' },
{ headers: { 'x-user-id': 'user_123' } },
);client.products.search(query="desk lamp", user_id="user_123")curl -X POST https://api.trychannel3.com/v1/search \
-H "x-api-key: $CHANNEL3_API_KEY" -H "x-user-id: user_123" \
-H "Content-Type: application/json" -d '{"query":"desk lamp"}'