Ayan Ali.
9 min readUpdated

How to Add an AI Chatbot to Shopify Grounded in Your Product Catalogue

A Shopify chatbot that does not invent products needs four parts: a synced catalogue index, retrieval, a grounded prompt, and a Liquid section that talks to your backend. Here is each part.

ShopifyRAGAILiquid
A frosted speech-bubble form above a grid of product boxes, connected by thin lime lines, with the Shopify logo set into the floor and the OpenAI logo on the bubble.

Short answer: build four parts. A sync job puts your products into a vector database, a retrieval step finds the products relevant to each question, a grounded prompt tells the model to answer only from those products, and a custom Liquid section in your theme sends questions to your backend and shows the streamed reply. Product links come from the retrieved results, never from the model's text. The sync code, the prompt and the section are below. My RAG experience is on drama catalogues, and the same pipeline applies to products.

What are the parts of a Shopify AI chatbot?

There are five pieces, and each one has a separate job. Splitting them this way lets you test and replace each one.

| Piece | Job | Where it runs | |---|---|---| | Catalogue sync | Keep product data in the index | Your backend | | Vector database | Find products by meaning | Qdrant, pgvector or similar | | Retrieval | Pick the relevant products per question | Your backend | | Grounded prompt | Make the model answer from them only | Your backend, calling an LLM | | Chat section | Ask and display | Your Shopify theme |

The backend is a small web service. In my drama recommender it is FastAPI on Railway with Qdrant Cloud, and a Shopify version would use the same shape. The theme never holds your model API key, because the browser only talks to your backend.

How do I get my products into the index?

Pull everything once with a bulk operation, then keep it current with webhooks. Shopify's GraphQL Admin API supports bulk operations through bulkOperationRunQuery, which runs a query asynchronously and gives you a file of results. You can subscribe to the bulk_operations/finish webhook, or poll the operation, to know when it is ready.

After the first load, subscribe to product webhooks so the index follows the store. Handle three topics: create, update and delete. On a delete, remove the product's chunks from the index, and on a create or update, rebuild only that product's chunks and re-embed them. Re-embedding one product is cheap, whereas re-embedding the whole catalogue on every change is not.

@app.post("/webhooks/products")
async def products_webhook(request: Request, x_shopify_hmac_sha256: str = Header(...)):
    raw = await request.body()
    if not valid_hmac(raw, x_shopify_hmac_sha256):   # HMAC-SHA256 of the raw body, base64
        raise HTTPException(status_code=401)
    topic = request.headers["X-Shopify-Topic"]
    product = json.loads(raw)
    if topic == "products/delete":
        index.delete_product(product["id"])
    else:
        index.replace_product(product["id"], product_to_chunks(product))
    return {"ok": True}

Always verify the webhook signature before touching the index, since an unverified endpoint lets anyone rewrite your catalogue. Registering webhooks and reading products needs an app installed on the store with the read_products access scope.

What should go into each product chunk?

Index the text a shopper would describe the product with, and leave volatile fields out. Good chunk text includes the title, product type, vendor, tags, a plain-text description (strip the HTML), and option names and values.

def product_to_chunks(p):
    tags = [t.strip() for t in p.get("tags", "").split(",") if t.strip()]
    text = (
        f"{p['title']}\n"
        f"Type: {p.get('product_type','')} | Vendor: {p.get('vendor','')}\n"
        f"Tags: {', '.join(tags)}\n"
        f"{strip_html(p.get('body_html',''))}"
    )
    return [{"id": f"{p['id']}__main", "product_id": p["id"], "handle": p["handle"],
             "title": p["title"], "text": text}]

Add chunks for what shoppers ask besides products: shipping, returns, sizing and care pages. Without them the chatbot cannot answer "how long is delivery?" and will either refuse or guess. Keep price and stock out of the embedded text. A price in the vector goes stale the moment someone runs a sale, and the model will repeat it confidently. See how to chunk documents for RAG for the chunking reasoning.

