David Lewin David Lewin
E-Commerce, Research & Ads

Automating "Where is my order?" with AI chatbots: Tidio vs a custom Dialogflow bot

Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.

Short answer

"Where is my order?" is usually the biggest single type of e-commerce support request, at roughly 18-30% of tickets in the most cited data, so a bot that answers it from live order data takes a large share of the load off your team. The bot needs three things: an order number, the email used at checkout, and an endpoint that returns the order's status only when both match. Give a wrong pair and a missing order the same answer, rate limit failures per order and per chat, and return status and tracking only, never addresses. On Shopify, Tidio's Lyro has a ready-made order status action. On WooCommerce, point a Lyro Action at a small lookup service like the one in this guide (tested against a real store, 18 of 18 checks passed). A custom Dialogflow CX bot can call the same service, but you then build and maintain the conversation, the hand-over to people and the chat channel yourself. Tidio is our pick for most stores; Gorgias fits stores that want a full helpdesk around the bot.

"Where is my order?" (support teams call it WISMO) is the question every store answers most and learns least from. The answer already sits in your order data; a person just has to look it up, paste a tracking link and move on. That makes it the best first job for a support bot. This guide builds the part that matters: a lookup that answers from live WooCommerce data without handing strangers your customers' orders. Then it connects that lookup to Tidio's AI agent and to a custom Dialogflow CX bot, so you can compare the two honestly.

How this guide was made

We tested the lookup service against a real WordPress 7.1.3 and WooCommerce 11.2.1 store through its REST API, with a read-only key. Orders were real WooCommerce orders, with tracking stored the way the WooCommerce Shipment Tracking extension stores it. We sent requests shaped like Tidio's API call and Dialogflow CX's webhook request. We didn't connect a live Tidio project or Dialogflow agent. Setup steps, plan limits and prices come from Tidio's help center and pricing page and Google's Dialogflow CX documentation, checked in October 2026.

1. The short answer

QuestionTidio (Lyro AI agent)Custom Dialogflow CX bot
Shopify order statusReady-made action: asks for email and order ID, can also ask for zip code or phoneYour webhook calls Shopify's Admin API
WooCommerce order statusA Lyro Action that calls your lookup endpoint (this guide)A webhook that calls the same endpoint (this guide)
Understanding messy questionsBuilt in; Lyro also answers from your help contentYou design intents, pages and parameters
Hand-over to a person, inbox, chat widgetIncludedYou build or integrate them
Cost shapeMonthly plan plus Lyro conversationsGoogle Cloud usage plus your engineering time
Best forMost stores that want WISMO handled this monthTeams with developers who need full control of the conversation

2. How the bot answers

Order status bot: from question to answer
Order status bot: from question to answerCustomer → Bot (Tidio Lyro or Dialogflow CX) → Lookup webhook → WooCommerce REST API → Match / No match / Store downCustomer: "Where is my order?"Customer"Where is my order?"Bot (Tidio Lyro or Dialogflow CX): collects the order number and checkout emailBot (Tidio Lyro or Dialogflow CX)collects the order number and checkout emailLookup webhook: checks the bot's token and the rate limitsLookup webhookchecks the bot's token and the rate limitsWooCommerce REST API: read-only key: GET /orders/{number}WooCommerce REST APIread-only key: GET /orders/{number}Match: status and tracking linkMatchstatus andtracking linkNo match: the same answer for every missNo matchthe same answerfor every missStore down: hand over to a personStore downhand over to aperson
The bot never talks to WooCommerce directly. The webhook decides what may be said, and only order status and tracking ever leave it.

3. How much load this removes

Published WISMO numbers vary because they count different things. Gorgias data on its merchants has put "where is my order" at about 18% of requests on average, and a Gorgias survey of 12,000 stores at up to 30% of tickets. Vendor estimates for direct-to-consumer brands run from 25% to 50%, and higher around peak season. No independent study settles it, so count your own: tag a week of tickets and see what share is order status.

