Use cases

How Shopify brands answer 'where is my order?' over iMessage

ZeroSettle lets a Shopify or DTC brand answer “where is my order?” over iMessage: a “Text us” link prefilled with the order number opens Messages on the customer’s iPhone, ZeroSettle posts the text to your webhook about 1–2 seconds after it reaches your number, and your handler shows a typing bubble while it looks the order up in Shopify, then replies with status and tracking. Because the customer texts first, order-status support is never limited by ZeroSettle’s new-contact caps, and the tracking link is tappable. ZeroSettle’s hosted AI agent can’t do this out of the box: it answers from the business context and files you give it, and has no built-in Shopify lookup.

Key takeaways

  • What it is: a “Text us” link on order pages and shipping emails, answered from Shopify order data by your own webhook handler, on your brand’s own iMessage number.
  • Who it fits: Shopify and DTC brands whose customers mostly text from iPhones, with a developer to run a small handler.
  • Price: Pro is $49/month for one number; Growth is from $599/mo. A handler you run uses no AI credits.
  • Main limit: throughput, not volume. Inbound is never capped, but a line sends one message at a time, so a burst of questions is answered in turn.
  • What it does not do: there is no Shopify app and no SMS fallback for Android, and the hosted AI agent has no built-in order lookup.

Who uses iMessage for order-status support?

iMessage order-status support suits Shopify and DTC brands, such as beauty, apparel and home-goods stores, whose customers mostly use iPhones. ZeroSettle is an iMessage API: a real phone number on real iMessage, running on Apple hardware we operate for you, that your code texts customers from and hears back on through a bearer token and signed webhooks.

“Where is my order?” (WISMO) questions are lookups: the carrier, tracking link and estimated delivery already sit on the order’s fulfillment in Shopify, so a handler can answer most of them without a person. A chat widget is gone when the tab closes; an iMessage thread stays in Messages, so the follow-up (“it says delivered but it’s not here”) lands under the first answer. Pre-sale product questions are a different job (text-to-buy over iMessage), and more are in 50 ways businesses use iMessage.

How iMessage order-status support works, step by step

The customer starts the thread, ZeroSettle delivers the text to your webhook, and your handler answers from Shopify.

  1. Put a “Text us” link on the order status page and in shipping emails: an sms: link to your number with the body prefilled as “Order #20431”.
  2. ZeroSettle POSTs a signed message.received event to your webhook about 1–2 seconds after the line receives the text (how iMessage webhooks are signed and retried).
  3. The handler parses the order number, calls POST /api/imessage/typing so the customer sees the “…” bubble, and looks the order up in the Shopify Admin API.
  4. If the order’s phone matches the sender, the handler replies with POST /api/imessage/send: carrier, estimated delivery and tracking link. The send clears the bubble.
  5. A damage photo arrives with the file (kind: "image", a camera photo as a JPEG) and goes to a person who approves the replacement (photos, voice and tapbacks over the API).
  6. Anything else goes to a person too: your team answers in the ZeroSettle console, or your handler relays it to Slack or a help desk.

Why this guide doesn’t use the hosted AI agent

ZeroSettle’s hosted AI agent (Beta) answers from the business context and files you give it, so it can explain your shipping policy but has no built-in way to read order #20431. The docs also let you connect it to your own MCP servers; this guide uses a handler you run. It is either-or: on numbers the agent manages, inbound texts never reach your webhook and your own sends are refused with 403 managed_by_zerosettle. This guide answers WISMO from your own handler, which can also call your own model.

Example conversation: order-status support

A customer texts an order number, sees the store typing, and gets the delay and a new ETA in one reply; the damage photo goes to a person. The greeting and the weather detail need a fuller handler that also reads the carrier’s tracking data; the code below answers from Shopify alone.

  1. Tue 10:14 AM
  2. Customer: Hi! order #20431 hasn’t moved in 4 days?
  3. Fernwood Home is typing
  4. Fernwood Home: Hi Alicia! It’s at the UPS hub in Memphis, delayed by weather. New ETA is Thursday: fernwood.example/track/20431
  5. Customer: ok thanks. also one of the mugs from my last order came chipped
  6. Customer: [Photo of a chipped mug]
  7. Fernwood Home: So sorry! A replacement ships today, no need to send it back ❤️
Illustrative example with a fictional home-goods store. Blue bubbles come from the store’s number; gray bubbles are the customer’s.

Setup: an order-lookup handler with a typing bubble

The setup is a prefilled link in Shopify’s notifications and one webhook handler, following the customer support section of the ZeroSettle docs.

liquid (shipping confirmation email)
<a href="sms:+18055550100&body=Order%20{{ order_name | url_encode }}">Text us about this order</a>

