Can AI call agents pull real-time order status from Shopify while on a customer call?
Yes. While the customer is on the line, the AI agent calls a tool that queries Shopify's Admin API and reads back the live status: unfulfilled, shipped, in transit, out for delivery or delivered, with the carrier and tracking number. The lookup happens at the moment the caller asks, not from a nightly sync, and takes about the length of a short "let me check that for you".
Three details decide whether it works on real calls, and most guides skip all three: finding the right order, Shopify's 2026 change to API tokens, and not reading order details to the wrong person. This guide covers each one.
How a mid-call Shopify lookup works
- The caller asks "Where's my order?" The agent recognises the intent from your instructions or a structured order-status flow.
- The agent collects an identifier. That is the order number if the caller has it. If not, the caller ID is already known, so the agent doesn't have to ask.
- The agent calls a tool, which is an HTTP request to Shopify (or to your endpoint in front of Shopify).
- Shopify returns the order with fulfillment status, shipments and tracking.
- The agent answers in plain speech, for example: "Your order 1042 shipped with UPS on Tuesday and is out for delivery today."
- If nothing matches, the agent asks for the email on the order or transfers to a person.
On Edesy, step 3 is a custom tool: a name, a
description that tells the agent when to use it, the parameters to collect, and the HTTP request to
make. Tools can use the caller's number directly through the {{call.phone_number}} placeholder.
Problem 1: "Order 1001" is not the order ID
This is the most common reason a Shopify voice integration "can't find" orders.
- The number customers see and say (#1001) is the order name.
- The Admin API's order ID is a different, much longer internal number.
- So
GET /admin/api/…/orders/1001.jsonreturns nothing, even though order #1001 exists.
The fix is to search by name instead of fetching by ID. With the GraphQL Admin API:
query ($q: String!) {
orders(first: 1, sortKey: CREATED_AT, reverse: true, query: $q) {
nodes {
name
displayFulfillmentStatus
cancelledAt
fulfillments(first: 1) {
displayStatus
estimatedDeliveryAt
trackingInfo(first: 1) { number company url }
}
}
}
}
Pass q as name:1001 for an order number. If the caller doesn't know it, use the phone, for example
phone:*4155550142 with the last ten digits, which tolerates the +1, spaces or dashes stored on the
order. Sorting newest-first means a repeat customer gets their latest order.
Two more things to handle in the tool:
- Spoken numbers come out messy. "Ten forty-two" can be transcribed as
1042,10 42or#1042. Strip everything except digits before searching. - The 60-day window. Apps with standard order access only see the last 60 days of orders. For
older orders you need the
read_all_ordersscope, which Shopify has to approve.
Problem 2: Shopify's 2026 token change
Most tutorials say: create a custom app in Shopify admin, copy the shpat_… Admin API token, and
paste it into your voice agent. That still works if you already have one of those apps. Since
January 1, 2026, new custom apps are created in Shopify's Dev Dashboard, and they work
differently:
- You exchange a client ID and secret for a token using the client-credentials grant.
- Shopify's docs say these tokens expire after 24 hours.
- They only work for stores in your own Shopify organization.
A token pasted into the agent today stops working tomorrow, and the agent starts telling every caller it "can't find the order". So pick your setup by what your store has:
| Your situation | Setup |
|---|---|
Existing legacy custom app with a permanent shpat_ token |
Direct: the agent's tool calls Shopify's GraphQL API with the token stored in the credential vault |
| New app from the Dev Dashboard (2026 onwards) | Small endpoint: your endpoint refreshes the token and does the lookup; the agent's tool calls your endpoint |
Option A: Direct to Shopify (legacy token)
Store the token in the credential vault, then define a tool like this (field names from the tools API):
{
"name": "get_order_status",
"description": "Look up a Shopify order's live status and tracking. Use when the caller asks where their order is, and they have given an order number.",
"parametersSchema": {
"order_number": { "type": "string", "description": "Order number the caller gave, digits only, e.g. 1042" }
},
"requiredParams": ["order_number"],
"httpMethod": "POST",
"httpUrl": "https://your-store.myshopify.com/admin/api/2026-10/graphql.json",
"httpHeaders": { "X-Shopify-Access-Token": "{{vault.shopify-admin}}", "Content-Type": "application/json" },
"httpBody": "{\"query\":\"query($q:String!){orders(first:1,sortKey:CREATED_AT,reverse:true,query:$q){nodes{name displayFulfillmentStatus cancelledAt fulfillments(first:1){displayStatus estimatedDeliveryAt trackingInfo(first:1){number company url}}}}}\",\"variables\":{\"q\":\"name:{{order_number}}\"}}",
"credentialRefs": [{ "placeholder": "shopify-admin", "vaultSlug": "shopify-admin" }]
}
Use a current stable API version. Shopify supports each version for at least 12 months, and requests to a retired version are served by the oldest supported one.
Option B: A small endpoint in front of Shopify (recommended)
Even without the token problem, an endpoint between the agent and Shopify is the better design. It can search by caller ID, verify the caller, and return one sentence ready to be spoken instead of raw JSON. A minimal version in Node.js:
const SHOP = process.env.SHOP; // "your-store" (without .myshopify.com)
let cached = { token: null, exp: 0 };
async function shopifyToken() {
if (cached.token && Date.now() < cached.exp) return cached.token;
const r = await fetch(`https://${SHOP}.myshopify.com/admin/oauth/access_token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.SHOPIFY_CLIENT_ID,
client_secret: process.env.SHOPIFY_CLIENT_SECRET,
}),
});
const { access_token, expires_in } = await r.json();
cached = { token: access_token, exp: Date.now() + (expires_in - 300) * 1000 }; // refresh early
return access_token;
}
const QUERY = `query ($q: String!) { orders(first: 1, sortKey: CREATED_AT, reverse: true, query: $q) {
nodes { name displayFulfillmentStatus cancelledAt
fulfillments(first: 1) { displayStatus estimatedDeliveryAt trackingInfo(first: 1) { number company } } } } }`;
async function findOrder(q) {
const r = await fetch(`https://${SHOP}.myshopify.com/admin/api/2026-10/graphql.json`, {
method: "POST",
headers: { "X-Shopify-Access-Token": await shopifyToken(), "Content-Type": "application/json" },
body: JSON.stringify({ query: QUERY, variables: { q } }),
});
return (await r.json()).data?.orders?.nodes?.[0] ?? null;
}
function speak(o) {
if (!o) return { found: false, say: "I couldn't find that order. Could you tell me the email you used?" };
if (o.cancelledAt) return { found: true, say: `Order ${o.name} was cancelled.` };
const f = o.fulfillments?.[0];
const t = f?.trackingInfo?.[0];
const carrier = t?.company ? ` with ${t.company}` : "";
switch (f?.displayStatus) {
case "DELIVERED": return { found: true, say: `Order ${o.name} was delivered${carrier}.` };
case "OUT_FOR_DELIVERY": return { found: true, say: `Order ${o.name} is out for delivery today${carrier}.` };
case "IN_TRANSIT": return { found: true, say: `Order ${o.name} is on its way${carrier}.` };
}
if (o.displayFulfillmentStatus === "FULFILLED") return { found: true, say: `Order ${o.name} has shipped${carrier}.` };
return { found: true, say: `Order ${o.name} hasn't shipped yet. It's being prepared.` };
}
app.post("/voice/order-status", async (req, res) => {
if (req.get("x-tool-secret") !== process.env.TOOL_SECRET) return res.sendStatus(401);
const clean = (v) => (typeof v === "string" && !v.startsWith("{{") ? v.replace(/\D/g, "") : "");
const orderNumber = clean(req.body.order_number);
const callerDigits = clean(req.body.caller).slice(-10);
let order = orderNumber ? await findOrder(`name:${orderNumber}`) : null;
if (!order && callerDigits) order = await findOrder(`phone:*${callerDigits}`);
res.json(speak(order));
});
The agent's tool then posts to your endpoint, passing the caller ID automatically:
{
"name": "get_order_status",
"description": "Check the caller's Shopify order status. Call it as soon as they ask about an order; include the order number only if they gave one.",
"parametersSchema": {
"order_number": { "type": "string", "description": "Order number if the caller gave one, digits only" }
},
"httpMethod": "POST",
"httpUrl": "https://your-app.com/voice/order-status",
"httpHeaders": { "x-tool-secret": "{{vault.order-tool-secret}}", "Content-Type": "application/json" },
"httpBody": "{\"order_number\":\"{{order_number}}\",\"caller\":\"{{call.phone_number}}\"}",
"credentialRefs": [{ "placeholder": "order-tool-secret", "vaultSlug": "order-tool-secret" }]
}
The clean() helper is there for a reason. When the agent doesn't supply order_number, the
platform leaves the placeholder text {{order_number}} in place rather than blanking it, so the
endpoint treats anything starting with {{ as empty.
Problem 3: Don't read orders to the wrong person
A phone line is not a logged-in session. Before the agent reads tracking or delivery details:
- Caller ID matches the order's phone: sharing status is reasonable.
- Different number: ask for a second detail, such as the email or ZIP code on the order, and check it in your endpoint before returning anything.
- Never read the full address. "Delivering to your address in Austin" is enough.
The Option B endpoint is the natural place to enforce this. Return found: true only when the check
passes, and the agent never sees data it shouldn't say.
Make it sound natural
- Cover the lookup. Tell the agent to say a short line before calling the tool ("One moment, let me pull that up") so the caller never hears silence.
- Return speech, not data. A ready sentence from your endpoint is faster for the agent to say and harder to garble than a JSON blob.
- Keep the response small. Request only the fields you speak. Fewer tokens means a faster reply.
- Plan the next step. If it was delivered but the customer says it never arrived, hand off to a person, or log the call outcome so support follows up.
What a call sounds like
Caller: Hi, I'm checking on my order. Agent: Sure. I can see you're calling from the number on your order. One moment while I check... Your order 1042 is out for delivery today with UPS. Caller: Great, thanks. Agent: You're welcome. Is there anything else?
No hold music and no ticket. "Where is my order" is one of the most common calls a store gets, and this is how it ends in under a minute.
Try it with your store
Build an agent, add the get_order_status tool (Option A if you have a legacy token, Option B
otherwise), and call it from the phone number on a recent test order.
Start building on Edesy, no demo needed →
More on this: Shopify + AI voice overview, AI voice agents for online stores and the order-status use case.