Skill: ramius88/booksearch-api (version 1.0.7)

--- name: booksearch-api description: Search Amazon KDP books on the BeyondBSR public API, retrieve BSR (Best Sellers Rank) history for a single book, and explore the Amazon category taxonomy (browse nodes) for any supported marketplace. Each book-search result now ships its full root → leaf category ancestor chain(s) inline, so the skill can aggregate market-opportunity reports by macro/sub category without follow-up calls. Use this skill whenever the user wants to discover, filter, or research self-published or traditionally published books on Amazon by BSR, category, keyword, royalty range, rating, reviews, publication date, binding type, or marketplace (US, UK, DE, FR, IT, ES, CA); when the user wants the BSR timeline of a specific ASIN over the last N days; when the user wants to look up Amazon category codes (browse node IDs), walk the category tree (children, ancestors/breadcrumb), or search categories by name to use as filters in book search; or when the user wants to group/aggregate search results by macro category (e.g. "how many Personal Finance opportunities, broken down by sub-category?"). Typical intents include KDP niche research, low-competition book discovery, sales estimation, royalty/revenue projection, competitor analysis, paperback/hardcover filtering, bulk listing of books matching numeric/textual criteria, single-ASIN BSR history charts, resolving a human-readable category name (e.g. "manga", "self-help") into the Amazon `catId` to pass to `categoryIds` in book search, and producing category-grouped market-opportunity summaries from a single search response. Do not use for price-history timelines, review/rating timelines, or account/user data — only book search, BSR history, and category browsing are exposed. metadata: clawdbot: requires: env: - BOOKSEARCH_API_KEY --- # BookSearch API ## ⚠️ API Access & Beta Program The BeyondBSR BookSearch API is currently in **private beta**. This skill requires an API key (`BOOKSEARCH_API_KEY`) which is **not publicly available** at this time. Users interested in accessing Amazon KDP book data (BSR history, reviews, categories, keyword research) can apply to the early adopter program by contacting **[REDACTED_SECRET]**. Requests are reviewed individually and approved keys are issued on a case-by-case basis. Without a valid key, all endpoints below will return `401 Unauthorized`. --- Programmatic search over the BeyondBSR book catalogue, BSR history retrieval for a single book, and Amazon category taxonomy browsing. Six endpoints, JSON in / JSON out, API-key auth. ## When to use this skill Use it when the user asks to: - Find books matching numeric filters: BSR range, rating, reviews count, royalty, page count age, publication recency. - Discover niches by keyword inclusion/exclusion or Amazon category IDs. - Filter by marketplace (Amazon.com, .co.uk, .de, .fr, .it, .es, .ca). - Distinguish self-publishers from traditional publishers. - Estimate sales (daily / weekly / monthly / quarterly) and revenue per copy. - Compare BSR averages across multiple time windows (7d / 30d / 90d / 180d / 365d). - Retrieve the **BSR timeline** of a single book (by `domainId` + `asin`) over the last N days, e.g. for charting rank evolution. - **Resolve a category name into an Amazon `catId`** (browse node ID), look up a category's direct children, walk its breadcrumb up to the root, or list top-level book categories for a marketplace — to feed `categoryIds` into book search, or just to explore the taxonomy. **Do NOT use** for: price-history charts, review/rating timelines, account/user data, or anything not in the response schemas below. Those are out of scope. ## Endpoints ``` POST https://beyondbsr.com/api/v1/books/search GET https://beyondbsr.com/api/v1/books/{domainId}/{asin}/bsr-history?days={1..365} GET https://beyondbsr.com/api/v1/categories?domainId={..}&depth={0..5}&includeFiction={true|false} GET https://beyondbsr.com/api/v1/categories/children?domainId={..}&catId={..} GET https://beyondbsr.com/api/v1/categories/search?domainId={..}&q={..}&limit={1..200} GET https://beyondbsr.com/api/v1/categories/ancestors?domainId={..}&catId={..} Content-Type: application/json (book search only) X-[REDACTED_SECRET] ``` ## Authentication - Read the key from the `BOOKSEARCH_API_KEY` env var. Format: `bbsr_live_<43-char-base64url>`. - **Never** print, echo, log, or include the key in any user-facing output. Never paste it into another tool's input. - On `401 Unauthorized`: do not retry. Report "API key missing or invalid — check `BOOKSEARCH_API_KEY` env var" and stop. - Send `X-API-Key` exactly once. Multi-valued headers are rejected. ## Marketplace domains Map natural-language marketplace references (e.g. "Amazon.de", "the UK store", "amazon italia") to `domainId` using this table: | domainId | locale | country | name | |----------|--------|---------|----------------| | 1 | com | US | United States | | 2 | co.uk | GB | United Kingdom | | 3 | de | DE | Germany | | 4 | fr | FR | France | | 6 | ca | CA | Canada | | 8 | it | IT | Italy | | 9 | es | ES | Spain | IDs `5` and `7` are intentional gaps in the dataset — do not invent them. The validator technically accepts `1–12`, but only the seven IDs above are guaranteed to return data. If the user asks for a marketplace not listed (e.g. Japan, Australia), tell them it is not currently supported. ## Request schema All fields are optional except `domainId`. Enums accept either the string name (e.g. `"Weekly"`) or the integer value. ### Required | Field | Type | Constraint | |------------|------|-----------------------------| | `domainId` | int | One of the 7 IDs in the marketplace table above (validator allows 1–12). | ### BSR filters | Field | Type | Default | Notes | |------------|-------------|-----------|-----------------------------------------------------------------------------| | `bsrType` | enum | `Weekly` | `Historical(-1)`, `Current(0)`, `Weekly(7)`, `Days30(8)`, `Days90(9)`, `Days180(10)`, `Days365(11)` | | `bsrMin` | int? | 1 | ≥ 1 | | `bsrMax` | int? | 100000 | ≥ 1, `bsrMin ≤ bsrMax` | | `bsrYear` | short? | null | 2000–(current year + 1). Required together with `bsrMonth`. Use only with `bsrType=Historical`. | | `bsrMonth` | short? | null | 1–12. Required together with `bsrYear`. | ### Product filters | Field | Type | Default | Notes | |--------------------|---------|-------------|----------------------------------------------------| | `bindingType` | enum? | null (all) | `All`, `Paperback`, `Hardcover` | | `publisherType` | enum? | `All` | `All`, `SelfPublishersOnly`, `PublishersOnly` | | `interiorType` | enum? | `BlackWhite`| `BlackWhite`, `FullColor` | | `vatType` | enum? | `Reduced` | `Reduced`, `Standard` | | `includePreOrders` | bool? | `false` | — | ### Quality filters | Field | Type | Constraint | |---------------|---------|------------------------------------| | `ratingMin` | double? | 0.0–5.0, `ratingMin ≤ ratingMax` | | `ratingMax` | double? | 0.0–5.0 | | `reviewsMin` | int? | ≥ 0, `reviewsMin ≤ reviewsMax` | | `reviewsMax` | int? | ≥ 0 | ### Economic filters | Field | Type | Notes | |------------------------------|----------|------------------------------------------------------| | `royaltyMin` | decimal? | In currency units, **not** cents. `min ≤ max`. | | `royaltyMax` | decimal? | — | | `monthsSincePublicationMin` | int? | `min ≤ max` | | `monthsSincePublicationMax` | int? | — | ### Discovery | Field | Type | Default | Constraint | |-------------------|----------|---------|-------------------------------------------------------------| | `includeKeywords` | string[] | null | ≤ 25 items, each ≤ 100 chars, non-empty. **Each element is matched as a literal contiguous substring** (case-insensitive) against title, publisher and authors. Multiple elements are combined with **OR**. Pass multi-word phrases as a single element (`["small business taxes"]`), NOT as separate tokens (`["small","business","taxes"]`) — the latter would match any book containing just one of those words. | | `excludeKeywords` | string[] | null | ≤ 25 items, each ≤ 100 chars, non-empty. Same matching semantics as `includeKeywords`: each element is a literal contiguous substring; books matching **any** element on title, publisher or authors are excluded. | | `categoryIds` | long[] | null | ≤ 100 items, each > 0 (Amazon BrowseNode IDs). **Subtree-expanded**: passing a non-leaf node (e.g. depth-2 "Quick & Easy", `catId=17`) returns every book tagged with any descendant leaf — you do not need to enumerate leaves yourself. Pass any node from `/categories`, `/categories/search`, `/categories/children`, or `/categories/ancestors`. Mixing leaves and parents in one call is allowed (logical OR across the union of the expanded sets). Stale / unknown `catId`s collapse to no overlap and are silently dropped, not an error. | | `excludeFiction` | bool? | `true` | When `true` (default), filters out fiction books — i.e. books whose categories all roll up to depth-2 ancestors flagged as fiction (Literature & Fiction, Romance, Mystery, Sci-Fi, Children's Books, Teen/YA, etc.) under the local Books root for the requested marketplace. A book is kept if **at least one** of its categories has a depth-2 ancestor flagged as non-fiction. Set `false` to include fiction. **Auto-disabled** when `categoryIds` is non-empty: explicit category intent overrides the fiction filter, otherwise passing a fiction category with the default would silently return zero results. Currently unsupported on Amazon.ca (`domainId=6`): with `excludeFiction=true` no books are returned for CA — pass `false` for that marketplace. | ### Pagination | Field | Type | Default | Range | |----------|------|---------|------------| | `limit` | int? | 100 | 1–300 | | `offset` | int? | 0 | 0–100000 | ## Workflow 1. **Marketplace** — Identify `domainId` from intent using the marketplace table. If ambiguous (e.g. "Amazon"), ask which country. 2. **Translate** — Map every natural-language filter to its schema field. Don't guess enum values; use the table. Note: by default fiction is excluded (`excludeFiction=true`). If the user asks for fiction (e.g. "romance", "mystery novels", "show me fiction too") or a mixed catalog, set `excludeFiction: false` explicitly. Most KDP/low-content niche queries are non-fiction so the default is usually correct. 3. **Pagination** — Default to `limit: 50`. Raise to 100–300 only if the user explicitly wants many results. Start with `offset: 0`. 4. **Send** — POST the JSON body. Inspect the status code first. 5. **Paginate if needed** — If `returnedCount == limit`, more results likely exist. Offer to fetch the next page with `offset += limit`. 6. **Present** — Surface `title`, `asin`, `bsr`, sales/revenue estimates, and a clickable cover URL (see response notes). ## Example request ```json { "domainId": 3, "bsrType": "Days90", "bsrMin": 1, "bsrMax": 50000, "bindingType": "Paperback", "publisherType": "SelfPublishersOnly", "ratingMin": 4.0, "reviewsMin": 10, "interiorType": "BlackWhite", "vatType": "Reduced", "royaltyMin": 2.50, "royaltyMax": 15.00, "monthsSincePublicationMax": 24, "includePreOrders": false, "includeKeywords": ["journal", "notebook"], "excludeKeywords": ["coloring"], "categoryIds": [266162, 3248921], "excludeFiction": false, "limit": 50, "offset": 0 } ``` ## Example cURL ```bash curl -X POST "https://beyondbsr.com/api/v1/books/search" \ -H "Content-Type: application/json" \ -H "X-[REDACTED_SECRET]" \ -d '{ "domainId": 3, "bsrType": "Days90", "bsrMax": 50000, "bindingType": "Paperback", "publisherType": "SelfPublishersOnly", "ratingMin": 4.0, "limit": 50 }' ``` ## Response schema ### Envelope — `BookSearchApiResponse` | Field | Type | Notes | |-----------------|---------|--------------------------------------------------------------------| | `returnedCount` | int | Items in **this page**. NOT a total-match count (no total exposed).| | `limit` | int | Effective limit applied (capped at 300). | | `offset` | int | Effective offset applied. | | `results` | array | `BookSearchResultDto[]`. Empty if no match. | ### Result item — `BookSearchResultDto` (most relevant fields) | Field | Type | Notes | |----------------------|----------|----------------------------------------------------------------------------------------| | `id` | long | Internal book ID. | | `asin` | string | 10-char Amazon ASIN. | | `title` | string | Full title. | | `authors` | string? | Comma-separated, ordered by `display_order`. | | `publicationDate` | datetime?| Nullable. | | `publisher` | string? | Manufacturer/publisher name. | | `coverImageFilename` | string? | Build URL: `https://m.media-amazon.com/images/I/{filename}`. | | `imageFilenames` | string[] | All carousel images (same URL pattern). | | `rating` | double | 0.0–5.0. | | `reviews` | int | Total reviews. | | `bsr` | int | **The column matching the requested `bsrType`** — i.e. the value filtered/sorted on. | | `bsrCurrent` | int? | Latest snapshot BSR, regardless of `bsrType`. | | `avgBsr7d` | int? | Always populated. | | `avgBsr30d` | int? | Always populated. | | `avgBsr90d` | int? | Always populated. | | `avgBsr180d` | int? | Always populated. | | `avgBsr365d` | int? | Always populated. | | `pageCount` | int? | — | | `priceCents` | int? | List price (MSRP) in cents. | | `dailyEstimate` | decimal | Estimated copies/day from BSR model. | | `weeklyEstimate` | decimal | Estimated copies/week. | | `monthlyEstimate` | decimal | Estimated copies/month. | | `quarterlyEstimate` | decimal | Copies/quarter (needs ≥ 13 weeks of data). | | `royalty*Cents` | int? | Per-copy royalty in cents. 4 combinations: B/W or Color × Reduced or Standard VAT. | | `*Revenue*Cents` | long? | Derived = estimate × royalty. 16 fields total: {daily,weekly,monthly,quarterly} × {Black,Color} × {Reduced,Standard}. | | `binding` | string? | Localised label (e.g. `"Paperback"`, `"Hardcover"`, `"Non-standard"`). | | `dimensions` | string? | Formatted, prefixed by binding. Example: `"Paperback: 152 x 8 x 229 mm"`. | | `trim/spineWidthMm` | int? | Trim/spine measurements. | | `categories` | long[] | Raw leaf Amazon `cat_id`s the book is tagged with (verbatim from the database). Preserves original tag order. May be empty for books still being enriched. | | `categoryPaths` | `Array<Array<{catId:long,name:string,depth:int}>>` | One inner array per entry in `categories`, each is the full root → leaf ancestor chain sorted by `depth` ascending. Use this to aggregate / group books by macro or sub category in a single round-trip without calling `/categories/ancestors` per leaf. | **Important caveats** - All royalty and revenue fields are in **cents** — divide by 100 before showing currency. - `bsr` ≠ `bsrCurrent`. `bsr` is whatever column was chosen by `bsrType`; `bsrCurrent` is always the latest snapshot. - In `bsrType=Historical` mode, the `avgBsrXd` averages come from the current snapshot table while `bsr` itself is the historical month value — there is a documented temporal asymmetry inside the same response. Mention this if the user is doing a strict historical analysis. - `categoryPaths.length === categories.length` for a healthy dataset. If a leaf `cat_id` has been deleted upstream after the book was indexed, the corresponding inner array is silently dropped (never null) — so `categoryPaths.length` may be ≤ `categories.length`. The `categories` array always reflects the original tag set. - Both `categories` and `categoryPaths` may be **empty arrays `[]`** (never `null`) for books still being enriched or whose taxonomy snapshot hasn't propagated yet. Aggregation code must skip these books rather than fail on missing chains. ### Aggregating results by category Each `categoryPaths` inner array is sorted by `depth` ascending. The taxonomy under the Books root is consistent across marketplaces (only the labels are localised): | Depth | Role | US example | IT example | |-------|------|------------|-----------| | 0 | Books root | `Books` | `Libri` | | 1 | Container shell | `Subjects` | `Categorie` | | 2 | **Top-level subject** ("macro") | `Self-Help`, `Cookbooks, Food & Wine`, `Crafts, Hobbies & Home` | `Cucina, casa e giardinaggio` | | 3 | **Sub-category** | `Crafts & Hobbies`, `Christian Books & Bibles` | — | | 4-5 | **Niche / micro-niche** (KDP-relevant) | `Coloring Books for Grown-Ups`, `Bible Study & Reference` | — | To produce a market-opportunity report, group on the **`catId` at the desired depth**, not the `name` (names are locale-dependent; `catId` is stable). For KDP niche research the interesting depths are **3 and 4**: depth 2 is usually too broad (e.g. "Crafts & Hobbies" alone covers thousands of books), while depth 3-4 isolates real nicchie ("Coloring Books for Grown-Ups", "Word Search", "Christian Living"). Pattern — nested macro → sub-niche histogram with revenue rollup, from one search response: ```js // macro (depth=2) -> { name, books, subniches: Map<catId, { name, books, monthlyRevCents }> } const report = new Map(); for (const book of response.results) { const seenMacros = new Set(); // dedupe within the same book const seenNiches = new Set(); for (const chain of book.categoryPaths) { // may be [] for un-enriched books const macro = chain.find(n => n.depth === 2); const niche = chain.find(n => n.depth === 3) ?? chain.find(n => n.depth === 4); if (!macro) continue; if (!seenMacros.has(macro.catId)) { seenMacros.add(macro.catId); const m = report.get(macro.catId) ?? { name: macro.name, books: 0, subniches: new Map() }; m.books++; report.set(macro.catId, m); } if (niche && !seenNiches.has(niche.catId)) { seenNiches.add(niche.catId); const m = report.get(macro.catId); const n = m.subniches.get(niche.catId) ?? { name: niche.name, books: 0, monthlyRevCents: 0 }; n.books++; // pick whichever revenue field matches the user's interior/VAT context n.monthlyRevCents += book.monthlyRevenueBlackReducedVatCents ?? 0; m.subniches.set(niche.catId, n); } } } ``` Sample fragment of a result with two categories (US, depth-1 is always `Subjects` — depth-2 is the macro): ```json { "asin": "1234567890", "title": "Quick Weeknight Dinners", "categories": [4259, 9876], "categoryPaths": [ [ { "catId": 283155, "name": "Books", "depth": 0 }, { "catId": 1000, "name": "Subjects", "depth": 1 }, { "catId": 6, "name": "Cookbooks, Food & Wine","depth": 2 }, { "catId": 17, "name": "Quick & Easy", "depth": 3 }, { "catId": 4259, "name": "General", "depth": 4 } ], [ { "catId": 283155, "name": "Books", "depth": 0 }, { "catId": 1000, "name": "Subjects", "depth": 1 }, { "catId": 10, "name": "Health, Fitness & Dieting", "depth": 2 }, { "catId": 9876, "name": "Diet & Weight Loss", "depth": 3 } ] ] } ``` **One-shot aggregation tip.** When the goal is a single histogram/report (as opposed to interactive paging), call `POST /books/search` with `limit=300` (the max). A 300-result response is ~850 KB and returns in ~200-350 ms in production — almost always cheaper than paginating. Only fall back to paged fetches if more than 300 books matter for the report (rare for niche analysis). ## BSR History endpoint Single-ASIN BSR timeline. Returns raw snapshots from the time-series store, ordered ascending by `recordedAt`. ``` GET https://beyondbsr.com/api/v1/books/{domainId}/{asin}/bsr-history?days={1..365} X-[REDACTED_SECRET] Accept: application/json ``` ### When to use - The user has a specific ASIN and wants its BSR over time (chart, drill-down, sanity-check the search-time `avgBsrXd` averages). - The user wants to verify a book's recent rank trajectory (e.g. "is it gaining or losing visibility?"). If the user has filter criteria but no specific ASIN, use `POST /search` first, then call this endpoint per ASIN of interest. ### Path & query parameters | Param | In | Type | Required | Constraint | |------------|-------|--------|----------|----------------------------------------------------------------------------| | `domainId` | path | int | yes | One of the 7 marketplace IDs (see Marketplace domains table; validator allows 1–12). | | `asin` | path | string | yes | Exactly 10 chars, regex `^[A-Z0-9]{10}$` (uppercase letters / digits only). | | `days` | query | int? | no | 1–365. Default `365`. Window is `[now - days, now]` UTC. | ### Example request ``` GET /api/v1/books/1/1635864348/bsr-history?days=90 X-[REDACTED_SECRET] Accept: application/json ``` ### Example cURL ```bash curl -X GET "https://beyondbsr.com/api/v1/books/1/1635864348/bsr-history?days=90" \ -H "X-[REDACTED_SECRET]" \ -H "Accept: application/json" ``` ### Response schema — `BookBsrHistoryApiResponse` | Field | Type | Notes | |---------------|-------------|--------------------------------------------------------------------| | `asin` | string | Echoed from request. | | `domainId` | int | Echoed from request. | | `fromUtc` | datetime | Window start (`now - days`), UTC. | | `toUtc` | datetime | Window end (`now`), UTC. | | `pointCount` | int | Number of BSR snapshots in `points`. | | `points` | array | `BsrPointDto[]` ordered ascending by `recordedAt`. | `BsrPointDto`: | Field | Type | Notes | |--------------------|-----------|----------------------------------------------------------------| | `recordedAt` | datetime | UTC timestamp of the snapshot. | | `bestSellersRank` | int? | BSR at that timestamp. May be null if Keepa returned no rank. | Example body: ```json { "asin": "1635864348", "domainId": 1, "fromUtc": "2026-01-27T00:00:00Z", "toUtc": "2026-04-27T00:00:00Z", "pointCount": 412, "points": [ { "recordedAt": "2026-01-27T03:14:00Z", "bestSellersRank": 1234 }, { "recordedAt": "2026-01-27T15:02:00Z", "bestSellersRank": 1218 } ] } ``` ### Status codes (BSR history specific) | Code | Meaning | Notes | |------|-------------------------------|-----------------------------------------------------------------------| | 200 | OK | `pointCount` may be 0 if no snapshots exist in the window. | | 400 | Validation failed | `ValidationProblemDetails`. Common causes: `days` out of range, `asin` wrong format, `domainId` out of `[1,12]`. | | 404 | ASIN not found for that domain| Empty body. The book is not tracked in BeyondBSR for this marketplace. Tell the user — do not retry with the same pair. | | 401 / 429 / 500 | See generic table below. | ### Caveats - Granularity is **raw**: Keepa snapshots arrive 1–4× per day on actively-tracked books. A 365-day window typically yields 365–1460 points. No daily aggregation is applied. - The endpoint returns only `(recordedAt, bestSellersRank)`. Use `POST /search` if you also need rating/review/price data. - ASIN is case-sensitive — must be uppercase. `1635864348` (all digits) is valid. - Default 365 days is the cap. Longer histories are not exposed; do not retry with `days > 365`. ## Categories endpoints The four endpoints under `/api/v1/categories` expose the Amazon taxonomy (browse nodes) BeyondBSR has ingested per marketplace. They share the same API-key auth, rate limit (`api-key` policy), and `domainId` semantics as book search. All four are `GET`, JSON out, no request body. ### Use cases - **Resolve a name → `catId`** to pass into `POST /books/search` `categoryIds`. Example: user asks for "manga" books on US → call `/categories/search?domainId=1&q=manga` to get candidate cat_ids, then feed the chosen ones into `categoryIds`. - **Top-level book categories** of a marketplace (e.g. "list the main Books categories on Amazon.de"): `GET /categories?domainId=3&depth=2`. - **Walk down the tree** from a known node (e.g. "what's under Cookbooks?"): `GET /categories/children?domainId=1&catId=6`. - **Walk up the tree / breadcrumb** for a leaf node (e.g. "where does cat 4546138031 sit?"): `GET /categories/ancestors?domainId=8&catId=4546138031` → returns root → … → node, ordered by depth. ### Shared response object — `CategoryBrowseNodeDto` | Field | Type | Notes | |-------------------|----------|---------------------------------------------------------------------------------------------| | `catId` | long | Amazon browse node ID. **This is the value to pass into `categoryIds` in book search.** | | `parentCatId` | long? | Parent's `catId`. `null` for roots. | | `rootCatId` | long | Top-level ancestor's `catId` (e.g. the Books root for the marketplace). | | `name` | string | Localised name in the marketplace's language. | | `contextFreeName` | string? | Name without parent context (Keepa-provided). May be null. | | `depth` | int | 0 = root. Top-level book categories are typically depth 2 (root → Categorie → top node). | | `isFiction` | bool | Internal flag used by `excludeFiction` in book search. Only meaningful at depth=2 under Books root. | | `productCount` | int? | Approximate number of products in that node (Keepa-reported). May be null. | ### 1. List categories at a depth (top-level Books browser) ``` GET /api/v1/categories?domainId={..}&depth={0..5}&includeFiction={true|false} ``` | Param | Type | Default | Notes | |------------------|-------|---------|----------------------------------------------------------------------------------------------------------------------------------------| | `domainId` | int | — | Required. Marketplace ID (1-12, but only the 7 IDs in the marketplace table return data). | | `depth` | int? | 2 | Hierarchy level. 0 = root. 2 = top-level book categories ("Self-Help", "Cookbooks…", etc.). Allowed 0-5. | | `includeFiction` | bool? | `false` | Whether to include nodes flagged as fiction (Literature & Fiction, Romance, Sci-Fi, Children's Books, Teen/YA, Comics, etc.). | **Filters applied automatically (do not appear in params):** only browse nodes (`isBrowseNode=true`), and only nodes whose root is the **Books** root of the marketplace — i.e. non-book trees (toys, electronics) are excluded. Use this endpoint to enumerate the canonical KDP-relevant taxonomy. Order: `productCount` DESC, then `name` ASC. Response envelope — `CategoriesApiResponse`: | Field | Type | Notes | |-----------------|---------|--------------------------------------| | `domainId` | int | Echo. | | `depth` | int | Echo (resolved default if omitted). | | `returnedCount` | int | Number of items in `results`. | | `results` | array | `CategoryBrowseNodeDto[]`. | ### 2. Direct children of a category ``` GET /api/v1/categories/children?domainId={..}&catId={..} ``` | Param | Type | Notes | |------------|------|----------------------------------------------------------------------------------| | `domainId` | int | Required. | | `catId` | long | Required. Parent category's Amazon browse node ID. | **No implicit filters.** Returns ALL active direct children of the parent — including non-browse nodes and fiction nodes. Use it for true tree navigation regardless of book/non-book context. Order: `productCount` DESC, then `name` ASC. Returns **404** if `catId` does not exist for the given marketplace. Do not retry on 404. Response envelope — `CategoryChildrenApiResponse`: | Field | Type | Notes | |-----------------|--------|--------------------------------------| | `domainId` | int | Echo. | | `parentCatId` | long | Echo of the input `catId`. | | `returnedCount` | int | | | `results` | array | `CategoryBrowseNodeDto[]`. | ### 3. Search categories by name ``` GET /api/v1/categories/search?domainId={..}&q={..}&limit={1..200} ``` | Param | Type | Default | Notes | |------------|---------|---------|--------------------------------------------------------------------------------------------------------------------| | `domainId` | int | — | Required. | | `q` | string | — | Required. Case-insensitive substring match on `name`. Min **3** chars, max 100. Wildcards (`%`, `_`, `\`) are treated as literals (escaped server-side). | | `limit` | int? | 50 | 1-200. Pushed down to SQL. | **No implicit filters.** Searches across the ENTIRE category tree of the marketplace (not just Books) — so a query like "Sport" on Amazon.it will surface both "Sport e tempo libero" (under Books) and the toy/apparel "Sport" nodes. The agent should filter client-side by `rootCatId` if the user only wants book categories. Order: `productCount` DESC, then `depth` ASC, then `name` ASC. Response envelope — `CategorySearchApiResponse`: | Field | Type | Notes | |-----------------|--------|--------------------------------------| | `domainId` | int | Echo. | | `query` | string | Echo of `q`. | | `limit` | int | Effective limit applied. | | `returnedCount` | int | | | `results` | array | `CategoryBrowseNodeDto[]`. | **Tip:** if the user-typed term is broad (e.g. "fiction") and the default `limit=50` truncates likely candidates, raise `limit` to 200. If still not enough, refine the term or use `/categories?depth=2` instead. ### 4. Ancestors / breadcrumb of a category ``` GET /api/v1/categories/ancestors?domainId={..}&catId={..} ``` | Param | Type | Notes | |------------|------|--------------------------------------------------------| | `domainId` | int | Required. | | `catId` | long | Required. Category whose breadcrumb to retrieve. | Returns the full ancestor chain **including the node itself**, ordered by `depth` ASC. No implicit filters (intermediate non-browse / promotional nodes are included). Returns **404** if `catId` does not exist for the marketplace. Response envelope — `CategoryAncestorsApiResponse`: | Field | Type | Notes | |-----------------|--------|----------------------------------------------------------------------| | `domainId` | int | Echo. | | `catId` | long | Echo. | | `returnedCount` | int | Length of the chain (includes the requested node). | | `results` | array | `CategoryBrowseNodeDto[]` from root (depth=0) to the node itself. | ### Example workflows **Resolve "manga" on Amazon.it then search books:** ```bash # 1. find candidate cat_ids curl -H "X-[REDACTED_SECRET]" \ "https://beyondbsr.com/api/v1/categories/search?domainId=8&q=manga&limit=10" # 2. inspect the breadcrumb of a candidate to confirm it's under Libri curl -H "X-[REDACTED_SECRET]" \ "https://beyondbsr.com/api/v1/categories/ancestors?domainId=8&catId=4546138031" # 3. drill down children if needed curl -H "X-[REDACTED_SECRET]" \ "https://beyondbsr.com/api/v1/categories/children?domainId=8&catId=4546138031" # 4. plug the chosen cat_id(s) into book search curl -X POST -H "X-[REDACTED_SECRET]" -H "Content-Type: application/json" \ -d '{"domainId":8,"categoryIds":[4546138031],"limit":50}' \ "https://beyondbsr.com/api/v1/books/search" ``` **List top-level Books categories on Amazon.com (non-fiction only, default):** ```bash curl -H "X-[REDACTED_SECRET]" \ "https://beyondbsr.com/api/v1/categories?domainId=1&depth=2" ``` ### Status codes (categories endpoints) | Code | Meaning | Notes | |------|----------------------------------|---------------------------------------------------------------------------------------------| | 200 | OK | `results` may be empty (e.g. no children, no matches, marketplace not seeded). | | 400 | Validation failed | `ValidationProblemDetails`. Common causes: `domainId` out of range, `q` < 3 chars, `limit` out of 1-200, `depth` out of 0-5. | | 401 | Auth failed | Same handling as book search — stop, report misconfigured key. | | 404 | `catId` not found for `domainId` | Empty body. Only on `/children` and `/ancestors`. Do not retry the same pair. | | 429 | Rate limit | Honour `Retry-After`. Same key budget as book search (30 req/min). | | 500 | Server error | Retry once, then escalate. | ### Caveats - **Read-only.** No POST/PUT/DELETE on categories. - **`catId` vs internal id.** The wire contract exposes only Amazon's `catId` (browse node ID). The internal database `id` is never surfaced and is not interchangeable. - **`depth=2` ≠ "top-level under Books" universally.** Most marketplaces seed Books root at depth=0 → "Categorie/Categories" at depth=1 → top nodes at depth=2. If `/categories?depth=2` returns unexpectedly few rows for a marketplace, retry with `depth=1`. - **`excludeFiction` semantics propagate.** A node's `isFiction=true` here is exactly what `excludeFiction` in book search rolls up on. If a user complains that a fiction sub-genre isn't being excluded, the agent can spot-check via `/categories/ancestors?catId=…` whether the depth-2 ancestor is flagged. - **Children endpoint includes non-browse nodes.** Some Amazon nodes are promotional or grouping shells (`isBrowseNode=false`). They cannot be used as `categoryIds` filters in book search. Skip them or warn the user. ## Status codes & error handling | Code | Meaning | Body | Agent action | |------|------------------------|--------------------------------------------|-------------------------------------------------------------------------------| | 200 | OK (may be empty list) | `BookSearchApiResponse` | Parse `results`. Empty array = no matches, not an error. | | 400 | Validation failed | `ValidationProblemDetails` (RFC 7807) | Read the `errors` map, fix the body, do not retry blindly. Surface the issue. | | 401 | Auth failed | empty + `WWW-Authenticate: ApiKey realm="BeyondBSR"` | Stop. Report misconfigured `BOOKSEARCH_API_KEY`. Do not retry. | | 429 | Rate limit exceeded | `"Rate limit exceeded. Please try again later."` + `Retry-After` header | Honour `Retry-After`. Back off. Do not hammer the endpoint. | | 500 | Unhandled server error | `ProblemDetails` JSON | Retry once after a few seconds. If it persists, escalate to the user. | ### Example 400 body ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "DomainId": ["DomainId must be between 1 and 12."], "Limit": ["Limit must be between 1 and 300."] } } ``` ## Sorting & pagination notes - Default sort: `ORDER BY <chosen BSR column> ASC` (lower BSR = better seller appears first). - Historical mode (`bsrType=Historical` + `bsrYear`/`bsrMonth`): sorted by the historical monthly BSR rank ASC. - Tie-breaker is non-deterministic — books with identical BSR may appear in different orders across page fetches. Warn the user if they need a perfectly stable ordering. - No `orderBy` parameter is exposed. ## Limits - **Rate limit**: 30 requests/minute per API key. Shared across all callers using the same key. - **Body size**: 128 KB max. - **Page size**: 300 results max per request (`limit ≤ 300`). - **Offset cap**: 100,000 (deep pagination beyond this is not supported — refine filters instead).


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)