Shopify’s order_name is usually a pound sign plus the order number (#20431). url_encode turns the # into %23; a raw # starts a URL fragment and can cut the order number off. The filter writes spaces as +, so the space stays outside it as %20.

python
# wismo.py: run with  uvicorn wismo:app --port 8100
import asyncio, hashlib, hmac, json, os, re, time
import httpx
from fastapi import BackgroundTasks, FastAPI, Request, Response

app = FastAPI()
ZS = "https://portal.zerosettle.io"
TOKEN, SECRET = os.environ["ZEROSETTLE_TOKEN"], os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
SHOP = f"https://{os.environ['SHOPIFY_STORE']}.myshopify.com/admin/api/2026-10/graphql.json"
QUERY = """query ($q: String!) { orders(first: 1, query: $q) { edges { node {
  name phone cancelledAt shippingAddress { phone }
  fulfillments(first: 5) { estimatedDeliveryAt trackingInfo(first: 1) { company number url } } } } } }"""
lock, seen = asyncio.Lock(), set()     # a line sends one message at a time; keep seen in your DB

def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        t, v1 = int(parts["t"]), parts["v1"]
    except Exception:
        return False
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

async def zs(path, body):              # True when ZeroSettle answered 200
    try:
        async with lock, httpx.AsyncClient() as c:
            r = await c.post(ZS + path, headers={"Authorization": f"Bearer {TOKEN}"}, json=body, timeout=90)
        return r.status_code == 200
    except httpx.HTTPError:
        return False

async def find_order(name):
    async with httpx.AsyncClient() as c:
        r = await c.post(SHOP, headers={"X-Shopify-Access-Token": os.environ["SHOPIFY_ADMIN_TOKEN"]},
                         json={"query": QUERY, "variables": {"q": f"name:{name}"}}, timeout=20)
    edges = ((r.json().get("data") or {}).get("orders") or {}).get("edges", [])
    return edges[0]["node"] if edges and edges[0]["node"]["name"] == name else None

def owns(o, handle):   # the order's phone or shipping phone is the sender's number
    last10 = lambda s: re.sub(r"\D", "", s or "")[-10:]
    mine = "" if "@" in handle else last10(handle)
    return len(mine) == 10 and mine in (last10(o["phone"]), last10((o["shippingAddress"] or {}).get("phone")))

def answer(o):
    if o["cancelledAt"]:
        return f"Order {o['name']} was cancelled."
    f = next((f for f in o["fulfillments"] if f["trackingInfo"]), None)
    if f is None:
        return f"Order {o['name']} doesn't have tracking yet."
    t, eta = f["trackingInfo"][0], f["estimatedDeliveryAt"]
    return (f"Order {o['name']} shipped with {t['company'] or 'the carrier'}."
            + (f" ETA {eta[:10]}." if eta else "") + f" Track it: {t['url'] or t['number']}")

def to_team(m):   # your help desk here; m["image"]["url"] is fetched with the same bearer
    print("for a person:", m["handle"], m.get("text"), m.get("image"))

async def handle(m):
    hit = re.search(r"#\s*(\d{3,})", m.get("text") or "")
    if m.get("kind") == "image" or not hit:
        return to_team(m)                  # photos and other questions go to a person
    name = "#" + hit.group(1)
    if not lock.locked():                  # skip the bubble while a burst is queued
        await zs("/api/imessage/typing", {"to": m["handle"], "state": "start", "ttl_s": 20})
    try:
        order = await find_order(name)
    except (httpx.HTTPError, ValueError):
        order = None
    ok = order is not None and owns(order, m["handle"])
    text = answer(order) if ok else f"I couldn't find {name} for this number. Someone from our team will reply here."
    if not await zs("/api/imessage/send", {"to": m["handle"], "text": text}) or not ok:
        to_team(m)                         # no match, or the send was refused (say, after STOP)

@app.post("/webhook")
async def webhook(req: Request, tasks: BackgroundTasks):
    raw = await req.body()
    if not verify(SECRET, req.headers.get("X-ZeroSettle-Signature", ""), raw):
        return Response(status_code=401)
    ev = json.loads(raw); m = ev.get("message")
    if m is None or ev["event"] != "message.received" or (m["id"], ev["event"]) in seen:
        return {"ok": True}                # react only to new inbound; retries repeat id and event
    seen.add((m["id"], ev["event"]))
    tasks.add_task(handle, m)              # answer 2xx now, work afterwards
    return {"ok": True}

The ownership check stops someone who texts a neighboring order number from reading a stranger’s tracking; an order with no phone on it, or a text from an iMessage email address, goes to a person. The Shopify token needs read_orders, and the Admin API returns only the last 60 days of orders without read_all_orders, so older order numbers go to a person too. Register the handler with PUT /api/imessage/webhook and store the signing secret, which is shown once (webhook steps in the docs).

Limits and consent for order-status support

Order-status support is customer-initiated, so ZeroSettle’s new-contact limits never apply to it. Throughput does: a line sends one message at a time.

ZeroSettle limits as they apply to order-status support (API docs, checked October 12, 2026)
LimitValueEffect on WISMO
New contacts per numberPro 5/day; Growth 50/dayNone: anyone who texted the line first is never limited
Typing callNormally under 2 sNot billed, never a new contact
SendNormally 2–5 s, one at a time per lineAbout 12 s in the queue returns 429 line_busy; retry after 2 s
Unanswered messages2 until they reply, on every planCaps unprompted “delivered” follow-ups

Say weather stalls a UPS hub and 30 customers text within minutes. Each answer holds the line for its send (normally 2–5 s) plus under 2 s of typing, so the last answer goes out roughly 1 to 3½ minutes after the burst starts. The handler skips typing while the line is busy, and console replies wait in the same queue.

Answering a customer who texted about an order is the purpose of the thread; promotions are not. ZeroSettle’s docs: “Do not send automated marketing into a support thread. It is a different consent, and it is how support lines get reported as spam.” ZeroSettle’s docs add that in the US, marketing texts generally need written consent and that nothing promotional should go out before 8 am or after 9 pm in the customer’s time zone; rules vary, so check yours with counsel. This isn’t legal advice.

When ZeroSettle is not a fit for order-status support

ZeroSettle is the wrong tool for order-status support when nobody can run a handler or most buyers can’t receive iMessage.

  • You want a no-code answer. The hosted AI agent has no built-in order lookup.
  • Most customers use Android. iMessage is Apple-only and other phones get SMS or RCS, the sms: link opens their SMS app, and ZeroSettle has no SMS fallback, so keep email or chat for them.
  • Your orders rarely carry a phone number. The handler matches the sender to the order’s phone, so with email-only checkouts most texts go to a person.
  • You would send promotions into the support thread. That needs separate consent.
  • You need a native Shopify app. There is none; you build and host the handler.

How much does iMessage for order-status support cost?

Order-status support costs the plan price: ZeroSettle does not charge per message, and a handler you run uses no AI credits.

ZeroSettle plans (zerosettle.io, checked October 12, 2026)
PlanPriceWhat you get
Pro$49/monthOne number to start, unlimited messages with people who message you first, 5 new contacts a day, no setup fee, self-serve
GrowthFrom $599/moSeveral numbers working as one, dedicated support, after a demo

Both carry a 30-day money-back guarantee on the subscription. Hosted-agent texts use AI credits: 1,000 a month on Pro, 5,000 on Growth, and packs of 1,000 for $50 or 5,000 for $200. See ZeroSettle pricing or get a number.

Frequently asked questions

What if a customer texts “please stop my order”?

It opts them out: a text with the word STOP in it is an opt-out, so your typing call and reply return 403 recipient_opted_out, and the handler above passes the text to a person. Handle the cancellation by email; the customer can text START to opt back in.

Can customers send a video or several photos of the damage?

Ask for photos, one per text. A video arrives as a message with no file, and of several photos sent together only the first is delivered.

Can I text customers first when an order is delayed?

Only customers who gave you their number for order updates, and the first text should say who you are. Anyone who hasn’t texted you before counts as a new contact (5 a day per number on Pro, 50 on Growth), and a link in that first text isn’t tappable until they reply, so ask for a reply before sending tracking.

Will customers see the store’s name or just a number?

The store’s name and photo, once you set them: ZeroSettle shares them automatically with your first reply in each conversation, and the customer can save them. The exact display depends on their contact settings.

Sources

  1. ZeroSettle iMessage API docs: “Send a message” (name and photo), “Attachments”, “Typing indicators”, “Customer support”, “AI agent”, “Webhooks, step by step” and “Errors, limits, and compliance”. Checked October 12, 2026.
  2. Order object, GraphQL Admin API, Shopify: the name, phone, cancelledAt and fulfillments fields, the read_orders scope, and 60-day default access without read_all_orders. Checked October 8, 2026.
  3. orders query, GraphQL Admin API, Shopify: the name search filter and the 2026-10 endpoint. Checked October 8, 2026.
  4. Fulfillment object, GraphQL Admin API, Shopify: estimatedDeliveryAt and trackingInfo. Checked October 8, 2026.
  5. FulfillmentTrackingInfo object, GraphQL Admin API, Shopify: the tracking company, number and url. Checked October 8, 2026.
  6. Notification variables reference, Shopify Help Center: order_name, typically a pound sign followed by the order number. Checked October 8, 2026.
  7. url_encode filter, Shopify Liquid reference: URL-unsafe characters are percent-encoded and spaces become +. Checked October 8, 2026.
  8. What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support: iMessage goes to another iPhone or Apple product; other phones get RCS or SMS/MMS. Checked October 8, 2026.

Changelog

  • Updated for current limits (Pro: 5 new contacts a day; two unanswered messages on every plan) and the hosted agent's files and MCP connectors.
  • First published.

Written by the ZeroSettle team. ZeroSettle is an iMessage API: a real phone number on real iMessage, running on Apple hardware we operate for you, that your code texts customers from and hears back on through a bearer token and signed webhooks.

Product facts on this page come from the ZeroSettle iMessage API docs and zerosettle.io as of October 12, 2026. Fernwood Home is a fictional store, and this page is not legal advice.

Text your customers from your own iMessage number

ZeroSettle gives your business its own phone number on real iMessage, running on Apple hardware we operate for you in a server facility. Your code sends with one API call and hears every reply on a signed webhook.

Pro is $49 a month with a 30-day money-back guarantee. Growth, for several numbers working as one, is from $599/mo.