If WISMO is 30% of your tickets and the bot resolves most of them, you take most of that 30% off your team's queue. "Resolves" is the operative word. A bot that answers "I can't find that order" to half of the questions only moves the work around. The lookup has to be forgiving with input ("#1042", "order 1042", an email typed in capitals) and strict about who it answers.

4. The rule that keeps it safe: number and email, same answer for every miss

A chat widget is public. If your bot answers "order 1043 is shipped to Leeds" to anyone who types a number, someone will type 1044, 1045 and so on. WooCommerce order numbers are sequential by default, which makes this trivial. Tidio's own documentation warns about exactly this enumeration risk for its API call action. The lookup below follows four rules:

  • Two factors. The order number and the billing email must both match. The email comparison ignores case and spaces and runs in constant time.
  • Every miss looks the same. A wrong email and an order that doesn't exist get the same reply, so nobody can learn which order numbers are real.
  • Rate limits on failures. After 5 failed attempts in 15 minutes, the order is locked, and so is the chat that made them. A locked order can't be read even with the right email. This blocks guessing the email for a known order number. A real customer caught by the lock is told they're being passed to a person.
  • Minimal answers. Status, items, tracking number and link. No name, address or phone number. The bot gets nothing it could leak.

The WooCommerce key behind it has Read permission only. Our test confirmed a write request with it is refused, so a leaked key can't cancel or edit orders.

5. The lookup service

One small Python service (standard library only) serves both bots. Tidio sends a flat JSON body; Dialogflow CX sends its WebhookRequest format. Both authenticate with the same bearer token, which Tidio stores as a secret in its Bearer authentication option and Dialogflow sends as a custom header.

"""Order status for chatbots ("Where is my order?"). One lookup, two front doors:
  POST /tidio       Tidio Lyro Action or Flows "API call": JSON {"order": "...", "email": "...", "visitor": "..."}
  POST /dialogflow  Dialogflow CX webhook: session parameters order_number and email
Both send "Authorization: Bearer <BOT_TOKEN>". An order is shown only when its number AND billing email match. A wrong
pair and a missing order get the same answer, and failures are rate limited per order and per chat, so the bot can't be
used to probe other people's orders.

Env: WC_URL, WC_KEY, WC_SECRET (a WooCommerce REST key with Read access only), BOT_TOKEN (long random string)."""
import base64, hmac, json, os, re, time, urllib.error, urllib.request
from collections import defaultdict, deque
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

WC = os.environ.get('WC_URL', 'https://shop.example.com').rstrip('/') + '/wp-json/wc/v3'
WC_AUTH = 'Basic ' + base64.b64encode(f"{os.environ.get('WC_KEY', '')}:{os.environ.get('WC_SECRET', '')}".encode()).decode()
MAX_FAILS, WINDOW = 5, 900  # 5 wrong guesses per order (and per chat) in 15 minutes
fails = defaultdict(deque)  # ponytail: in memory, one process; move to Redis if you run several workers

STATUS = {'pending': 'waiting for payment', 'on-hold': 'on hold while we confirm your payment',
          'processing': 'paid and being prepared for shipping', 'completed': 'shipped', 'cancelled': 'cancelled',
          'refunded': 'refunded', 'failed': 'not paid (the payment failed)'}
TRACK = {'ups': 'https://www.ups.com/track?tracknum=', 'usps': 'https://tools.usps.com/go/TrackConfirmAction?tLabels=',
         'fedex': 'https://www.fedex.com/fedextrack/?trknbr=', 'dhl': 'https://www.dhl.com/global-en/home/tracking.html?tracking-id='}
ASK = 'Please send me your order number and the email address you used at checkout.'
NO_MATCH = "I couldn't find an order with that number and email. Please check both against your order confirmation email."
LIMITED = "I can't check that order right now. I'm passing you to a member of our team."
DOWN = "I can't reach our order system at the moment. I'm passing you to a member of our team."


def failed_recently(key):
    q = fails[key]
    while q and q[0] < time.time() - WINDOW:
        q.popleft()
    return len(q) >= MAX_FAILS


