On this page
For Developers
Integrate ProductIndex into your app or AI agent — your users get AI product search across 858+ synthesized profiles in 41 categories. Your agent gets real-time pricing via the RFQ marketplace: consumer agents can negotiate prices with verified producers on behalf of your users. Integration surfaces: 6 MCP tools, per-product endpoints in HTML / Markdown / JSON-LD, and an A2A-compatible agent card. Free tier available; OAuth 2.1 handles sign-in — clients like Grok and Claude Desktop manage it automatically.
Quick Reference
| MCP endpoint | POST https://mcp.productindex.ai/mcp/ |
| Protocol | MCP — legacy (through 2025-11-25) and 2026-07-28, served automatically per request · Streamable HTTP |
| Auth header | Authorization: Bearer <access_token> |
| Scope | mcp |
| Rate limit | 500 requests / day per authenticated user |
| Authorize URL | https://productindex.ai/oauth/authorize/ |
| Token URL | POST https://productindex.ai/api/oauth/token/ |
| Client registration | CIMD (preferred) or DCR at POST https://productindex.ai/api/oauth/register/ |
| OAuth server metadata | /.well-known/oauth-authorization-server.json |
| MCP descriptor | /.well-known/mcp.json |
| Tools | 29 total — 10 consumer, 19 producer (catalog, pricing, team, enrichment). Full list: /docs/mcp |
| CIMD supported | Yes — client_id_metadata_document_supported: true in server metadata |
29 tools over Streamable HTTP — the server serves both the legacy protocol family
(through MCP 2025-11-25) and 2026-07-28 automatically per request, depending on how your
client shapes its handshake. All requests are POST to
https://mcp.productindex.ai/mcp/ with a JSON-RPC 2.0 body.
OAuth 2.1 Bearer token required on every request.
| Endpoint | POST https://mcp.productindex.ai/mcp/ |
| Protocol | MCP — legacy (through 2025-11-25) and 2026-07-28, served automatically · Streamable HTTP (POST only) |
| Auth | Authorization: Bearer <access_token> — OAuth 2.1 required |
| Descriptor | /.well-known/mcp.json |
Core Search Tools
The 6 consumer search tools below cover the common case. ProductIndex has 29 tools total
(10 consumer — including saved-product management and the bundle-aware
product_curator — plus 19 producer tools for catalog, pricing, team, and
enrichment management); see the complete Consumer/Producer breakdown at
/docs/mcp.
| Tool | Description |
|---|---|
list_categories | All 41 categories with product counts. No parameters. |
list_category_products | Paginated product list for a category. Params: category (required), limit, offset, sort, on_sale, include_discontinued. |
get_product | Full product profile by slug. Params: category, slug (both required), sections (string array — subset retrieval). |
search_products | Semantic search via pgvector + Cohere reranking with keyword fallback. Params: query (required), category, min_price, max_price, sort, on_sale, include_discontinued, limit. |
find_similar | Nearest-neighbour similarity to a known product. Params: category, slug (both required), max_price, limit. |
find_retailers | Merchants carrying a product — triggers RFQ fanout and returns ranked offers with affiliate-attributed buy links. Params: query (required), product_slug, max_results. |
Request / Response Example
Every call is a POST to the same endpoint. Send initialize first, then any tool call.
// 1. Initialize
POST https://mcp.productindex.ai/mcp/
Authorization: Bearer <access_token>
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-app","version":"1.0"}}}
// 200 OK
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"ProductIndex","version":"1.0.0"}}}
// 2. Tool call
POST https://mcp.productindex.ai/mcp/
Authorization: Bearer <access_token>
Content-Type: application/json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_products","arguments":{"query":"espresso machine under $500","limit":5}}}
// 200 OK
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"..."}]}} Full setup instructions for Grok, Claude Desktop, Cursor, and Windsurf: MCP Server documentation →
Multi-format content endpoints
Every product and category is available in machine-readable formats for direct RAG ingestion, structured data pipelines, and LLM agents that prefer URL-based retrieval over the MCP server. No authentication required.
| Format | URL pattern | Use case |
|---|---|---|
| HTML — product | /p/{category}/{slug} | Human-readable product page |
| Markdown — product | /p/{category}/{slug}.md | LLM-friendly; verdict, review themes, specs, images, where-to-buy. noindex. |
| JSON-LD — product | /p/{category}/{slug}.json | Schema.org Product object. noindex. |
| HTML — category | /c/{category} | Human-readable category listing with all products |
| Markdown — category | /c/{category}.md | Full dump of every product profile in the category as a single document — suitable for bulk RAG ingestion. noindex. |
ProductIndex publishes a Google Agent-to-Agent (A2A) compatible agent card describing its skills and capabilities in machine-readable JSON. Read automatically by A2A-compatible orchestrators.
| Card URL | /.well-known/agent.json |
| Skills | Product recommendation · Product lookup · Category browse · Product offers · Request for Quote · What changed |
| Registry | Pending — Google A2A registry and Linux Foundation AI Agent Registry |
RFQ Integration — Level 2 Producer Agents
If you operate a retailer or manufacturer and want to receive live Request-for-Quote (RFQ) calls from consumer agents via ProductIndex, register your agent endpoint in the Producer Dashboard → Settings → Agent Integration. ProductIndex will fan out to your endpoint when a consumer agent submits an RFQ for a product you carry.
RFQ Request — What Your Agent Receives
ProductIndex calls your registered offer endpoint with a JSON-RPC 2.0 POST request:
{
"jsonrpc": "2.0",
"method": "message/send",
"id": "<rfq_session_uuid>",
"params": {
"message": {
"messageId": "<uuid>",
"role": "user",
"parts": [{ "kind": "text", "text": "Request for quote: Baratza Encore ESP, qty 1, ship to CA 94103" }],
"metadata": {
"skill": "respond-to-rfq",
"rfq_id": "<rfq_session_uuid>",
"product_slug": "coffee-grinders/baratza-encore-esp",
"quantity": 1,
"delivery_region": "CA-US",
"deadline_iso": "2026-06-01T12:00:00Z",
"requestor": "https://productindex.ai/.well-known/agent.json"
}
}
}
} | Field | Type | Description |
|---|---|---|
metadata.product_slug | string | Canonical ProductIndex slug for the requested product |
metadata.quantity | integer | Units requested (default 1) |
metadata.delivery_region | string | Postal region in STATE-COUNTRY format (e.g. CA-US) |
metadata.deadline_iso | ISO 8601 | Latest acceptable delivery date (optional) |
RFQ Response — What You Must Return
Respond with a JSON object (not JSON-RPC envelope — bare object only) within 25 seconds. ProductIndex fans out RFQs to registered agent endpoints asynchronously — the consumer's agent gets an immediate task handle and doesn't block on your response time, so you have more room to work than a synchronous request-response cycle would suggest:
{
"price_usd": 169.99,
"list_price_usd": 189.99,
"in_stock": true,
"estimated_shipping_days": 3,
"shipping_cost_usd": 0,
"offer_id": "<uuid>",
"offer_expires_at": "2026-06-01T12:00:00Z",
"claim_url": "https://yourstore.com/cart?offer=<uuid>",
"terms": "Free returns within 30 days"
} | Field | Required | Description |
|---|---|---|
price_usd | Yes | Your offered price in USD |
in_stock | Yes | Boolean — false causes your offer to be deprioritized |
claim_url | Yes | URL consumer agent can redirect to for purchase. Must be https://. |
offer_expires_at | Yes | ISO 8601 — when the quoted price expires. Max 24 hours from now. |
shipping_cost_usd | No | Shipping cost (0 for free shipping). Omit if unknown — you will rank lower. |
list_price_usd | No | Original MSRP if your offer is discounted |
estimated_shipping_days | No | Business days to deliver. Omit if unknown. |
terms | No | Short return/warranty/conditions string shown to consumer |
Offers are ranked by total cost (price_usd + shipping_cost_usd). Out-of-stock
offers are demoted regardless of price. Responses that arrive after 25 seconds are dropped.
Authentication
Every RFQ call from ProductIndex carries authentication headers so your server can verify the request is genuine. Choose a scheme, generate your own secret, and register it in the Producer Dashboard → Settings → Agent Integration. ProductIndex encrypts and stores the secret — it is never returned after registration.
| Scheme | You generate | Headers sent by ProductIndex | How to verify |
|---|---|---|---|
| Bearer token | Any secret string ≥ 16 chars (use a random token) | Authorization: Bearer <your-token> | Compare header value against your stored token with a constant-time comparison |
| HMAC-SHA256 | A random signing secret ≥ 16 chars (32+ bytes of entropy recommended) | X-ProductIndex-Signature: <hex>X-ProductIndex-Timestamp: <unix_seconds> | Recompute HMAC and compare — see spec below |
Generating a secret
// Node.js — generate a cryptographically random secret
import { randomBytes } from "crypto";
const secret = randomBytes(32).toString("hex"); // 64-char hex string
console.log(secret); // paste this into the dashboard Go to Producer Dashboard → Settings → Agent Integration, select your auth type, paste your secret, and save. ProductIndex will use it to sign all outbound RFQ calls to your endpoint from that point forward.
HMAC Verification
Signature is HMAC-SHA256(secret, "${timestamp}.${rawBodyString}") — the Unix
timestamp in seconds, a literal period, then the raw request body string exactly as received.
Reject requests where |now - timestamp| > 300 seconds (5-minute replay window).
// Node.js verification example
import { createHmac, timingSafeEqual } from "crypto";
function verifyProductIndexRequest(secret, req) {
const timestamp = parseInt(req.headers["x-productindex-timestamp"], 10);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${req.rawBody}`)
.digest("hex");
// Use timingSafeEqual to prevent timing attacks
return timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-productindex-signature"]));
} Agent Card Requirement
Your agent card (at /.well-known/agent.json on your verified domain) must
declare the respond-to-rfq skill. Minimum declaration:
{
"skills": [{
"id": "respond-to-rfq",
"description": "Responds to price quote requests with current pricing, availability, and shipping terms"
}]
} ProductIndex fetches and validates this card when you register your agent card URL. The card must be on the same domain as your verified producer organization.
ProductIndex derives your RFQ endpoint from your agent card URL by taking the domain root
and appending /message:send. If your card is at
https://mystore.com/.well-known/agent.json, ProductIndex will POST RFQ
requests to https://mystore.com/message:send. Your server must expose a
POST /message:send route at the domain root.
Timeout and Error Handling
Return HTTP 200 with a valid JSON response body within 25 seconds. If you cannot fulfill the
RFQ (out of stock, product not carried), return HTTP 200 with
{"in_stock": false, "price_usd": 0, "claim_url": "", "offer_expires_at": ""}
rather than a non-200 status. Repeated timeouts or errors degrade your integration status
and reduce your ranking priority.
Your endpoint is called from an async continuation, not the consumer agent's original
request — ProductIndex acknowledges the RFQ immediately with an A2A Task handle
(status.state: "submitted"), fans out to every registered producer for that
product concurrently, and the consumer agent polls tasks/get until the task
reaches completed. Your 25-second window is bounded by ProductIndex's own
Lambda execution limit, not by how long a human shopper is willing to wait on a page load.
Full request/response schemas, status codes, and validation rules for the routes below are published as an OpenAPI 3.1 spec at /openapi.json — this section covers the common cases. Producer routes (offers, org integration) require a producer account; see Producer Dashboard → Settings to create one.
Search
POST /api/search/ runs the same semantic search pipeline behind the site's own
search bar — pgvector + BM25 with Cohere reranking. No authentication required.
// Request
{
"query": "quiet coffee grinder under $200"
}
// Response — 200
{
"query_id": "<uuid>",
"results": [
{
"slug": "baratza-encore-esp",
"title": "Baratza Encore ESP",
"category": "coffee-grinders",
"verdict": "The best burr grinder for espresso under $200.",
"price_range": "$179–$199",
"url": "/p/coffee-grinders/baratza-encore-esp",
"similarity": 0.87,
"is_sponsored": false
}
]
}
A signed-in, email-verified caller can add "bundle": true to request a
multi-category bundle plan (e.g. "full kitchen set, $900 budget") instead of a flat result
list — see /openapi.json for the bundle response shape.
Offers
Producer-managed promotional offers on your claimed products. All four routes require a producer session or the Shopify plugin's bearer link token.
| Method & path | Purpose |
|---|---|
POST /api/offers/create/ | Create a new offer (draft status) for a product you have a verified sells/manufactured_by relationship to |
PATCH /api/offers/edit/ | Edit an existing offer's fields (headline, price, dates, etc.) |
GET /api/offers/list/ | List your organization's offers plus the products you're eligible to create offers for |
PATCH /api/offers/update/ | Change an offer's status — active, paused, or expired |
Org Agent Integration
Configure how your organization responds to RFQ requests — either a self-hosted A2A agent card (see RFQ Integration above) or ProductIndex-managed pricing rules.
| Method & path | Purpose |
|---|---|
POST /api/org/register/ | Create a new producer organization (signed-in session only — no bearer-token path) |
POST /api/org/register-agent-card/ | Register your self-hosted agent card URL for RFQ delivery. Owner/admin session role required (bearer-token callers are exempt). |
GET / PATCH /api/org/agent-integration/ | Read or update your Managed vs. self-hosted mode and pricing rules |
POST /api/org/ucp-check/ | Re-check your domain for Universal Commerce Protocol (UCP) checkout support (signed-in session only) |
Static files formatted for LLM context windows and RAG pipelines, following the llms.txt convention. No authentication required.
| File | Contents |
|---|---|
| llms.txt | MCP server reference + index of all products with one-line verdicts. Start here. |
| llms-full.txt | Full text of every product profile — suitable for offline RAG ingestion. Large file. |
Discovery Endpoints
Machine-readable discovery documents for automated use by agents and OAuth clients — mostly
under the standard .well-known path, plus the OpenAPI spec at the site root.
All public — no authentication required.
| Path | Purpose |
|---|---|
| /.well-known/mcp.json | MCP descriptor — tool schemas and per-client config blocks |
| /.well-known/agent.json | A2A agent card — skills, capabilities, and MCP extension pointer |
| /.well-known/oauth-authorization-server.json | OAuth 2.1 authorization server metadata (RFC 8414) — includes all endpoint URLs, supported grant types, CIMD support flag |
| /.well-known/oauth-protected-resource.json | OAuth protected resource metadata (RFC 9728) |
| /openapi.json | OpenAPI 3.1 spec for the public REST surface — search, product/category reads, offers, and producer org agent-integration endpoints |
Client Authorization — CIMD
ProductIndex supports Client ID Metadata Documents (CIMD) — a stateless alternative to OAuth Dynamic Client Registration (DCR) being standardized in MCP SEP-991 and a companion IETF draft. Instead of pre-registering a client and receiving an opaque client ID, your app hosts a static JSON file at a stable HTTPS URL. That URL is the client ID — no registration step, no database entry on our side.
How it works
Host a JSON file at any stable HTTPS URL on your domain. That URL becomes your
client_id. When you send an OAuth authorization request, ProductIndex fetches
the document, validates that the client_id field inside it exactly matches the
URL it was fetched from, then uses your redirect_uris list to verify the
redirect_uri in your request.
{
"client_id": "https://yourapp.example.com/oauth/client.json",
"client_name": "Your App Name",
"client_uri": "https://yourapp.example.com",
"redirect_uris": [
"https://yourapp.example.com/oauth/callback"
],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
Then start the OAuth flow using that URL as client_id:
GET https://productindex.ai/oauth/authorize/
?client_id=https://yourapp.example.com/oauth/client.json
&redirect_uri=https://yourapp.example.com/oauth/callback
&response_type=code
&code_challenge=<S256 PKCE challenge>
&code_challenge_method=S256
&scope=mcp
&state=<random state> Requirements
| Requirement | Detail |
|---|---|
| URL scheme | https:// only — no http://, no localhost URLs |
client_id self-reference | The client_id field inside the JSON must exactly match the URL it was fetched from |
| Required fields | client_id, client_name, redirect_uris (non-empty array) |
| Auth method | token_endpoint_auth_method: "none" — public clients with PKCE only |
| Response size | Must be under 64 KB |
| Fetch timeout | 5 seconds — serve the file from a CDN or static host |
ProductIndex caches valid CIMD documents for 5 minutes and invalid results for 1 minute. Private IP ranges, loopback addresses, and link-local addresses are blocked before fetch (SSRF protection).
Traditional DCR
If your client cannot host a static HTTPS file (e.g., a CLI tool or local app), register
via Dynamic Client Registration at POST /api/oauth/register/. DCR issues an
opaque UUID as the client_id. CIMD is preferred for web apps and server-side
integrations.
OAuth 2.1 + PKCE Flow
Complete flow for obtaining an access token. All clients are public
(token_endpoint_auth_method: none) — PKCE is the only
security mechanism, no client secret is issued or required.
Step 1 — Generate PKCE pair
// Generate a cryptographically random code_verifier (43–128 URL-safe chars)
const verifier = base64url(crypto.randomBytes(32));
// Derive code_challenge
const challenge = base64url(crypto.createHash('sha256').update(verifier).digest()); Step 2 — Redirect user to authorization endpoint
GET https://productindex.ai/oauth/authorize/
?response_type=code
&client_id=<your-client-id>
&redirect_uri=<your-registered-redirect-uri>
&code_challenge=<challenge>
&code_challenge_method=S256
&scope=mcp
&state=<random-csrf-token>
User signs in (or is auto-approved if they previously granted this client).
ProductIndex redirects to your redirect_uri with ?code=<auth_code>&state=<state>.
Step 3 — Exchange code for tokens
POST https://productindex.ai/api/oauth/token/
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<auth_code_from_redirect>
&redirect_uri=<same-redirect-uri>
&client_id=<same-client-id>
&code_verifier=<verifier>
// 200 OK
{
"access_token": "<token>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<refresh_token>"
} Step 4 — Call the MCP server
POST https://mcp.productindex.ai/mcp/
Authorization: Bearer <access_token>
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}} Step 5 — Refresh an expired token
POST https://productindex.ai/api/oauth/token/
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=<refresh_token>
&client_id=<same-client-id>
// 200 OK — new access_token and refresh_token issued; old refresh_token revoked | Response header | Value |
|---|---|
X-RateLimit-Remaining | Requests remaining today |
X-RateLimit-Reset | Unix timestamp when the daily quota resets |
Rate limits
OAuth 2.1 with PKCE is required for all requests. AI clients like Grok and Claude Desktop handle the sign-in flow automatically — connect once and they manage token refresh.
| Tier | Limit | How to activate |
|---|---|---|
| Authenticated (free) | 500 requests / day | OAuth 2.1 — clients like Grok and Claude Desktop handle sign-in automatically |
Remaining quota and reset time are returned on every response as
X-RateLimit-Remaining and X-RateLimit-Reset headers.
Questions
For integration questions or to report an issue with the API: hello@productindex.ai
ProductIndex