Build a COD confirmation tool
Find the order by phone, confirm or cancel it, schedule the call-back.
Most orders in Bangladesh are cash on delivery, and most merchants call the customer before shipping to confirm. This guide builds that loop over the API: pull the queue, find an order by the number that is calling, check the customer's history, and record what they said.
You need a key with orders:write (which implies orders:read) and
customers:read. The customer history in step 3 needs orders:read as well,
which orders:write already covers.
1. Pull the queue
Two lists make up a call agent's day: new orders nobody has confirmed yet, and call-backs that are now due.
New orders are status=pending. Due call-backs are orders in follow_up
whose follow-up time has passed — status=follow_up with the current time as
follow_up_before. That is the same list the dashboard's overdue follow-up
count and the MCP follow_up_due queue use:
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/orders?status=follow_up&follow_up_before=2026-10-02T12%3A00%3A00.000Z&expand=customer" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/orders");
Object.entries({
status: "follow_up",
follow_up_before: "2026-10-02T12:00:00.000Z",
expand: "customer",
}).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const { data, meta } = await response.json();import os
import requests
response = requests.get(
"https://api.shopdeskbd.com/ext/v1/orders",
params={
"status": "follow_up",
"follow_up_before": "2026-10-02T12:00:00.000Z",
"expand": "customer",
},
headers={
"Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
page = response.json()
rows, meta = page["data"], page["meta"]<?php
$key = getenv('SHOPDESK_API_KEY');
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/orders?status=follow_up&follow_up_before=2026-10-02T12%3A00%3A00.000Z&expand=customer');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
$rows = $response['data'];
$meta = $response['meta'];follow_up_before and follow_up_after only ever match orders that have a
follow-up scheduled, so combine them for a window ("today") or use one alone
for "overdue". Keep status=follow_up in the query: an order that has since
been confirmed or cancelled keeps its old follow_up_at, and the status is
what takes it off the list. Every order carries its follow_up_at, follow_up_note and
assignee ({ id, name }, or null), and assignee_id narrows the list to
one agent's orders. Assigning orders is done in the dashboard; the API reads
the assignment.
2. Find the order by phone
When a customer calls in, all you have is their number, in whatever form your
phone system shows it. search matches order numbers, customer names and
customer phones, and a phone matches in any Bangladeshi form:
+8801712345678, 8801712345678, 01712345678 and +880 1712-345678 all
find the same orders, because phones are stored as 01XXXXXXXXX and the search
also tries the term rewritten that way. Order numbers and names are matched as
typed, so 20260929-0001 still finds ORD-20260929-0001.
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/orders?search=%2B8801712345678&status=pending&expand=customer" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/orders");
Object.entries({
search: "+8801712345678",
status: "pending",
expand: "customer",
}).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const { data, meta } = await response.json();import os
import requests
response = requests.get(
"https://api.shopdeskbd.com/ext/v1/orders",
params={
"search": "+8801712345678",
"status": "pending",
"expand": "customer",
},
headers={
"Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
page = response.json()
rows, meta = page["data"], page["meta"]<?php
$key = getenv('SHOPDESK_API_KEY');
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/orders?search=%2B8801712345678&status=pending&expand=customer');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
$rows = $response['data'];
$meta = $response['meta'];The same rule applies to GET /customers?search=, which also matches name and
email. To look up exactly one number, use GET /customers?phone= instead: it
matches the phone only, in any Bangladeshi form, so an email such as
01712345678@gmail.com never gets in the way.
3. Check the customer's history
Before you confirm a ৳5,000 COD order, it helps to know whether this customer
usually keeps what they order. Add expand=stats to the customer read:
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/customers/REPLACE_WITH_CUSTOMER_ID?expand=stats" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/customers/REPLACE_WITH_CUSTOMER_ID");
Object.entries({
expand: "stats",
}).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const object = await response.json();import os
import requests
response = requests.get(
"https://api.shopdeskbd.com/ext/v1/customers/REPLACE_WITH_CUSTOMER_ID",
params={
"expand": "stats",
},
headers={
"Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
obj = response.json()<?php
$key = getenv('SHOPDESK_API_KEY');
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/customers/REPLACE_WITH_CUSTOMER_ID?expand=stats');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}| Field | What it counts |
|---|---|
orders_count | Orders that are not cancelled, returned or spam — the count the dashboard's customer page shows. |
total_spent | The total of their delivered orders, delivery and tax included. |
last_order_at | When their latest order was placed (spam excluded). |
returned_count | Orders that came back whole. |
4. Record the outcome
The customer confirmed. Move the order to confirmed with the status
route. The same transition rules as the dashboard apply:
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X PATCH "https://api.shopdeskbd.com/ext/v1/orders/REPLACE_WITH_ORDER_ID/status" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: REPLACE_WITH_A_UUID" \
-H "Content-Type: application/json" \
-d '{
"status": "confirmed",
"note": "Confirmed on the phone."
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/orders/REPLACE_WITH_ORDER_ID/status");
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
"Idempotency-Key": "REPLACE_WITH_A_UUID",
"Content-Type": "application/json",
},
body: JSON.stringify({
"status": "confirmed",
"note": "Confirmed on the phone."
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const object = await response.json();import os
import requests
payload = {
"status": "confirmed",
"note": "Confirmed on the phone.",
}
response = requests.patch(
"https://api.shopdeskbd.com/ext/v1/orders/REPLACE_WITH_ORDER_ID/status",
json=payload,
headers={
"Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
"Idempotency-Key": "REPLACE_WITH_A_UUID",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
obj = response.json()<?php
$key = getenv('SHOPDESK_API_KEY');
$payload = json_encode([
'status' => 'confirmed',
'note' => 'Confirmed on the phone.',
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/orders/REPLACE_WITH_ORDER_ID/status');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: REPLACE_WITH_A_UUID', 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}The customer cancelled. Use POST /orders/{id}/cancel, which releases the
reserved stock.
No answer, or "call me after 4pm". Move the order to follow_up with the
status route (as for a confirmation, with "status": "follow_up"), then set
the call-back time with PATCH /orders/{id}:
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS -X PATCH "https://api.shopdeskbd.com/ext/v1/orders/REPLACE_WITH_ORDER_ID" \
-H "Authorization: Bearer $SHOPDESK_API_KEY" \
-H "Idempotency-Key: REPLACE_WITH_A_UUID" \
-H "Content-Type: application/json" \
-d '{
"follow_up_at": "2026-10-02T10:00:00.000Z",
"follow_up_note": "No answer at 11am — try after 4pm."
}'const url = new URL("https://api.shopdeskbd.com/ext/v1/orders/REPLACE_WITH_ORDER_ID");
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
"Idempotency-Key": "REPLACE_WITH_A_UUID",
"Content-Type": "application/json",
},
body: JSON.stringify({
"follow_up_at": "2026-10-02T10:00:00.000Z",
"follow_up_note": "No answer at 11am — try after 4pm."
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const object = await response.json();import os
import requests
payload = {
"follow_up_at": "2026-10-02T10:00:00.000Z",
"follow_up_note": "No answer at 11am — try after 4pm.",
}
response = requests.patch(
"https://api.shopdeskbd.com/ext/v1/orders/REPLACE_WITH_ORDER_ID",
json=payload,
headers={
"Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
"Idempotency-Key": "REPLACE_WITH_A_UUID",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
obj = response.json()<?php
$key = getenv('SHOPDESK_API_KEY');
$payload = json_encode([
'follow_up_at' => '2026-10-02T10:00:00.000Z',
'follow_up_note' => 'No answer at 11am — try after 4pm.',
]);
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/orders/REPLACE_WITH_ORDER_ID');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: REPLACE_WITH_A_UUID', 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}PATCH /orders/{id} changes the order note, follow_up_at and
follow_up_note, and nothing else. It works on an order in any status, runs
the same code as the dashboard's order-details edit, and cannot touch prices,
lines or stock. Send null to clear a field and leave out a field to keep it.
Send at least one field.
Both pending and confirmed orders can move to follow_up. Once the order
is confirmed or cancelled, its status takes it off the call list; the old
follow_up_at stays on the order, as it does in the dashboard. Send
{"follow_up_at": null, "follow_up_note": null} if you want it cleared too.
5. Stay in sync
If other staff work the same orders in the dashboard, subscribe to
order.updated. It fires when the status moves (data.previous.status) and
when the note, follow-up or delivery address changes. On those changes
data.previous holds the old note, follow_up_at, follow_up_note or
shipping_address, named as in data.object. See the
event catalog.
With the store MCP server
The same loop runs from an AI assistant connected to the
store MCP server with a key holding
orders:write and customers:read. Each step above is one tool call:
| Step | Tool |
|---|---|
| Pull the queue | list_orders_needing_attention with kind: "unconfirmed" (pending orders) or "follow_up_due" (orders in follow_up whose call-back time has passed), oldest first |
| Find the order by phone | find_orders with the number in any form, an order number or a name |
| Check the history | get_customer_summary with the phone or customer_id: the stats and the latest five orders |
| Record the outcome | record_call_outcome |
record_call_outcome takes the order id, an outcome and an optional note:
outcome | What it does |
|---|---|
confirmed | Moves the order to confirmed. |
cancelled | Cancels it through the cancel route; reserved stock is released. |
call_back | Needs call_back_at. Moves the order to follow_up and sets the call-back time, with note as the reason. |
no_answer | Moves the order to follow_up; sets the call-back time if you send call_back_at. |
The note also goes on the order timeline with the status change. The status
rules are the dashboard's: an order that cannot move to the new status is
refused before anything changes. Repeating the same outcome changes nothing
more, and an idempotency_key makes a retried call return the first answer.
The result lists the steps taken and the order as it now stands.
update_order_details edits the note and call-back directly, like
PATCH /orders/{id}. The order note it sets replaces the whole note, and
notes these tools show are shortened to 500 characters — so an assistant
should never rebuild a note from a value it read. Notes, addresses and names
come back in labelled fields with links and markup removed; see
MCP tools.