def answer(reason, message, **order):
    # Always the same keys, so a Tidio action that maps every output never fails on a missing one.
    out = {'found': reason == 'found', 'reason': reason, 'message': message, 'status': '', 'tracking_number': '', 'tracking_url': ''}
    return {**out, **order}


def lookup(order, email, chat=''):
    # "#1042" or "order 1042" from Tidio; Dialogflow's @sys.number sends 1042.0
    num = str(int(order)) if isinstance(order, (int, float)) else re.sub(r'\D', '', str(order or ''))
    email = str(email or '').strip().lower()
    if not num or '@' not in email:
        return answer('need_input', ASK)
    keys = ['order:' + num] + (['chat:' + chat] if chat else [])
    if any(failed_recently(k) for k in keys):
        return answer('rate_limited', LIMITED)
    try:
        req = urllib.request.Request(f'{WC}/orders/{num}', headers={'Authorization': WC_AUTH})
        with urllib.request.urlopen(req, timeout=3) as r:  # stay well inside the bot's webhook timeout
            o = json.load(r)
    except urllib.error.HTTPError as e:
        if e.code != 404:
            return answer('unavailable', DOWN)
        o = None
    except (OSError, ValueError):
        return answer('unavailable', DOWN)
    if not o or o.get('status') == 'checkout-draft' or not hmac.compare_digest(
            o['billing']['email'].strip().lower().encode(), email.encode()):
        for k in keys:
            fails[k].append(time.time())
        return answer('no_match', NO_MATCH)

    # Tracking from the WooCommerce Shipment Tracking extension's order meta, if you use it.
    meta = {m['key']: m['value'] for m in o.get('meta_data', [])}
    t = (meta.get('_wc_shipment_tracking_items') or [{}])[-1]
    number = t.get('tracking_number', '')
    url = t.get('custom_tracking_link') or (TRACK[t['tracking_provider']] + number
                                             if number and t.get('tracking_provider') in TRACK else '')
    items = ', '.join(f"{i['name']} x{i['quantity']}" for i in o['line_items'][:3]) + (' and more' if len(o['line_items']) > 3 else '')
    status = STATUS.get(o['status'], o['status'].replace('-', ' '))
    msg = f"Order #{o['number']} ({items}) is {status}."
    if number:
        msg += f' Tracking number: {number}' + (f', track it here: {url}' if url else '.')
    return answer('found', msg, status=o['status'], tracking_number=number, tracking_url=url)


class Handler(BaseHTTPRequestHandler):
    def reply(self, code, obj):
        b = json.dumps(obj).encode()
        self.send_response(code)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', str(len(b)))
        self.end_headers()
        self.wfile.write(b)

    def do_POST(self):
        if not hmac.compare_digest(self.headers.get('Authorization', '').encode(), f"Bearer {os.environ['BOT_TOKEN']}".encode()):
            return self.reply(401, {'error': 'unauthorized'})
        try:
            body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0))))
        except ValueError:
            return self.reply(400, {'error': 'bad json'})
        if self.path == '/tidio':
            return self.reply(200, lookup(body.get('order'), body.get('email'), str(body.get('visitor') or '')))
        if self.path == '/dialogflow':
            s = body.get('sessionInfo', {})
            p = s.get('parameters', {})
            a = lookup(p.get('order_number'), p.get('email'), s.get('session', ''))
            return self.reply(200, {  # 200 even when not found: the flow routes on order_found, not on webhook errors
                'fulfillmentResponse': {'messages': [{'text': {'text': [a['message']]}}]},
                'sessionInfo': {'parameters': {'order_found': a['found'], 'lookup_reason': a['reason'],
                                               'order_status': a['status'], 'tracking_url': a['tracking_url']}}})
        self.reply(404, {'error': 'not found'})


if __name__ == '__main__':
    ThreadingHTTPServer(('0.0.0.0', int(os.environ.get('PORT', 8080))), Handler).serve_forever()

Run it behind HTTPS with the four environment variables set. In WooCommerce, create the key under WooCommerce > Settings > Advanced > REST API with Read permission, for a user that can view orders.