How do I ground the prompt?

Pass only the retrieved products, and tell the model to answer only from them. The prompt should also say what to do when nothing fits, since retrieval always returns the nearest items even when none is a good match.

You are a shopping assistant for {store name}.
Recommend ONLY products listed under "Available products". Never invent
products, prices, sizes or policies.
For each recommendation, give the product name and one or two sentences on why
it fits, using details from its listing.
If none of the available products fit, say so and ask one clarifying question.
If the question is about shipping or returns, answer only from the policy text provided.

Retrieve with the question's embedding, rerank if you can, and place the top handful in the prompt. The full pipeline, including two-stage retrieval and the check for invented titles, is in building a RAG chatbot that doesn't hallucinate.

Return the handles of the retrieved products alongside the text, and let the theme build the cards. The model should never write a URL, because a model can produce a plausible link that goes nowhere. Your backend knows which products it retrieved, so it sends their handles in a response header or a JSON field.

The theme then fetches live data for each handle from the store's own /products/{handle}.js endpoint, which returns the title, price in cents, image and availability. That keeps price and stock correct even if the index is a few minutes behind, and lets you drop sold-out items from the cards.

What does the Liquid section look like?

A custom section with a form, an answer area and a deferred script. It needs no app, and the endpoint is a section setting so you can change it without editing code.

<section class="ai-chat" data-endpoint="{{ section.settings.endpoint }}">
  <h2>{{ section.settings.heading }}</h2>
  <form class="ai-chat__form">
    <label for="ai-q-{{ section.id }}" class="visually-hidden">Ask about our products</label>
    <input id="ai-q-{{ section.id }}" name="q" type="text" maxlength="300" required autocomplete="off">
    <button type="submit">Ask</button>
  </form>
  <div class="ai-chat__answer" aria-live="polite"></div>
  <ul class="ai-chat__cards"></ul>
</section>

<script src="{{ 'ai-chat.js' | asset_url }}" defer></script>

{% schema %}
{
  "name": "AI chat",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "Ask about our products" },
    { "type": "url",  "id": "endpoint", "label": "Chat endpoint" }
  ],
  "presets": [{ "name": "AI chat" }]
}
{% endschema %}

The script streams the reply and renders it with textContent, never innerHTML, so model output cannot inject markup into your page.

document.querySelectorAll('.ai-chat').forEach((root) => {
  const form = root.querySelector('form');
  const answer = root.querySelector('.ai-chat__answer');
  const cards = root.querySelector('.ai-chat__cards');

  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    answer.textContent = '';
    cards.replaceChildren();
    const res = await fetch(root.dataset.endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: form.q.value }),
    });
    const handles = JSON.parse(res.headers.get('X-Products') || '[]');
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      answer.textContent += decoder.decode(value, { stream: true });
    }
    for (const handle of handles) {
      const p = await (await fetch(`/products/${handle}.js`)).json();
      if (!p.available) continue;
      const li = document.createElement('li');
      const a = Object.assign(document.createElement('a'), { href: `/products/${handle}`, textContent: p.title });
      const price = new Intl.NumberFormat(undefined, { style: 'currency', currency: window.Shopify?.currency?.active || 'USD' }).format(p.price / 100);
      li.append(a, ` ${price}`);
      cards.append(li);
    }
  });
});

Make the endpoint allow your store's domain in CORS, and restrict it to that origin. Load the script with defer, so it stays off the critical path, as covered in how to speed up a Shopify store without apps.

What protects the endpoint from abuse and cost?

A public chat endpoint is a public bill, so limit it. Add per-IP rate limiting, a cap on question length (the form above sets 300 characters), a cap on output tokens, and an origin allowlist. Without these, anyone who finds the URL can run up your model costs.

Keep the model key on the backend only, and log queries so you can see what shoppers ask. Do not send customer personal data into prompts. Order status and account questions need authenticated access to customer data, which is a separate project, so scope the first version to products, policies and sizing.

