{
  "info": {
    "name": "Indexa API",
    "description": "Indexa — managed search & discovery for Shopify storefronts.\n\nSet INDEXER_URL to your own host and PUBLIC_KEY to your tenant browser key before sending requests.\nStorefront endpoints require X-Indexa-Key: use PUBLIC_KEY (pk_) in the browser or SECRET_KEY (sk_) only on your server. When an origin allow-list is configured, include a matching Origin header in browser-key requests from Postman.\n\nFull reference: https://docs.indexa.now",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "INDEXER_URL",
      "value": "https://YOUR-SLUG.api.indexa.now",
      "type": "string",
      "description": "Your indexer host, one per store. Replace YOUR-SLUG before sending anything: the placeholder is a real hostname shape that resolves to nothing, so a request you forgot to configure fails at DNS instead of hitting someone else. Your host is in your welcome email and in the console."
    },
    {
      "key": "TYPESENSE_URL",
      "value": "https://YOUR-SLUG.search.indexa.now",
      "type": "string",
      "description": "Your search engine host. Only needed if you build engine queries yourself. Same rule: replace YOUR-SLUG."
    },
    {
      "key": "SEARCH_KEY",
      "value": "",
      "type": "string",
      "description": "Read-only, collection-scoped engine key. Safe in a browser. Only for direct engine access."
    },
    {
      "key": "PUBLIC_KEY",
      "value": "",
      "type": "string",
      "description": "Your store's browser key (pk_…), from the Indexa console. Goes in the theme app embed. Public by design — it ships in your pages."
    },
    {
      "key": "SECRET_KEY",
      "value": "",
      "type": "string",
      "description": "Your store's server-side key (sk_…). Never send it to a browser: it also lifts the per-IP rate limit."
    },
    {
      "key": "ADMIN_KEY",
      "value": "",
      "type": "string",
      "description": "Bearer token for /admin/*. Server-side only — never ship it to a storefront."
    },
    {
      "key": "LANG",
      "value": "it",
      "type": "string",
      "description": "Language / collection to query (it, en)."
    },
    {
      "key": "PRODUCT_ID",
      "value": "7523891",
      "type": "string",
      "description": "A product id, for the product-scoped endpoints."
    },
    {
      "key": "COLLECTION_HANDLE",
      "value": "new-arrivals",
      "type": "string",
      "description": "Handle of an existing Shopify collection for strategy operations."
    },
    {
      "key": "PICK_SLUG",
      "value": "homepage-hero",
      "type": "string",
      "description": "Slug of a curated list created in your console."
    }
  ],
  "item": [
    {
      "name": "Storefront (public)",
      "description": "Called straight from a shopper's browser. Every endpoint here takes your store's API key: pk_… from a browser, sk_… from your own server. CORS and per-IP rate limits still apply on top. The pk_ key ships inside your pages and is public by construction — what it buys is revocation without redeploying your theme, and traffic we can attribute to you; the sk_ key is real authentication, and additionally lifts the per-IP limit, which matters when one server fronts every shopper in the shop.",
      "item": [
        {
          "name": "GET /search",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Indexa-Key",
                "value": "{{PUBLIC_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/search?q=maglione%20verde%20xl&lang={{LANG}}",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "search"
              ],
              "query": [
                {
                  "key": "q",
                  "value": "maglione verde xl",
                  "description": "The shopper's query. An empty q returns an empty result unless a collection listing is requested.",
                  "disabled": false
                },
                {
                  "key": "lang",
                  "value": "{{LANG}}",
                  "description": "Language to search. Defaults to the store's first language.",
                  "disabled": false
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "1-based page number. Default 1.",
                  "disabled": true
                },
                {
                  "key": "per_page",
                  "value": "24",
                  "description": "Page size. Default 24, capped at 100.",
                  "disabled": true
                },
                {
                  "key": "collection",
                  "value": "giacche-uomo",
                  "description": "Render a category listing instead of a search: everything inside this collection handle, ordered by the same relevance, with merchandising looked up by collection rather than by query. Leave `q` empty. A handle that is not a valid Shopify handle matches nothing. On configured and enabled tenants, Indexa Intelligence may rerank a bounded leading window on page 1 with no query or explicit sort; hard collection boosts skip AI. Pins and filters are preserved. Failure retains deterministic order. This does not publish an order to Shopify.",
                  "disabled": true
                },
                {
                  "key": "sid",
                  "value": "a1b2c3d4",
                  "description": "Anonymous session ID, max 64 characters. Rotate after 30 minutes of inactivity; used for search-assisted attribution and co-view recommendations.",
                  "disabled": true
                },
                {
                  "key": "analytics",
                  "value": "false",
                  "description": "Set false to suppress search logging until analytics consent. Search still works.",
                  "disabled": true
                },
                {
                  "key": "sort",
                  "value": "price_asc",
                  "description": "Order results with relevance (default), price_asc, price_desc, discount_desc, newest or popularity. Declared scalar product metafields support mf_namespace_key:asc or :desc after reindex, e.g. mf_custom_priority:asc. Availability is validated against this tenant and language's live schema; unknown/malformed metafield sorts return 400. Unsupported preset names retain the relevance fallback. Explicit sorting replaces text relevance; pinned products stay pinned. Missing metafield values go last in either direction. Integer priorities use int64 within the JavaScript safe range; decimals use engine float precision and sufficiently close values may tie. Lists, structured values and variant metafields are excluded.",
                  "disabled": true
                },
                {
                  "key": "filter_vendor",
                  "value": "Nike,Adidas",
                  "description": "Facet filter on any indexed field. Comma-separated values are an OR within that field: filter_vendor=Nike,Adidas means Nike or Adidas. Repeatable, and every filter_* parameter is ANDed with the others: filter_vendor=Nike&filter_sizes=XL,L means Nike AND (XL or L). Field names are validated against ^[a-z][a-z0-9_]{0,40}$ and values are quoted, so neither can break out of the filter expression.",
                  "disabled": true
                },
                {
                  "key": "filter_not_vendor",
                  "value": "Nike",
                  "description": "Everything but these values: filter_not_vendor=Nike,Adidas excludes both. Same validation and same AND with the other filters.",
                  "disabled": true
                },
                {
                  "key": "filter_price_min",
                  "value": "50",
                  "description": "Numeric range on an indexed number field, either end optional: filter_price_min=50&filter_price_max=100 is 50 <= price <= 100. A bound that is not a finite number is dropped rather than guessed at, so a malformed URL narrows nothing instead of erroring. Note a field whose own name ends in _min or _max cannot be filtered for equality through this parameter.",
                  "disabled": true
                }
              ]
            },
            "description": "Run a search. Redirects, relevance weights, query understanding, merchandising and did-you-mean are applied server-side, in that order.\n\n`search_event_id` is an optional UUID for a logged search. Carry it with `query` on result clicks; previews, category listings and analytics=false omit it. Logging is best effort. Analytics zero results use displayed results after relaxation; original counts remain available for debugging.\n\n`redirect` is an answer, not a ranking: when a merchant has mapped the query to a page, `hits` is empty and `redirect` holds the target. Validate it against your own origin before navigating — the engine already refuses anything that is not a relative path or a same-origin URL.\n\nMetafields declared in your console are ordinary fields here: filter_mf_<namespace>_<key> (mfv_ for variant-level ones), with the same equality, OR, and filter_not_ forms, and matching is case-insensitive. They are always indexed as string lists, so the _min/_max range form does not apply to them — it would match nothing rather than error.\n\n`facets` carries value counts for the built-in facets, plus any metafield ticked as *Show as filter* in your console (up to 8) once it is in the index — that is what a filter sidebar is built from. Filtering a metafield never requires the tick; only knowing its values does.\n\nA filter naming a field the index does not have answers 400 `Unknown filter field: <name>` — a bookmarked URL carrying a facet that has since been renamed is the caller's mistake, not an outage, and dropping the clause instead would answer a filtered request with the whole catalogue.\n\nFilters combine as AND across parameters and OR within one field. OR *across different fields* is deliberately not expressible here: it needs a filter grammar, and a grammar on a browser-facing endpoint is a surface that cannot be closed again once it is open. If you need one, run the query server-side against `/admin/preview` with your admin key, which accepts a raw expression.\n\nPaging is capped: `page` × `per_page` may not exceed 1000, and a request past that answers 400 rather than silently returning an earlier page. Shoppers do not reach page 11; catalogue scrapers do nothing else. Narrow with filters instead.\n\n`didYouMean` is offered when fewer than 3 products match, and suggests a query shoppers have actually run — not a dictionary word.\n\n`understood` reports what was pulled out of the phrase into filters — size, colour and department — so you can show it back to the shopper and let them drop one.\n\n`relaxed` is present only when the exact query matched nothing and a weaker one was answered instead. It names what was given up, in the order the engine gives things up: trailing words first (a word the catalogue has no vocabulary for is usually a typo or a marketing phrase), then size, then colour. Never the department, and never a filter the shopper set themselves. Say it on the page — a list that silently ignores half the query reads as a broken filter."
          },
          "response": []
        },
        {
          "name": "GET /suggest",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Indexa-Key",
                "value": "{{PUBLIC_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/suggest?q=magl&lang={{LANG}}&limit=5",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "suggest"
              ],
              "query": [
                {
                  "key": "q",
                  "value": "magl",
                  "description": "What has been typed so far.",
                  "disabled": false
                },
                {
                  "key": "lang",
                  "value": "{{LANG}}",
                  "description": "Language. Defaults to the store's first language.",
                  "disabled": false
                },
                {
                  "key": "limit",
                  "value": "5",
                  "description": "How many suggestions. Default 5, capped at 20.",
                  "disabled": false
                }
              ]
            },
            "description": "Query suggestions for an autocomplete box, drawn from what shoppers on this store actually searched."
          },
          "response": []
        },
        {
          "name": "POST /events",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-Indexa-Key",
                "value": "{{PUBLIC_KEY}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{ \"product_id\": \"7523891\", \"event_type\": \"view\", \"lang\": \"it\", \"sid\": \"a1b2c3d4\" }",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{INDEXER_URL}}/events",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "events"
              ]
            },
            "description": "Interaction beacon. The only public endpoint that writes, and the only source of data for the popular and people-also-viewed models.\n\nResult clicks should include query (up to 512 chars) and search_event_id (UUID from /search). Optional quantity is a positive integer up to 100000. Both sid and session_id are supported.\n\nRevenue, unit_price, currency, order_id, source and created_at from this public endpoint are ignored, including with sk_ keys. Browser purchase events never contribute authoritative revenue or conversion. Send only after required analytics consent.\n\nVerified purchases come from POST /webhooks/orders/paid with Shopify raw-body HMAC, shop domain and orders/paid topic headers. The indexer acknowledges after duplicate-safe storage; failures return 503 for retry. Configure the tenant Shopify subscription and preserve consented indexa_session_id/indexa_lang cart attributes through checkout. No customer PII is stored. Revenue is paid merchandise after discounts, excluding tax/shipping; refunds and later edits are not adjusted in V1.\n\nSend one object or an array of up to 20. The response comes back before the write: a beacon must not hold a page open while it navigates away.\n\n`event_type` is one of view, click, add_to_cart, purchase. A missing key means view; a key that is present but not a string is rejected rather than coerced — junk filed under `view` would poison the most used signal in the table.\n\n`product_id` must match ^[A-Za-z0-9_-]{1,64}$. Numbers are accepted and stringified. An unknown `lang` falls back to the store's default.\n\nContent type is not checked: sendBeacon picks its own, and cannot retry a failed preflight."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Recommendations (public)",
      "description": "Seven models, one response shape. Same guards as search — CORS, per-IP rate limit — because a Liquid theme has no server to proxy through.\n\nEvery model answers `products[]`, already resolved. The popular, also-viewed, custom and complete-the-look models also keep `product_ids[]` for integrations that resolve ids themselves.\n\nOrder is the recommendation — a curated list is a merchant's running order, popular is a ranking — so it is preserved exactly. Sold-out products are dropped.\n\nAn unknown model under /reco/ returns 404, not an empty 200.",
      "item": [
        {
          "name": "GET /reco/best-sellers",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Indexa-Key",
                "value": "{{PUBLIC_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/reco/best-sellers?lang={{LANG}}&limit=8",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "reco",
                "best-sellers"
              ],
              "query": [
                {
                  "key": "lang",
                  "value": "{{LANG}}",
                  "description": "Language.",
                  "disabled": false
                },
                {
                  "key": "limit",
                  "value": "8",
                  "description": "Default 12, capped at 50.",
                  "disabled": false
                },
                {
                  "key": "collection",
                  "value": "donna",
                  "description": "Collection handle to rank within.",
                  "disabled": true
                }
              ]
            },
            "description": "Shopify's own best-selling ranking, optionally within one collection."
          },
          "response": []
        },
        {
          "name": "GET /reco/related/{productId}",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Indexa-Key",
                "value": "{{PUBLIC_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/reco/related/{{PRODUCT_ID}}?lang={{LANG}}&limit=10",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "reco",
                "related",
                "{{PRODUCT_ID}}"
              ],
              "query": [
                {
                  "key": "lang",
                  "value": "{{LANG}}",
                  "description": "Language.",
                  "disabled": false
                },
                {
                  "key": "limit",
                  "value": "10",
                  "description": "Default 12, capped at 50.",
                  "disabled": false
                },
                {
                  "key": "intent",
                  "value": "RELATED",
                  "description": "RELATED (default, auto-generated by Shopify) or COMPLEMENTARY (needs the Shopify Search & Discovery app configured). Anything else is read as RELATED.",
                  "disabled": true
                }
              ]
            },
            "description": "Shopify's product recommendations for one product."
          },
          "response": []
        },
        {
          "name": "GET /reco/complete-the-look/{productId}",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Indexa-Key",
                "value": "{{PUBLIC_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/reco/complete-the-look/{{PRODUCT_ID}}?lang={{LANG}}&limit=6",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "reco",
                "complete-the-look",
                "{{PRODUCT_ID}}"
              ],
              "query": [
                {
                  "key": "lang",
                  "value": "{{LANG}}",
                  "description": "Configured catalog language for this tenant.",
                  "disabled": false
                },
                {
                  "key": "limit",
                  "value": "6",
                  "description": "Default 12, capped at 50; a rule can produce at most 18 products.",
                  "disabled": false
                }
              ]
            },
            "description": "Indexa-generated complementary products from merchant-defined categories and weighted field rules.\n\nConfigure and enable rules in the Complete the look admin section. Product scope wins over custom-pick list, category and default; priority then rule ID breaks ties. One rule applies, with no merging.\n\nRanks up to 200 available candidates per destination category plus explicit pins. Weighted equality, shared values, directional compatibility, numerical distance and ratio determine ranking. Required criteria, exclusions and availability remain mandatory for pins.\n\nNo matching rule or eligible products returns empty products and product_ids arrays. Invalid product ID or catalog language returns 400; unavailable generation returns 503. Internal weights and explanations are never public.\n\nSeparate from Shopify COMPLEMENTARY: no Shopify metafield writes or automatic fallback. Playground Complete the look uses this Indexa endpoint and saved enabled rules. Use data-indexa-reco=complete-the-look in Liquid or type=complete-the-look in the Hydrogen example proxy; storefront placement is opt-in."
          },
          "response": []
        },
        {
          "name": "GET /reco/popular",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Indexa-Key",
                "value": "{{PUBLIC_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/reco/popular?lang={{LANG}}&limit=12",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "reco",
                "popular"
              ],
              "query": [
                {
                  "key": "lang",
                  "value": "{{LANG}}",
                  "description": "Language.",
                  "disabled": false
                },
                {
                  "key": "days",
                  "value": "30",
                  "description": "Window in days. Default 30, capped at 365.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "12",
                  "description": "Default 12, capped at 50.",
                  "disabled": false
                }
              ]
            },
            "description": "Most-viewed products in a rolling window, from your own traffic.\n\nEmpty until view events arrive. An empty list is the correct answer for a store with no traffic yet, not an error."
          },
          "response": []
        },
        {
          "name": "GET /reco/also-viewed/{productId}",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Indexa-Key",
                "value": "{{PUBLIC_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/reco/also-viewed/{{PRODUCT_ID}}?lang={{LANG}}&limit=12",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "reco",
                "also-viewed",
                "{{PRODUCT_ID}}"
              ],
              "query": [
                {
                  "key": "lang",
                  "value": "{{LANG}}",
                  "description": "Language.",
                  "disabled": false
                },
                {
                  "key": "limit",
                  "value": "12",
                  "description": "Default 12, capped at 50.",
                  "disabled": false
                }
              ]
            },
            "description": "Products co-viewed in the same session as this one.\n\nNeeds view events carrying a session id (`sid`)."
          },
          "response": []
        },
        {
          "name": "GET /reco/custom/{slug}",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Indexa-Key",
                "value": "{{PUBLIC_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/reco/custom/{{PICK_SLUG}}?lang={{LANG}}&limit=12",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "reco",
                "custom",
                "{{PICK_SLUG}}"
              ],
              "query": [
                {
                  "key": "lang",
                  "value": "{{LANG}}",
                  "description": "Language.",
                  "disabled": false
                },
                {
                  "key": "limit",
                  "value": "12",
                  "description": "Default 12, capped at 50.",
                  "disabled": false
                }
              ]
            },
            "description": "A curated list, in the order a merchant arranged it in the console.\n\nCreate and edit lists in /custom-picks using catalog search, product thumbnails and drag-and-drop order. Use the saved slug in this endpoint; list creation does not install a storefront carousel. Missing or unavailable products are omitted from resolved products."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Admin",
      "description": "Server-side only, behind a bearer token. Never call these from a storefront.",
      "item": [
        {
          "name": "GET /admin/preview",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/admin/preview?lang={{LANG}}",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "preview"
              ],
              "query": [
                {
                  "key": "q",
                  "value": "maglione verde xl",
                  "description": "The query to try.",
                  "disabled": true
                },
                {
                  "key": "lang",
                  "value": "{{LANG}}",
                  "description": "Language.",
                  "disabled": false
                },
                {
                  "key": "collection",
                  "value": "giacche-uomo",
                  "description": "Preview a category page instead of a search.",
                  "disabled": true
                },
                {
                  "key": "per_page",
                  "value": "24",
                  "description": "Default 24.",
                  "disabled": true
                },
                {
                  "key": "sort",
                  "value": "price_asc",
                  "description": "Same fixed presets and validated dynamic metafield sorts as /search.",
                  "disabled": true
                },
                {
                  "key": "filter_vendor",
                  "value": "Nike,Adidas",
                  "description": "Every filter form the public /search takes — equality, filter_not_<field>, filter_<field>_min / _max — works here too.",
                  "disabled": true
                },
                {
                  "key": "raw_filter",
                  "value": "vendor:=Nike || discount_pct:>40",
                  "description": "A raw Typesense filter_by expression, ANDed with everything else. Accepted only on this route, because it is interpreted rather than bound: on a browser-facing endpoint it would hand any visitor arbitrary filters and arbitrarily expensive queries. Use it for the OR across different fields that the filter_* grammar cannot express.",
                  "disabled": true
                }
              ]
            },
            "description": "Run a search the way a shopper would get it, without recording it, and get back what the engine did. This is what the console's playground calls.\n\nNever written to `search_events`. A merchant testing a phrase twenty times would otherwise invent a popular query and move the suggestions their own shoppers see.\n\n`debug` exposes this store's configuration — effective weights, the merchandising rule matched — which is why it needs the admin key rather than being a flag on the public endpoint.\n\nA malformed `raw_filter` comes back as 400 carrying the engine's own message (`Could not find a filter field named \\`zzz\\` in the schema`) plus the expression you sent. The field is written by iterating, so the error has to be readable; every other failure is still a 500."
          },
          "response": []
        },
        {
          "name": "GET /admin/collections",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/admin/collections",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "collections"
              ]
            },
            "description": "List Shopify collections, saved strategies, readiness and preparation progress.\n\nUse the tenant TYPESENSE_ADMIN_API_KEY as ADMIN_KEY. Public search keys and storefront API keys are not accepted. JSON bodies are limited to 64 KiB.\n\nCollection handles must match ^[a-z0-9][a-z0-9_-]{0,254}$. Read the current revision before saving or publishing. Errors use {error:{code,message}}: 400 invalid, 401 unauthorized, 409 preparation/revision/lease/drift conflicts, 413 payload_too_large, 503 unavailable.\n\nEach collection has id, handle, title, sortOrder, productCount and automated. strategy is null and revision is 0 before the first save. Setup failures are explicit errors, not an empty collection list."
          },
          "response": []
        },
        {
          "name": "POST /admin/collections/sync",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/admin/collections/sync",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "collections",
                "sync"
              ]
            },
            "description": "Prepare the tenant: reconcile webhooks, backfill paid orders, sweep inventory and update indexed stock.\n\nReturns 202 while preparation runs asynchronously. Poll GET /admin/collections for sync progress. Completion requires status=succeeded and completed_at, plus ready prerequisites. Retry failed or interrupted runs; this does not enable or publish a strategy.\n\nPOST /admin/collections/sync is reserved for preparation; GET at that path reads a collection whose handle is sync."
          },
          "response": []
        },
        {
          "name": "GET /admin/collections/{handle}",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/admin/collections/{{COLLECTION_HANDLE}}",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "collections",
                "{{COLLECTION_HANDLE}}"
              ]
            },
            "description": "Read a collection, saved strategy, revision, live preview and operational status.\n\nIllustrative unconfigured response. A successful preview is populated; if strict signals cannot be loaded it is null with preview_error. Collection status is disabled, preparation_required, ready_to_publish, published, unresolved, drift or restore_required.\n\npreview includes collection, config, revision, currentIds, proposedIds, moves, changedProducts, ranking and products. products is keyed by ID with available presentation fields such as title and image_url, including unindexed Shopify members. ranking reports orderedIds, per-product scores/contributions/rules, signal coverage and unevaluatedIds. Compatible services also return preview.provenance: requestedMode and usedMode (rules or intelligence), nullable provider/modelKey/checkpointKey/featureSchemaVersion, latencyMs, comparisons, fallback, reason, candidateCount and rerankedCount. candidateCount counts indexed products in the bounded strategy window; rerankedCount counts products evaluated by the model, not changed positions. Rules scores describe candidate generation, not AI feature attribution."
          },
          "response": []
        },
        {
          "name": "POST /admin/collections/{handle}/preview",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"config\": {\n    \"enabled\": false,\n    \"sorters\": [\n      {\n        \"param\": \"units_sold\",\n        \"direction\": \"desc\",\n        \"weight\": 100\n      }\n    ],\n    \"lookback_days\": 30,\n    \"depth\": 200,\n    \"rules\": {\n      \"push_down_tags\": [],\n      \"push_up_tags\": [],\n      \"new_days\": 0,\n      \"unavailable_last\": true,\n      \"pins\": [],\n      \"tag_scores\": {},\n      \"vendor_scores\": {}\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{INDEXER_URL}}/admin/collections/{{COLLECTION_HANDLE}}/preview",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "collections",
                "{{COLLECTION_HANDLE}}",
                "preview"
              ]
            },
            "description": "Preview an unsaved configuration without saving it or changing Shopify.\n\nConfig fields: enabled (boolean), optional ranking_mode (rules or intelligence; defaults to rules when omitted; null/unknown values rejected), sorters (1–18 unique signals with direction asc/desc and weight 0–100; at least one positive), lookback_days (7 or 30), depth (1–20,000), and rules. Signal names: price, compare_at_price, discount_pct, days_since_published, units_sold, revenue, orders_count, views, add_to_cart, ctr, conversion_rate, revenue_per_view, inventory, in_stock_variants_ratio, days_to_sell_out, tag_score_adjust, vendor_score_adjust, random. Rules contain push_down_tags, push_up_tags, new_days, unavailable_last, pins ({id: numeric product ID string, position: one-based integer}), tag_scores and vendor_scores. Unknown fields are rejected.\n\nThe example is a disabled draft using units sold descending. Rules rank the complete collection. Intelligence only reranks a bounded leading window of at most 32 positions within depth, further limited by tenant topN and fixed/unindexed products. Exact pins, effective priority groups, membership and the remaining order survive reranking. A disabled draft can preview AI if tenant/deployment Intelligence is enabled. Missing strict signal data returns 503 rather than a fabricated score.\n\nSuccessful Intelligence provenance can include productMovements: [{id, fromPosition, toPosition}], with one-based final constrained positions relative to the rules-only baseline. This is distinct from preview.moves/changedProducts, which compare against current Shopify order. Optional productEvidence contains [{id, wins, comparisons, opponents:[{id, preferred}], features:[{name, source, value}], salesLookbackDays}]. value is numeric or boolean; at most eight actual mapped inputs are exposed per evaluated product. wins/opponents describe observed tournament decisions, not causal feature importance. Evidence is admin-only and omitted on fallback; legacy services may omit provenance entirely. A successful inference can evaluate products without moving any of them.\n\nCollection inference has a separate maximum 45-second deadline. Inspect usedMode and fallback rather than assuming requested Intelligence was used. Shopper inference remains bounded by at most 1,000 ms. On older services omit ranking_mode for Rules compatibility; Intelligence requires provenance support."
          },
          "response": []
        },
        {
          "name": "PUT /admin/collections/{handle}",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"config\": {\n    \"enabled\": false,\n    \"sorters\": [\n      {\n        \"param\": \"units_sold\",\n        \"direction\": \"desc\",\n        \"weight\": 100\n      }\n    ],\n    \"lookback_days\": 30,\n    \"depth\": 200,\n    \"rules\": {\n      \"push_down_tags\": [],\n      \"push_up_tags\": [],\n      \"new_days\": 0,\n      \"unavailable_last\": true,\n      \"pins\": [],\n      \"tag_scores\": {},\n      \"vendor_scores\": {}\n    }\n  },\n  \"revision\": 0\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{INDEXER_URL}}/admin/collections/{{COLLECTION_HANDLE}}",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "collections",
                "{{COLLECTION_HANDLE}}"
              ]
            },
            "description": "Save a strategy with revision checking. Disabled drafts do not reorder Shopify.\n\nUse revision 0 only for the first save; subsequently use the revision from detail/save. A stale revision returns 409.\n\nEnabling requires successful data preparation and current prerequisites. An enabled strategy opts into scheduled and event-driven publication; save is not an immediate publish command. The config accepts the same ranking_mode as preview. Saving a disabled Intelligence draft neither enables tenant AI nor changes Shopify order."
          },
          "response": []
        },
        {
          "name": "POST /admin/collections/{handle}/publish",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"revision\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{INDEXER_URL}}/admin/collections/{{COLLECTION_HANDLE}}/publish",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "collections",
                "{{COLLECTION_HANDLE}}",
                "publish"
              ]
            },
            "description": "Publish the saved strategy to Shopify using the expected revision.\n\nRequires the current saved revision. Enabled publication needs successful preparation and prerequisites. Optional minChangedProducts is an integer from 1 to 20,000.\n\nMay return status=skipped with reason disabled, unchanged or below_threshold. First publication captures the original baseline and switches Shopify sort mode to MANUAL. Order is shared across markets. Drift or an unresolved mutation blocks automatic overwrite. Intelligence mode requires fresh successful Intelligence inference, including after sort/membership recomputation; fallback blocks publication. Preview is not a frozen publication snapshot. Successful publication stores provenance in strategy.last_ranking; restoring the baseline clears it."
          },
          "response": []
        },
        {
          "name": "POST /admin/collections/{handle}/restore",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/admin/collections/{{COLLECTION_HANDLE}}/restore",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "collections",
                "{{COLLECTION_HANDLE}}",
                "restore"
              ]
            },
            "description": "Restore the captured original Shopify order and sort mode, then disable the strategy.\n\nSend no body or an empty JSON object. Restore is explicit and does not require a new successful preparation. It needs the captured original baseline and access to Shopify/storage. Retry an interrupted restore to reconcile its state; do not force another publication."
          },
          "response": []
        },
        {
          "name": "POST /admin/reindex",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/admin/reindex",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "reindex"
              ]
            },
            "description": "Full reindex of every language, run asynchronously."
          },
          "response": []
        },
        {
          "name": "POST /admin/search-key",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{ADMIN_KEY}}"
              }
            ],
            "url": {
              "raw": "{{INDEXER_URL}}/admin/search-key",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "admin",
                "search-key"
              ]
            },
            "description": "Mint a fresh read-only engine key, scoped to your collections."
          },
          "response": []
        },
        {
          "name": "GET /health",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{INDEXER_URL}}/health",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "health"
              ]
            },
            "description": "Liveness, and the languages this indexer serves."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Webhooks",
      "description": "Shopify pushes catalogue changes here. You register the URLs once at onboarding; they are documented because they are part of the surface, not because you call them.\n\nSigned with `X-Shopify-Hmac-Sha256` and verified against your webhook secret; anything unsigned is rejected with 401.\n\nOn collection-enabled tenants, product and collection webhooks return 200 after persisting a durable job, before catalog and strategy processing. Persistence failure returns a retryable error. Legacy optional deployments may use bounded in-memory processing.",
      "item": [
        {
          "name": "POST /webhooks/products/{create|update|delete}",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{INDEXER_URL}}/webhooks/products/create",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "webhooks",
                "products",
                "create"
              ]
            },
            "description": "Upserts or deletes one product across every language collection."
          },
          "response": []
        },
        {
          "name": "POST /webhooks/collections/{update|delete}",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{INDEXER_URL}}/webhooks/collections/update",
              "host": [
                "{{INDEXER_URL}}"
              ],
              "path": [
                "webhooks",
                "collections",
                "update"
              ]
            },
            "description": "Keeps collection membership honest between reindexes — an edited smart-collection rule changes which products belong without touching any product."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Engine (direct)",
      "description": "The search engine underneath, reachable with a read-only key. Use it when you need a query shape /search does not expose — everything else is better served by /search, which applies redirects, weights, query understanding and merchandising for you.",
      "item": [
        {
          "name": "POST $TYPESENSE_URL/multi_search",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-TYPESENSE-API-KEY",
                "value": "{{SEARCH_KEY}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"searches\": [{\n    \"collection\": \"products_it\",\n    \"q\": \"maglione verde\",\n    \"query_by\": \"title,vendor,model_code,sku,tags,primary_category\",\n    \"query_by_weights\": \"6,3,4,4,2,2\",\n    \"sort_by\": \"_text_match:desc,score:desc\",\n    \"per_page\": 12\n  }]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{TYPESENSE_URL}}/multi_search",
              "host": [
                "{{TYPESENSE_URL}}"
              ],
              "path": [
                "multi_search"
              ]
            },
            "description": "Raw engine query. One or several searches per call."
          },
          "response": []
        }
      ]
    }
  ]
}