Three details in the code matter more than they look:

  • The answer always has the same keys. Tidio's API call has an option, on by default, to run only when every mapped output is present. A "not found" answer without a tracking_url key would make the action fail instead of telling the customer something useful.
  • "Unavailable" is not "not found". If WooCommerce is down, times out or rejects the key, the bot says it can't check right now and hands over. It doesn't tell the customer their order doesn't exist, and the failure doesn't count against the order's rate limit.
  • A 3-second timeout to WooCommerce. Dialogflow CX retries a webhook once on a timeout or 5xx error, so a slow store could otherwise keep a customer waiting for twice the webhook timeout.

6. Connecting Tidio

On Shopify, skip the code. Tidio's ready-made "Get order status from Shopify" action recognizes order questions, asks for the email and order ID (optionally zip code or phone), and replies with payment status, fulfillment, items and tracking.

On WooCommerce, create a Lyro Action from scratch under Lyro AI Agent > Behavior > Actions & MCPs:

  1. Instructions (up to 5,000 characters) tell Lyro when to use it. For example: "Use this only when the customer asks where their order is, whether it has shipped, or for tracking. Ask for the order number from their confirmation email and the email address they used at checkout. This action only reads order status; it can't change, cancel or redirect orders. If it says the customer is being passed to a person, hand over the conversation."
  2. Input variables: "Order number" (text, required) and "Email" (text, required), each with a description of where the customer finds it.
  3. Action sequence: an API call node: POST to https://your-service/tidio, Bearer authentication with your BOT_TOKEN, JSON body with order and email filled from the input variables.
  4. Output variables: save message, status and tracking_url as session variables (not contact properties), so they only exist for this conversation.
  5. Test it in the Actions playground, then check the Action logs: they show the inputs, the full request and response, and whether each run succeeded.

The number of Actions depends on your plan: 1 on the free plan and on paid plans without Lyro, 3 with the Lyro AI Agent package, 10 on Plus and more on Premium. Order status needs one. If you prefer fixed scripts to an AI agent, Tidio Flows have the same API call action.

7. Connecting a custom Dialogflow CX bot

In Dialogflow CX, the same service answers on /dialogflow:

  1. Webhook: create a standard webhook pointing at https://your-service/dialogflow and add an authentication header Authorization: Bearer <BOT_TOKEN>.
  2. Page: an "Order status" page with two required form parameters: order_number (@sys.number) and email (@sys.email).
  3. Fulfillment: when the form is complete, call the webhook. The service returns the text to say and sets order_found, lookup_reason, order_status and tracking_url as session parameters.
  4. Routes: on $session.params.lookup_reason: no_match lets the customer try again, and rate_limited or unavailable go to your human hand-over. Also handle webhook.error events for network failures.

Two details to handle. @sys.number parameters arrive as JSON numbers, and a client can turn order 1042 into 1042.0; stripping non-digits from that gives 10420, so the service converts numbers before cleaning strings (our test sends a float to prove it). And Dialogflow CX retries once on timeouts and 5xx responses, so keep the webhook fast and safe to repeat (a read-only lookup is).

If you build on OpenAI instead, expose the same endpoint as a function tool in the Responses API. OpenAI has announced the end of the older Assistants API, so don't start a new bot on it. The identity rules stay the same: the model collects the number and email; the endpoint decides what may be said.

What Dialogflow doesn't give you is everything around the conversation: the chat widget on your store, the inbox where a person takes over, conversation history, and answers to all the questions that aren't about orders. Budget for building or integrating those, not just for the webhook.

8. What our test showed

