--- title: AI Chatbot for Ecommerce: Answer From Your Policies and Look Up Orders description: Build an AI chatbot for ecommerce with the Zavu API: it answers from your shipping and returns policies, looks up order status, and hands off to a person. date: 2026-10-19 author: Zavu Team locale: en source: https://www.zavu.dev/en/blog/chatbot-ia-tienda-online tags: AI Agents, Ecommerce, Shopify, Telegram, WhatsApp, Tutorial, API v2 --- # AI Chatbot for Ecommerce: Answer From Your Policies and Look Up Orders ## TL;DR - **Answers come from your policies.** The agent searches a knowledge base with your shipping, returns and FAQ documents before it replies. - **Order status comes from your store.** A `get_order_status` tool calls your HTTPS endpoint, which verifies the signature and returns the real status. - **You can test without messaging customers.** `POST /v2/agents/{agentId}/test` returns the reply and the tool the model chose; on the `zavu` provider a test is billed like a live reply. - **Start on Telegram, switch channels later.** Telegram connects with one API call; WhatsApp and Instagram connect from **Channels** in the dashboard, with the same agent. - **Replying from the inbox does not pause the agent.** Only a flow's `transfer` step does, until someone on your team replies. Most questions an online store gets are the same five: where is my order, how much is shipping, can I return this, do you have it in another size, how long does delivery take. An AI chatbot for ecommerce is worth building when it answers those from your real policies and your real order data, not from whatever the model guesses. This tutorial builds that over the Zavu API, end to end, with plain HTTP. At the end you have: - A chat channel connected (Telegram to start, WhatsApp or Instagram when you go live). - An AI agent that answers from a knowledge base holding your shipping policy, returns policy and FAQ. - A `get_order_status` tool: the agent calls your endpoint, you verify the signature, and you answer with the order's status from your store. - A way to test the whole thing without messaging a single customer. - A keyword that hands the conversation to a person, and the agent staying quiet until that person answers. If you prefer to click through the dashboard instead, [the support agent walkthrough](/en/blog/agente-de-soporte-con-ia) does the Telegram version with no code. This post is the API version, with your order data in the loop. ## What you need - **A Zavu account and an API key.** Every call below sends `Authorization: Bearer $ZAVUDEV_API_KEY`. - **A channel your customers write to.** Each one has a different setup cost: - **Telegram**: a bot token from @BotFather. One API call, no review, no Meta account. The fastest way to have something real to test. - **WhatsApp**: a WhatsApp Business Account through Meta's signup, connected from **Channels** in the dashboard. The most friction of any channel, and for many stores in Latin America the one customers actually use. - **Instagram**: also connected through Meta, from **Channels** in the dashboard. - **An HTTPS endpoint on your side** that can look up an order. It has to be public: Zavu refuses a tool whose URL is not a public HTTPS address. - **Your policies as text.** Shipping, returns, payment methods, the questions you answer every week. The SDKs (`@zavudev/sdk`, `zavudev`) call API v1. API v2 is called over HTTP, so the examples use `curl` and `fetch`. Two pieces of this build have no v2 endpoint yet, knowledge bases and tools, and live on v1 under `/v1/agents/{agentId}/...` with the same key and the same agent id. The [migration guide](https://docs.zavu.dev/api-v2/migrate-from-v1) lists what stays on v1. Each step below says which version it uses. ## 1. Connect a channel (v2) Start with Telegram. Create a bot with @BotFather, copy the token, and connect it: ```bash curl -X POST https://api.zavu.dev/v2/channels \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"type": "telegram", "botToken": "123456789:AA..."}' ``` The `201` lists the entries of the address you connected. A Telegram bot's address is `@botname`: ```json { "items": [ { "address": "@acme_store_bot", "type": "telegram", "active": true, "v1SenderId": "kd7..." } ] } ``` If Telegram rejects the token, nothing is created and you get `400 invalid_bot_token`, with Telegram's own words in `details.telegramSaid`. Copy the token again from @BotFather. Check `active`. A connection that would cost money on your plan starts switched off; turn it on with `POST /v2/channels/{address}/{type}/activate`, and the response tells you what it charged in `chargedCents` and `monthlyCents`. Details in [Addresses and channels](https://docs.zavu.dev/api-v2/addresses). WhatsApp and Instagram are not connected with this endpoint. Connect them from **Channels** in the dashboard; once connected they appear in `GET /v2/channels` and the rest of this tutorial works the same, with the phone number or `@username` as the address. ## 2. Create the agent (v2) ```bash curl -X POST https://api.zavu.dev/v2/agents \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme store assistant", "provider": "zavu", "model": "openai/gpt-4o-mini", "systemPrompt": "You answer customers of Acme, an online store. Answer only from the knowledge base and the get_order_status tool. If the answer is not there, say you do not know and offer a person. To check an order, ask for the order number and the email used at checkout. Never invent a delivery date. Keep replies short: this is a chat." }' ``` `name`, `provider`, `model` and `systemPrompt` are required, and v2 names every one that is missing in a single `400`. `provider` is one of `openai`, `anthropic`, `google`, `mistral` or `zavu`. `zavu` is the managed provider: no API key of yours, and each reply is charged to your Zavu balance per token. With any other provider you send `apiKey` and the tokens cost your provider account instead. On `zavu`, `model` is written as `provider/model`, for example `openai/gpt-4o-mini`; the dashboard's model picker lists the catalog in force. Two things in the response matter. The agent is created **disabled**, and its `channels` is empty: it answers nobody yet. Keep the `id`. ```javascript const res = await fetch("https://api.zavu.dev/v2/agents", { method: "POST", headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Acme store assistant", provider: "zavu", model: "openai/gpt-4o-mini", systemPrompt: SYSTEM_PROMPT, }), }); const { agent } = await res.json(); console.log(agent.id, agent.enabled); // "kh7...", false ``` The system prompt is where most chatbots go wrong. "Answer only from the knowledge base" and "never invent a delivery date" are the two lines that keep an ecommerce bot from promising things your warehouse cannot do. ## 3. Load your policies into a knowledge base (v1) A knowledge base is a set of documents the agent searches before it answers. Create one on the agent, then add a document per topic: ```bash curl -X POST https://api.zavu.dev/v1/agents/$AGENT_ID/knowledge-bases \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Store policies", "description": "Shipping, returns, payments and FAQ"}' ``` ```bash curl -X POST https://api.zavu.dev/v1/agents/$AGENT_ID/knowledge-bases/$KB_ID/documents \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Shipping policy", "content": "# Shipping\n\nOrders placed before 2pm ET ship the same business day.\nStandard shipping in the continental US takes 3 to 5 business days and is free over $50.\nExpress takes 1 to 2 business days and costs $15.\nWe do not ship to PO boxes." }' ``` Repeat for returns, payment methods and an FAQ. One topic per document retrieves better than one long document with everything. A document is processed after it is created; its `isProcessed` turns `true` once it is split and searchable. If you sell on Shopify, you already have this text: it is in your store's policy pages. Paste it as it is and fix what is stale. The agent will repeat it to customers exactly as written. ## 4. Give it a `get_order_status` tool (v1) The knowledge base answers "how long does shipping take". It cannot answer "where is order 1042". For that the agent calls a tool: an HTTPS endpoint of yours that Zavu calls with arguments the model fills in. ```bash curl -X POST https://api.zavu.dev/v1/agents/$AGENT_ID/tools \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "get_order_status", "description": "Look up the status of an order. Call it when the customer asks where their order is or when it will arrive. Requires the order number and the email used at checkout.", "webhookUrl": "https://shop.example.com/zavu/tools/order-status", "parameters": { "type": "object", "properties": { "order_number": { "type": "string", "description": "Order number, e.g. 1042" }, "email": { "type": "string", "description": "Email used at checkout" } }, "required": ["order_number", "email"] } }' ``` The `201` carries `tool.webhookSecret`. **It is returned only on create**, never on a later read. Store it. If you lose it, rotate it with `POST /v1/agents/{agentId}/tools/{toolId}/webhook/secret`. ### What your endpoint receives Zavu sends a `POST` with this JSON body: ```json { "tool": "get_order_status", "arguments": { "order_number": "1042", "email": "dana@example.com" }, "context": { "conversationId": "js72...", "channel": "telegram", "contactPhone": "...", "messageId": "jd7..." }, "timestamp": 1760889600000 } ``` and these headers: `X-Zavu-Tool` (the tool name), `X-Zavu-Timestamp`, and `X-Zavu-Signature`, the hex-encoded HMAC-SHA256 of the raw body with the tool's secret. No prefix, no timestamp mixed in. `contactPhone` is the identifier the customer wrote from: a phone number on WhatsApp, the chat id on Telegram. ### The endpoint ```javascript import express from "express"; import crypto from "node:crypto"; const app = express(); const SECRET = process.env.ZAVU_TOOL_SECRET; // Raw body: the signature is over the exact bytes Zavu sent. app.post("/zavu/tools/order-status", express.raw({ type: "application/json" }), async (req, res) => { const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex"); const received = req.get("X-Zavu-Signature") ?? ""; const ok = received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected)); if (!ok) return res.status(401).json({ error: "bad signature" }); const { arguments: args } = JSON.parse(req.body.toString("utf8")); // Your store: Shopify Admin API, your platform's API, or your own database. const order = await findOrder(args.order_number); // Do not confirm an order to someone who cannot prove it is theirs. if (!order || order.email.toLowerCase() !== String(args.email).toLowerCase()) { return res.json({ found: false }); } res.json({ found: true, status: order.status, // "paid" | "shipped" | "delivered" | ... carrier: order.carrier ?? null, trackingUrl: order.trackingUrl ?? null, estimatedDelivery: order.estimatedDelivery ?? null, }); }); app.listen(3000); ``` The same check in Python: ```python import hmac, hashlib def valid(raw_body: bytes, signature: str, secret: str) -> bool: expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature or "") ``` What you return is what the agent reads, so return facts, not prose: a status, a carrier, a tracking link, a date when you have one. A non-2xx response reaches the agent as a failed call and it answers around the error, so a `found: false` with `200` gives a better reply than a `404`. Keep the endpoint fast: the customer is waiting for the reply while it runs. Asking for the email is a choice, not a requirement of Zavu. On Telegram you have no phone number to match against the order, and an order number alone is easy to guess. ## 5. Connect the agent and turn it on (v2) ```bash curl -X POST https://api.zavu.dev/v2/agents/$AGENT_ID/channels \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"address": "@acme_store_bot", "type": "telegram"}' ``` `address` and `type` are one entry of `GET /v2/channels`. An address answers with one agent at a time: if another agent already holds it, the `400` names that agent. An address the project does not have is `404 not_found`. One-way SMS is refused with `409 channel_cannot_receive`, because nobody can reply to it and the agent would never answer. Then enable it: ```bash curl -X PATCH https://api.zavu.dev/v2/agents/$AGENT_ID \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"enabled": true}' ``` ## 6. Test it without messaging anyone (v2) `POST /v2/agents/{agentId}/test` runs the agent's prompt, model and knowledge base against a message and returns the reply instead of delivering it. No message goes out, no execution is logged. ```bash curl -X POST https://api.zavu.dev/v2/agents/$AGENT_ID/test \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "the email is dana@example.com", "history": [ { "role": "user", "content": "where is my order 1042?" }, { "role": "assistant", "content": "I can check that. What email did you use at checkout?" } ] }' ``` The response: ```json { "success": true, "text": "...", "knowledgeChunksUsed": 0, "plannedToolCalls": [ { "name": "get_order_status", "arguments": "{\"order_number\":\"1042\",\"email\":\"dana@example.com\"}" } ], "executedToolCalls": [], "warnings": [ "The model was offered 1 tool (get_order_status) but none ran, since running them would reach your real handlers. ..." ], "inputTokens": 812, "outputTokens": 41, "latencyMs": 1430, "cost": 0.000146 } ``` Read three fields: - **`plannedToolCalls`**: the tool the model chose and the arguments it filled in. By default nothing runs, so the reply stops at that choice. - **`knowledgeChunksUsed`**: ask "do you ship to PO boxes?" and this should be above zero. Zero means the answer did not come from your documents. - **`warnings`**: what a dry run cannot prove. A disabled agent, an agent connected to no channel, tools offered but not run, flows that were not evaluated, contact metadata that does not exist in a test. To run the whole loop, the model calling your real endpoint and answering with its result, add `"executeTools": true`. Your endpoint is called for real, and `executedToolCalls` lists what ran with `ok` and any `error`. Use a test order. A test run on a `zavu` agent is billed like a live reply, at the same per-token price, and the response's `cost` says how much. With an empty balance it is refused with `402 insufficient_balance`; on a bring-your-own-key agent it costs your provider. Then write to the bot from your own Telegram account. That is the first real conversation. ## 7. Watch conversations and take over In the dashboard, every chat the agent handles appears in **Conversations**, next to messages from every other channel. You can open a thread and reply yourself. Be precise about what that reply does. **Replying from Conversations does not pause the agent.** The customer's next message is answered by the agent again. The agent stays quiet only while a handoff is open, and a handoff opens when a flow reaches a `transfer` step. So give customers a way to ask for a person. Flows are v1: ```bash curl -X POST https://api.zavu.dev/v1/agents/$AGENT_ID/flows \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Talk to a person", "trigger": { "type": "keyword", "keywords": ["human", "agent", "talk to a person"] }, "steps": [ { "id": "handoff", "type": "transfer", "config": { "message": "A person from our team will reply here shortly." } } ], "enabled": true, "priority": 10 }' ``` A `keyword` trigger is a case-insensitive substring match. When it fires, the customer gets the message, the session is marked `transferred`, and the agent stops answering that contact. Their next messages still arrive in Conversations. The moment someone on your team replies, from Conversations or from the API, the handoff ends and the agent takes the thread back. Nobody is notified for you: there is no queue and no assignment. If your team does not watch Conversations, put a `tool` step before the `transfer` that pings your own Slack or helpdesk. For the difference between this and a scripted bot, see [AI agent vs chatbot](/en/blog/ai-agent-vs-chatbot). ## What can go wrong | What you see | Why | What to do | |---|---|---| | `400 invalid_bot_token` | Telegram rejected the token | Copy it again from @BotFather | | `400` naming `name, provider, model, systemPrompt` | Missing fields on create | Send all four | | `400` "already answered by the agent ..." | The address has another agent | Disconnect it there, or use another address | | `409 channel_cannot_receive` | You connected one-way SMS | Use a channel customers can reply on | | `402 insufficient_balance` on test | Zavu model, empty balance | Add funds, or use your own provider key | | Agent answers but invents order dates | It is not calling the tool | Check `plannedToolCalls`; sharpen the tool `description` | | `knowledgeChunksUsed: 0` | Documents not processed, or not relevant | Check `isProcessed`; split long documents by topic | v2 errors are flat, `{code, message, details}`. The [errors reference](https://docs.zavu.dev/api-v2/errors) lists them. ## Going to production with your AI chatbot for ecommerce - **Move to the channel your customers use.** Connect WhatsApp or Instagram from **Channels**, then `POST /v2/agents/{agentId}/channels` with that address. Same agent, same knowledge base, same tool. The same setup works as an AI customer service chatbot for a Shopify store on whichever channel its customers write from. - **WhatsApp costs.** Meta charges for service messages past 1,000 free ones per number each month, and stops delivering them if the WhatsApp Business Account has no payment method. [Here is how that works](/en/blog/whatsapp-service-messages-payment-method). - **SMS in the US.** If you add two-way SMS on a US number, sending to +1 numbers needs an approved 10DLC campaign; until then sends are refused with `403 ten_dlc_required`. Telegram and email need none. See [what A2P 10DLC is](/en/blog/what-is-a2p-10dlc). - **Agent cost.** On `zavu`, every reply is charged to your balance per token. Watch it in the agent's executions and stats (v1: `GET /v1/agents/{agentId}/executions` and `/stats`, or the agent's page in the dashboard). - **Limits.** Daily and monthly sending limits depend on your plan and verification. The [sending limits](https://docs.zavu.dev/concepts/sending-limits) page has the numbers. - **Keep policies current.** When your return window changes, update the document (`PATCH` on the document reprocesses it). The bot repeats what the knowledge base says, including what is out of date. To see where this fits in the new API, read [the Zavu 2.0 announcement](/en/blog/zavu-2-0) and the [v2 overview](https://docs.zavu.dev/api-v2/overview).