Repository navigation
Tutorial Order Tracking
An online shop's assistant answers "where is my order?": it asks for the order number, checks it in your shop, then asks the carrier where the parcel is, and tells the visitor in one or two sentences.
Visitor Hi, where's my order? It's been a week.
Assistant Sorry about the wait. What's your order number? It's in your confirmation email.
Visitor 1042
Assistant Thanks. And the email address you ordered with?
Visitor ada@example.com
Assistant It's on its way: it left the Sydney depot this morning, and USPS expects it on
Thursday 12 October.
What it teaches: asking for a value in the chat and saving it, one tool
feeding another ({{data.*}}), and picking a few fields from a big response.
Template: order-tracking (import it).
You need: an endpoint on your shop (step 1) and a
Shippo account for tracking (a free test token works
for everything below).
Visitor asks ─▶ order_lookup (your shop) ─▶ track_shipment (Shippo) ─▶ answer
carrier, tracking number status, place, ETA
After the chat ─▶ order_number (extract) ─▶ saved order number and email
Three tools: the two HTTP lookups run during the chat when named in the prompt; the extract tool runs after the chat without a prompt reference.
| Tool | Kind | What it does |
|---|---|---|
order_number |
extract | Saves the order number and email on the conversation (and as custom attributes, so your team sees them) |
order_lookup |
http | Asks your shop for the order: its status, carrier and tracking number |
track_shipment |
http | Asks Shippo where the parcel is, using what order_lookup returned |
The assistant should never see one customer's order because another typed its number. So the lookup takes the order number and the email together, and your endpoint answers only when both match.
GET https://shop.example.com/api/helppuff/orders/1042?email=ada@example.com
with Authorization: Bearer <your key>:
{ "found": true, "status": "fulfilled", "carrier": "usps", "tracking_number": "9205590164917312751089", "items": ["Linen shirt (M)", "Canvas tote"] }When the number and email don't match, answer HTTP 200 with:
{ "found": false }Not a 404: a 404 tells the assistant the check failed, and it would say "I couldn't check right now" instead of "those details don't match an order".
carrier is Shippo's carrier token
(usps, ups…). While testing, return "carrier": "shippo" and
"tracking_number": "SHIPPO_TRANSIT": Shippo's test token answers those
with a sample parcel on its way (SHIPPO_DELIVERED, SHIPPO_FAILURE… give
the other statuses).
A sketch for Shopify (a Cloudflare Worker)
// GET /api/helppuff/orders/:number?email=… → { found, status, carrier, tracking_number, items }
// Secrets: HELPPUFF_KEY (what HelpPuff sends), SHOPIFY_TOKEN (an Admin API token). Check the API version in Shopify's docs.
const CARRIERS: Record<string, string> = { USPS: 'usps', UPS: 'ups', FedEx: 'fedex', 'DHL Express': 'dhl_express' };
export default {
async fetch(request: Request, env: { HELPPUFF_KEY: string; SHOPIFY_TOKEN: string }): Promise<Response> {
if (request.headers.get('Authorization') !== `Bearer ${env.HELPPUFF_KEY}`) return new Response('Unauthorized', { status: 401 });
const url = new URL(request.url);
const number = url.pathname.split('/').pop()!.replace(/^#/, '');
const email = (url.searchParams.get('email') ?? '').trim().toLowerCase();
const query = `query($q: String!) { orders(first: 1, query: $q) { nodes { email displayFulfillmentStatus lineItems(first: 5) { nodes { name } } fulfillments { trackingInfo { company number } } } } }`;
const res = await fetch('https://your-store.myshopify.com/admin/api/2025-07/graphql.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': env.SHOPIFY_TOKEN },
body: JSON.stringify({ query, variables: { q: `name:#${number}` } }),
});
const order = ((await res.json()) as any).data?.orders?.nodes?.[0];
// Same answer for "no such order" and "wrong email": a number on its own reveals nothing.
if (!order || order.email?.toLowerCase() !== email) return Response.json({ found: false });
const tracking = order.fulfillments?.[0]?.trackingInfo?.[0];
return Response.json({
found: true,
status: order.displayFulfillmentStatus.toLowerCase(),
carrier: tracking ? (CARRIERS[tracking.company] ?? tracking.company.toLowerCase()) : null,
tracking_number: tracking?.number ?? null,
items: order.lineItems.nodes.map((i: { name: string }) => i.name),
});
},
};An extract tool calls no external API. After the chat ends, it reads the visitor's messages and saves the number and email they actually gave.
helppuff tools add order_number --extract \
--field order_number='The order number from the confirmation email, like 1042.' \
--field order_email='The email address they ordered with.' \
--description 'Find the order number and email address the visitor gave during the chat.'Saving matters even though the lookup gets the same values: the number shows
on the conversation as a custom attribute (order_number: 1042), your team
can filter by it, and it goes to webhooks and exports.
helppuff tools add order_lookup \
--url 'https://shop.example.com/api/helppuff/orders/{{args.order_number}}?email={{args.email}}' \
--header 'Authorization: Bearer ${SHOP_API_KEY}' \
--param order_number='The order number, like 1042 (without #).' \
--param email='The email address the order was placed with.' \
--pick found,status,carrier,tracking_number,items \
--description 'Look an order up by its number and the email address it was placed with: its status, carrier and tracking number.'-
{{args.order_number}}and{{args.email}}are filled in by the assistant. Their descriptions tell it what to ask for; "without #" stops it sending#1042. Each value is URL-encoded for its place, so whatever the visitor types stays a value. -
${SHOP_API_KEY}is read from.envand stored encrypted; it is never shown again. -
--pickkeeps only what the assistant needs.
helppuff tools add track_shipment \
--url 'https://api.goshippo.com/tracks/{{data.order_lookup.carrier}}/{{data.order_lookup.tracking_number}}' \
--header 'Authorization: ShippoToken ${SHIPPO_TOKEN}' \
--pick tracking_status.status,tracking_status.status_details,tracking_status.status_date,tracking_status.location.city,tracking_status.location.state,eta \
--timeout 8000 \
--description 'Where the order’s parcel is now and when it should arrive. Call it after order_lookup found a tracking number.'There is no {{args.*}} here: the carrier and tracking number come from
what order_lookup returned in this chat, as {{data.order_lookup.carrier}}.
The assistant never copies a tracking number, so it cannot mistype one.
Shippo's answer is long (the whole tracking history). The picked paths keep six values, nested as they were:
{ "tracking_status": { "status": "TRANSIT", "status_details": "Your shipment has departed from the origin.", "status_date": "2026-10-10T08:12:00Z", "location": { "city": "Sydney", "state": "NSW" } }, "eta": "2026-10-12T00:00:00Z" }helppuff tools test order_lookup --arg order_number=1042 --arg email=ada@example.com
helppuff tools test track_shipment --data order_lookup.carrier=shippo --data order_lookup.tracking_number=SHIPPO_TRANSIT--data stands in for what another tool would have returned. On the
dashboard, Test in the tool's window does the same, and ticking keys
there is the same as --pick.
## Order questions
When someone asks where their order is, or when it will arrive:
1. Ask for their order number and the email address they ordered with, one at a time, unless you already have them. Their email may be in the form: {{lead.email}}.
2. Look the order up with {{order_lookup}}.
- If it is not found, say the order number and email don't match an order we can see, and ask them to check both. Never say whether an order number exists on its own.
3. If the order has a tracking number, check the parcel with {{track_shipment}}. Then tell them, in one or two sentences, where it is and the expected delivery day if there is one ("Thursday 12 October").
4. If it has no tracking number yet, say it is being packed and usually ships within one business day.
What the tracking statuses mean:
- PRE_TRANSIT: the label is made; the carrier has not collected the parcel yet.
- TRANSIT: on its way.
- DELIVERED: delivered. If they say they don't have it, say sorry and offer a callback from the team.
- RETURNED or FAILURE: there is a problem with the delivery. Say sorry and offer a callback.
- UNKNOWN: the carrier has no news yet. Suggest checking again tomorrow.
Never promise a delivery date the tracking does not show.
Why it is written this way:
- Numbered steps, a tool at each. The assistant calls tools in the order the steps give, and only the tools the prompt names. Step 3's condition ("if the order has a tracking number") stops it calling Shippo with nothing.
-
"One at a time, unless you already have them." Visitors answer one
question better than two, and
{{lead.email}}(from the pre-chat form, if you ask for email there) saves a question. - What to say for each outcome, including not found. Without it, the assistant improvises, and "this order doesn't exist" would tell anyone guessing numbers which ones are real.
- What each status means, in the shop's words. Carrier statuses are codes; the prompt turns them into what to say and what to offer.
- No settings repeated. Tone, length and "never invent" are settings and built-in rules; the prompt only says what is specific to this shop.
helppuff ask "Where is my order 1042? I ordered with ada@example.com"Then chat on your site like a customer. On the dashboard, the conversation shows Data from tools: what each tool returned. After the chat ends, the order number the visitor stated also appears as an attribute.
| What you see | Why, and what to do |
|---|---|
| "I couldn't check that right now" | A tool failed (4xx, 5xx, timeout). helppuff tools list shows each tool's last status; test it with the same values |
| It asks for the order number again | Check the order_lookup parameter description and the prompt's instruction to use a number the visitor already gave |
It never calls track_shipment
|
order_lookup returned no tracking_number, or the tool is missing from the prompt |
| Shippo answers 401 | A test token only tracks the shippo test carrier; use your live token for real parcels |
Next: Send chats to your CRM, to send the order number and a summary to your help desk when the chat ends.
Start
Tutorials
- All tutorials
- Order tracking
- Send chats to your CRM
- Nearest store
- Verified account changes
- Logged-in customer support
- Product and stock
- Hot leads to Slack
- Knowledge base or tool?
- Testing and monitoring
- Why the AI never books
Configure
- Configuration
- Configuration reference
- CLI reference
- Knowledge base
- Prompt and instructions
- Tools: your APIs in the chat
- Agent files: import and export
- Signed-in visitors
- The widget
- The playground
- Leads and callbacks
- Jobs
- Photos and files
- Bookings and the calendar
- Integrations
- Live chat
- Telegram
- Webhooks
- Email notifications
API
API reference
- Overview
- Account
- Chat
- Conversations
- Live chat
- Labels and notes
- Leads
- Callbacks
- Jobs
- Appointments
- Integrations
- Knowledge
- Prompt
- Tools
- Settings
- Webhooks
- Analytics
- Team
- API keys
- Audit
Providers
- Overview
- Models and providers
- Custom model
- Custom knowledge base
- Workers AI (default)
- AI models: speed, quality, cost
- Cloudflare AI Search
- OpenAI
- Gemini
- Anthropic Claude
- Retell
- Your own API
Operate
- Deployment
- Turn on Turnstile
- Upgrading
- Costs and limits
- Cloudflare Free plan limits
- Security
- Troubleshooting
Build