All 18 checks passed against the real WooCommerce store:

  • "#<number>" with the email in different case and with spaces found the order. The reply said "shipped" and gave the UPS tracking link built from the stored tracking number. A custom carrier (Evri) kept its own link.
  • An order with 4 items listed the first 3 and "and more". A processing order without tracking returned empty tracking fields, not missing ones.
  • A wrong email and a non-existent order returned byte-for-byte identical answers. A checkout draft (an order never placed) counted as not found.
  • After 5 wrong emails, the order stayed locked even for the right email. A chat with 5 failed lookups was locked while another chat could still look up the same order.
  • Tidio-style requests got flat JSON with every key present, found or not. A wrong bearer token got 401.
  • Dialogflow-style requests with a float order number found the order and set the session parameters.
  • Store unreachable or the key revoked: "unavailable", with no failure counted. Store hanging: an answer after about 3 seconds.
  • The read-only key couldn't change an order (401).

9. Costs

ItemTidio (annual billing, October 2026)
Starter$24.17/month: 100 billable conversations, 50 one-off Lyro conversations
GrowthFrom $49.17/month, from 250 billable conversations
PlusFrom $300/month plus usage; OpenAPI, departments, Plus-level Action limits
Lyro AI AgentFrom $32.50/month for 50 Lyro conversations; Tidio says customers average a 75% resolution rate

For Dialogflow CX, check Google Cloud's current Conversational Agents pricing. You pay per request or session plus whatever runs your webhook, and the larger cost is the engineering time for the conversation design, hand-over and channel. The lookup service itself runs on the smallest VPS or serverless tier.

10. Our opinion: trade-offs

  • Build the lookup either way. It's about 110 lines, and it's where the security lives. Whichever bot sits in front of it, the identity check, the identical misses and the rate limits stay under your control.
  • Tidio for most stores: the widget, inbox, hand-over and AI agent are done, Shopify order status needs no code, and WooCommerce needs one Action pointed at your endpoint.
  • Gorgias if support is a team, not a side task: it's a full e-commerce helpdesk with order data next to every ticket, macros and its own AI agent across email, chat and social. Choose it when you're replacing a shared inbox, not just adding a bot.
  • Dialogflow CX when you have developers and need control over every turn of the conversation, voice channels or Google Cloud integration. Expect to build the parts Tidio includes.

Related: showing live WooCommerce context to LiveChat agents, scoring fraudulent WooCommerce orders and an n8n abandoned-cart workflow for Shopify.

11. Tools

Top picks

ToolRoleWhy it fits
TidioLive chat, Lyro AI agent, helpdeskReady-made Shopify order status action, Lyro Actions that call your WooCommerce lookup, Action logs, human hand-over and a free plan to start.
GorgiasE-commerce helpdesk with AI agentThe step up when support is a team: order data next to every ticket, macros, automation and an AI agent across channels.

Alternatives: free tools and references

We don't earn anything from these links.

12. Pitfalls

  • Custom order numbers. Plugins that change order numbers (prefixes, sequential numbering) make the number differ from the order ID the REST API looks up. Look orders up by your plugin's number field instead, or the bot will miss every order.
  • Returning addresses "to be helpful". Once the bot can say where a parcel goes, a guessed email becomes a privacy breach. Keep answers to status and tracking.
  • In-memory rate limits on several workers. Each process keeps its own counts. Run one process, or move the counters to Redis.
  • Letting the bot change orders. Address changes and cancellations need stronger proof than an email address. Hand them to a person, or use Tidio's visitor confirmation option and a separate, stricter endpoint.
  • Mapping the bot's "not found" to an error. Return 200 with a reason, and route on the reason. Error paths in both tools are for outages, not for customers who mistyped.
  • Guest orders with typos in the email. The bot can't help these customers. Its "I couldn't find that" reply should offer a person, not a loop.
Bottom line

Answer "Where is my order?" from live data, but only for the right person: order number plus checkout email, the same reply for every miss, rate limits on failures, and status and tracking only, through a read-only key. Put that in one small service you own. Then let Tidio's Lyro carry the conversation (no code at all on Shopify), move up to Gorgias when you need a full helpdesk, and build on Dialogflow CX only when you have the team to own the whole bot.

David Lewin
David Lewin

Founder of StealthScale.io and e-commerce infrastructure engineer writing field-tested playbooks on proxies, cloud VPS, anti-detect browsers and SEO.