Can I use Shopify's own catalogue tools instead of building the index?

For agents that search a single store, Shopify offers the Storefront Catalog MCP server, which lets an AI agent search a merchant's catalogue without you running a sync. It implements the UCP Catalog capability and exposes search_catalog, lookup and get_product tools.

It comes with rules. Shopify's usage guidelines say not to cache search results or re-use images, and that catalog queries are rate limited, with no rate-limit increases for keyless access. Those rules suit a live agent and not an index you build and own. If you want ranking you control, your own index is the route. Check Shopify's current documentation, since endpoints and limits can change.

What does this cost, and who is it for?

A query in my RAG project cost about one cent, covering one embedding call and one GPT-4o call. Your cost depends on how much product text goes in each prompt and how many questions you receive, so multiply the two and add hosting.

This is worth building when the catalogue is large or complex, when you need control over answers, or when you want to own the data and avoid per-conversation fees. For a small store with simple questions, an app is faster, as covered in Shopify AI chatbot app vs building your own. I have not published a Shopify chatbot case study yet, and the retrieval pipeline here is the one from my K-Drama RAG and Manhwa Recommender projects.

If you want this built for your store, see my Shopify development services or get in touch with your store URL and the questions shoppers ask most.

Sources

Frequently asked questions

How do I add an AI chatbot to Shopify without an app?

Host a small backend that retrieves products from your own index and calls a language model, then add a custom Liquid section to your theme that sends the shopper's question to that backend and shows the streamed answer.

How does the chatbot know my products?

You sync them into a vector database. Pull the full catalogue once with a GraphQL bulk operation, then keep it current with products/create, products/update and products/delete webhooks.

How do I stop the chatbot from inventing products?

Give the model only the products that retrieval returned, tell it to recommend only from those, and build the product links from the retrieved set rather than from the model's text.

Should the chatbot quote prices from the index?

Not as the source of truth. Prices and stock change, so fetch them live from the store when you display an answer, and use the index to decide which products are relevant.

What does it cost to run?

It depends on your catalogue, prompt size and traffic. In my similar RAG project, a query cost about one cent: one embedding call and one GPT-4o call. Multiply by your expected queries and add hosting.

Want an AI that actually knows your catalogue?

I build Shopify chatbots and product finders that answer from your real products instead of guessing — custom-coded into your theme, with no monthly app fee. Tell me what your store needs and I'll tell you what it takes.

Read next

How to Build an AI Product Finder Section for Shopify
A wide field of dark cubes receding into shadow with a narrow lime light picking out exactly three, and the Shopify logo on a plate in the foreground.
Shopify

How to Build an AI Product Finder Section for Shopify

A product finder takes 'a waterproof jacket for light hiking under $150' and returns matching products. It needs vector search and filters, and it does not need a chat model.

Read
How to Chunk Documents for RAG (With a 6,234-Chunk Example)
A tall dark slab sliced into many thin horizontal layers fanned apart, four of them lime-edged, with the Qdrant logo on a plate beneath.
RAG

How to Chunk Documents for RAG (With a 6,234-Chunk Example)

One vector per document blends everything into a blur. Splitting each record into typed chunks, one per kind of question, kept my retrieval sharp. Here are the real counts and rules.

Read
Building a RAG Chatbot That Doesn't Hallucinate
A thin lime line passing through four dark gates of narrowing aperture and exiting as one clean line, with the Qdrant logo at the final gate and the OpenAI logo at the first.
RAG

Building a RAG Chatbot That Doesn't Hallucinate

A RAG chatbot hallucinates less when the model only explains what retrieval found. Here is the pipeline I built over 838 dramas, the prompt rules, and the check I would add.

Read

Got a store or a build in mind?

Tell me what you're trying to ship — a Shopify section, a full storefront, a chatbot, or a web app. I'll tell you what it takes, honestly, before you commit.

ayanrjpoot@gmail.com