Skip to main content
POST
The Index has two verticals behind one publisher key: Your agent decides which one to call from the user’s intent. A simple rule that works: if the answer is software or a service the user signs up for, use /search; if it would be shipped to a doorstep, use /shop/search. Both accept the same identity/attribution fields, so the plumbing is shared.

Authentication

Same key as every other Index endpoint: your publisher API key as the X-API-Key header or as platform_api_key in the body. Any production-approved publisher key works; there is no separate Index or shopping entitlement. Requests without a key get 401; blocked publishers get 403.

Request

string
Natural-language description of the product (up to 1000 chars). Matched with full-text search across name, brand, category and description, so concrete nouns beat vibes: “wide toe box running shoes” outperforms “something comfy for my feet”. Required unless context.messages is sent.
object
Optional hints from the conversation. context.style (alias context.vibe) is a free-text string that adds a soft ranking boost, e.g. "neutral cushioned daily trainer" or "minimalist Scandinavian". context.messages ([{role, content}]) lets the Index work out the product need from the conversation itself when there is no explicit query; the response’s query field shows what it inferred. Other keys are stored with the search and are ignored today.
integer
default:"5"
1–25. Ask for what you will actually show; each result mints a tracked click link.
boolean
default:"true"
Run the retrieved set through the LLM reranker, which keeps only products that fit the ask and may return nothing. false returns raw keyword ranking with no LLM on the hot path.

Structured filters

Filters are applied before ranking, so they are hard constraints, not preferences. Only pass them when the user stated them.
number
Inclusive lower bound on the current (sale) price, in currency.
number
Inclusive upper bound on the current (sale) price, in currency.
string
default:"USD"
ISO 4217 code. Price filters and returned prices are in this currency. The catalog is predominantly USD today.
string[]
Up to 20 brand names, matched case-insensitively ("Nike" and "nike" are the same). Use it when the user names a brand; leave it out for “any brand”.
string[]
Up to 20 merchant names ("Nike", "Walmart", "Lululemon"), matched case-insensitively. Use it when the user wants to buy from a specific store.
boolean
default:"false"
Only return products currently discounted below their regular price.
string[]
Up to 10 colour words ("black", "navy"). Listings are product-level, so this matches the colour named in the product title.
string[]
Up to 10 sizes as the merchant lists them ("10", "M", "32x34"). Listings that name a different size are dropped; listings that name no size are kept, since the size may be picked at checkout.
string
men, women or kids.
string[]
Up to 10 of: apparel, footwear, furniture_decor, home_kitchen, beauty_personal_care, electronics_computing, jewelry_watches, sports_outdoors, tools_hardware, toys_kids, baby, pets, auto, books_media, other. Anything else is a 422.
Region is implied by the merchant catalog (US retailers today); there is no region filter.

Identity and attribution (optional)

Identical to /search. Send them if you want per-user attribution and conversion reporting.
string
Your identifier for this conversation, up to 200 characters on this endpoint.
string
Your stable identifier for this user.
string
SHA-256 of the user’s lowercased, trimmed email.
object
Free-form key/values stored with the search (surface, locale, A/B arm…). Never put secrets or raw PII here.

Response

string
Always "consumer" on this endpoint. Use it to branch your rendering if you funnel both endpoints through one handler.
string
Response contract version, currently "2026-09-09".
string
Unique ID for this search. Log it; it is the join key for click and conversion reporting.
string
The product ask that was searched: your query, or what the Index inferred from context.messages.
object | null
products[0], or null when nothing fits.
string
One sentence on why the top product was picked, or why nothing was.
object[]
Ranked best-first. Empty when nothing in stock matches the query and every filter; loosen a filter or rephrase before telling the user there is nothing.
integer
products.length.
object[]
Only non-empty when products is empty: up to 5 plain web listings for the same ask so the agent still has something to show. Each has name, brand, price, currency, description, image_url, url and site. price and currency are null unless the page states both; never invent one. These carry no tracked link and no checkout, so link url directly, say they are from the web rather than the catalog, and do not offer to buy them.
string
How the list was ordered. "llm": the reranker kept only genuine fits, best relevance tier first, and inside a tier the listing that pays your platform more. "retrieval": raw keyword order (rerank: false, or the reranker was unavailable). Informational.
object
What the agent could filter on next, observed across the retrieved set: brands, merchants, categories (top 10 each) and price_range ({min, max} or null).
There is no per-result reasoning here: products are ranked, not reasoned about one at a time.

Examples

Natural-language query with a price cap

Example response, trimmed to one product:
recommendation (omitted above) repeats products[0]; checkout is null for catalog listings and populated for Shopify storefronts. The full two-product response, the zero-match response with web_results, and a 422 are in the response panel at the top of this page.

Brand, price band and a style hint

The user said “I want Nikes or Brooks, something cushioned for daily miles, under $180.”
In testing this returned three Nike Journey Run road-running variants at 9595–100 sold by Nike; without brands the same query returns marketplace listings first.

Only deals

Rendering results

Show name, brand, merchant, price (with regular_price struck through when on_sale), image_url, and link the whole card to click_url. In chat surfaces the click_url alone is enough; it unfurls into the card. Do not rewrite or shorten click_url, and do not link to the merchant page directly; the redirect is what attributes the sale to your platform.

Buying on the user’s behalf

When a product carries a non-null checkout, your agent can complete the purchase itself instead of handing the user a link. Call the merchant’s endpoint (an MCP server) directly and pass the attribution object through unchanged; that is what attributes the order to your platform when no click happens. Gravity never sees the payment.
The merchant returns a checkout (status: "requires_escalation" until buyer, delivery and payment are supplied) and echoes attribution back. Continue with update_checkout and complete_checkout per the merchant’s UCP profile at https://{store}/.well-known/ucp, or hand the user checkout.continue_url to finish in a browser. Attribution rules:
  • Always send attribution exactly as returned; do not mint your own click IDs.
  • Get the user’s explicit consent before complete_checkout; it charges the payment method.
  • If the merchant rejects the call or checkout is null, use click_url as usual.

Deciding between /search and /shop/search

An agent-side router in three lines of prompt usually suffices. Over REST the two verticals are separate endpoints; over MCP there is one tool, gravity_index_search, and the consumer vertical is selected with vertical: "consumer".
If your platform only serves one audience, wire only that endpoint; there is no penalty for ignoring the other vertical.

Errors