Use cases

Back-in-stock alerts over iMessage for sold-out sizes

ZeroSettle lets an apparel or limited-run brand put a "Text me when it's back" button next to each sold-out size: the fan texts your iMessage number, and when that size is restocked your server texts them a photo and the product link. Because the fan texts first, the request never counts as a new contact and the link in your alert is tappable. One number sends one message at a time, normally 2–5 seconds each, so 300 back-in-stock alerts take about 10–25 minutes: this fits the hundreds of people who asked, not a 100,000-person list.

Key takeaways

  • What it is: a restock waitlist that lives in Messages. Fans text NOTIFY plus a product and size; your server stores the request from ZeroSettle's signed webhook and texts each requester a captioned photo when the size returns.
  • Who it fits: apparel, sneaker, small-batch food and limited-run brands with a few hundred requests per restock, from customers who mostly use iPhone.
  • Send time: one message at a time per number, normally 2–5 seconds each, so 300 alerts take about 10–25 minutes. Fan-started requests never use the new-contact limit.
  • Price: Pro is $49/month, starting with one number. Growth, for several numbers working as one, is from $599/mo. No per-message fees.
  • What it does not do: there is no SMS fallback for Android, and ZeroSettle does not watch your inventory or schedule sends. Your code triggers every alert.

Who uses iMessage for back-in-stock alerts?

Back-in-stock alerts on ZeroSettle fit brands whose best sellers sell out by size: apparel and sneaker labels, small-batch food makers and limited runs, with a few hundred requests per restock from fans who mostly use iPhone. 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.

The problem is reach at the moment of the restock. Gmail's Default inbox type sorts mail into categories automatically, putting "Deals, offers, and other promotional emails" in a separate Promotions tab, so a back-in-stock email can sit unseen while the size sells out again. A ZeroSettle alert lands in Messages, in a thread the fan started. Restock alerts are one of the commerce workflows in our list of iMessage use cases.

How iMessage back-in-stock alerts work, step by step

The fan starts the thread from your product page, your webhook stores the request, and your inventory system triggers the alerts. ZeroSettle carries the messages; the waitlist and stock logic stay in your code.

  1. Next to each sold-out size, add a "Text me when it's back" button: an sms: link to your number prefilled with NOTIFY linen-set sage M (see how to get customers to text you first).
  2. The fan taps Send. ZeroSettle posts a signed message.received event to your webhook about 1–2 seconds later. Your server stores the handle, product, size and time, and replies with a confirmation that names your brand.
  3. When the size is back, your store's inventory webhook fires and your server loads every open request for it, oldest first.
  4. For each requester, one at a time, your server calls POST /api/imessage/send with the product photo (image_b64) and a caption carrying the link.
  5. A fan who replies HOLD gets the size reserved for 30 minutes. That is your store's logic, not a ZeroSettle feature.

Because every requester texted first, the link in the caption is tappable. The same link in a cold first text would show as plain text; see why links in a first iMessage aren't tappable.

Example conversation: one sold-out size

This thread shows one fan's request, the restock alert two weeks later and a HOLD, all in the thread the fan started.

  1. Tue 7:41 PM
  2. Fan: NOTIFY linen-set sage M
  3. Brand: Maren Studio here. You're on the list for the Linen Set in Sage, size M. We'll text you here as soon as it's back.
  4. Two weeks later, Thu 4:00 PM
  5. Brand: Photo: the Linen Set in Sage
    It's back! Linen Set, Sage, M: maren.example.com/p/linen-sage
  6. Fan: HOLD
  7. Brand: Held for you until 4:30 PM 🙌❤️
Illustrative example with a fictional brand. Blue bubbles are sent from Maren Studio's number through the API; gray bubbles are the fan's texts. The photo and caption arrive as one bubble.

Setup: NOTIFY requests stored from the webhook, sent on restock

The setup is a link on each product page plus one small server that stores NOTIFY requests and runs the restock loop. The signature check is copied from the webhook steps in the ZeroSettle docs.

html
<a href="sms:+18055550100&body=NOTIFY%20linen-set%20sage%20M">Text me when it's back</a>

On a fan's iPhone the link opens Messages with the text filled in. On Android it opens the SMS app, so keep an email sign-up next to the button.

python
# restock.py: run with  uvicorn restock:app --port 8100
import base64, hashlib, hmac, json, os, sqlite3, time
import requests
from fastapi import BackgroundTasks, FastAPI, Request, Response

BASE = os.environ.get("ZEROSETTLE_BASE_URL", "https://portal.zerosettle.io")
TOKEN = os.environ["ZEROSETTLE_TOKEN"]
SECRET = os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
PRODUCTS = {"linen-set sage m": ("Linen Set, Sage, M", "https://maren.example.com/p/linen-sage", "linen-sage.jpg")}
db = sqlite3.connect("waitlist.db", check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS waitlist (handle, sku, msg_id, asked_at, alerted_at, PRIMARY KEY (handle, sku))")
app, seen = FastAPI(), set()

def verify(secret, header, raw_body, tolerance=300):
    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)

def send(body):
    while True:                                # the line sends one at a time
        r = requests.post(f"{BASE}/api/imessage/send", json=body,
                          headers={"Authorization": f"Bearer {TOKEN}"}, timeout=120)
        if r.status_code != 429 or r.json()["error"]["code"] != "line_busy":
            return r
        time.sleep(2)                          # line_busy: retry after 2 s

def on_text(msg):
    cmd, _, sku = (msg.get("text") or "").strip().partition(" ")
    sku = sku.lower()
    if cmd.upper() == "NOTIFY" and sku in PRODUCTS:
        # the opt-in record; a NOTIFY after an earlier alert reopens the request
        db.execute("INSERT INTO waitlist VALUES (?, ?, ?, ?, NULL)"
                   " ON CONFLICT (handle, sku) DO UPDATE SET msg_id = excluded.msg_id,"
                   " asked_at = excluded.asked_at, alerted_at = NULL WHERE alerted_at IS NOT NULL",
                   (msg["handle"], sku, msg["id"], time.time()))
        db.commit()
        send({"to": msg["handle"], "text": f"Maren Studio here. You're on the list for the "
              f"{PRODUCTS[sku][0]}. We'll text you here as soon as it's back."})
    elif cmd.upper() == "HOLD":
        row = db.execute("SELECT sku FROM waitlist WHERE handle = ? AND alerted_at IS NOT NULL"
                         " ORDER BY alerted_at DESC", (msg["handle"],)).fetchone()
        if row:                                # reserve a unit of row[0] here, or say it's gone
            until = time.strftime("%-I:%M %p", time.localtime(time.time() + 30 * 60))
            send({"to": msg["handle"], "text": f"Held for you until {until} \U0001F64C"})

def restock(sku):                              # call from your inventory webhook, in a background job
    name, url, path = PRODUCTS[sku]
    with open(path, "rb") as f:                # encode the photo once, not per fan
        photo = base64.b64encode(f.read()).decode()
    rows = db.execute("SELECT handle FROM waitlist WHERE sku = ? AND alerted_at IS NULL"
                      " ORDER BY asked_at", (sku,)).fetchall()       # oldest first
    for (handle,) in rows:
        r = send({"to": handle, "image_b64": photo, "image_format": "jpg",
                  "text": f"It's back! {name}: {url}"})
        if r.ok or r.json()["error"]["code"] == "recipient_opted_out":
            db.execute("UPDATE waitlist SET alerted_at = ? WHERE handle = ? AND sku = ?",
                       (time.time(), handle, sku))
            db.commit()
        # any other error leaves the request open; run restock(sku) again to retry it

@app.post("/webhook")
async def webhook(request: Request, tasks: BackgroundTasks):
    raw = await request.body()
    if not verify(SECRET, request.headers.get("X-ZeroSettle-Signature", ""), raw):
        return Response(status_code=401)
    payload = json.loads(raw)
    msg = payload.get("message") or {}
    if payload["event"] == "message.received" and msg["id"] not in seen:   # retries repeat the id
        seen.add(msg["id"])
        tasks.add_task(on_text, msg)
    return Response(status_code=200)

Register the receiver with PUT /api/imessage/webhook and store the signing secret it returns, which is shown only once. Send the product photo as a JPEG 1600–2048 px on its long edge: ZeroSettle sends a JPEG up to 2048 px exactly as you give it, re-encodes anything larger, and caps photos at 5 MB.

Limits and consent for back-in-stock alerts

Back-in-stock alerts never touch ZeroSettle's new-contact limit, because every requester texted the line first. The limit that matters is send time: one number sends one message at a time, normally 2–5 seconds each.

Time to alert everyone who asked, at 2–5 seconds per send
Requests for the sizeOne numberThree Growth numbers
100about 3–8 minutesabout 1–3 minutes
300about 10–25 minutesabout 3–8 minutes
1,000about 33–83 minutesabout 11–28 minutes
10,000about 6–14 hoursabout 2–5 hours

Our arithmetic from ZeroSettle's docs: a send takes "normally 2–5 seconds and at most about 75 for a text (about 105 for a voice message or photo, which ZeroSettle converts first)". The Growth column assumes requests spread evenly across three numbers, with one loop per number running at the same time.

  • Don't send in parallel on one number. Concurrent sends queue and come back as 429 line_busy after about 12 seconds; retry after 2 seconds.
  • Spread requests across Growth numbers as fans text in. A contact is always messaged from the line their conversation is on, so show different visitors different numbers on the button and requests for the same size land on every line. The code above is for one number; on Growth, also store the line_id each message.received carries and run one restock loop per line at the same time.
  • Stop at two unanswered texts. The confirmation and the alert are two. A third, such as a "last few left" nudge, is refused with 403 awaiting_reply until the fan sends anything back. This cap applies on every plan.

A fan's NOTIFY text is reasonably treated as their opt-in for that product and size, and the stored message is your record of it. It is generally not consent to marketing. This isn't legal advice; check your own situation with counsel.

  • Ask before adding requesters to launch announcements. ZeroSettle's docs note that US marketing texts need written consent under the TCPA (the FCC's rules are in 47 CFR § 64.1200), with statutory damages of $500 to $1,500 per message for texting without prior express consent.
  • STOP is enforced on every number. A text with the word STOP in it, or a clear request like "take me off your list", opts the fan out of every number on your account, and the alert to them returns 403 recipient_opted_out with nothing sent.
  • Quiet hours. The docs ask that nothing promotional go out before 8 am or after 9 pm in the recipient's time zone. If stock goes live at night, have your restock job hold the alerts until morning.

When ZeroSettle is not a fit for back-in-stock alerts

ZeroSettle is the wrong tool for restock alerts in these cases:

  • Your restock list runs to tens of thousands. At one message at a time, 10,000 alerts take about 6–14 hours on one number. Use email, push or an SMS platform.
  • You'd add NOTIFY requesters to launch announcements without asking. ZeroSettle's terms prohibit high-volume marketing and unsolicited messaging, and ZeroSettle suspends lines used for unsolicited messaging.
  • Many fans use Android. iMessage goes to iPhone and other Apple products, and ZeroSettle has no SMS fallback, so Android fans can't get the alert.
  • You need every alert at one fixed time. ZeroSettle does not schedule sends, and with 300 requesters the last fan hears 10–25 minutes after the first.
  • You need proof each fan saw the alert. The API reports message.sent and message.failed, not read status; see iMessage delivery status, webhook retries and latency.

How much does iMessage for back-in-stock alerts cost?

Back-in-stock alerts cost the plan price and nothing per message: ZeroSettle's outbound and inbound messages are unlimited (counted).

ZeroSettle plans for back-in-stock alerts (zerosettle.io, checked October 8, 2026)
PlanPriceFits
Pro$49/monthOne number to start, self-serve, no setup fee, 5 new contacts a day per number, unlimited messages with people who message you first, 30-day money-back guarantee
GrowthFrom $599/mo3+ numbers working as one, dedicated support, set up after a demo

A brand alerting a few hundred fans per restock fits Pro; Growth fits when one restock draws more requests than one number reaches before the stock runs out. See the ZeroSettle pricing table or get a number. For selling in the same thread, see text-to-buy over iMessage and retail clienteling from store-owned numbers.

Frequently asked questions

Can a fan ask for more than one size or product?

Yes. Each NOTIFY text is its own request, stored per product and size. Note, though, that a fan who has had a confirmation and one alert since their last text can't get a second alert until they reply (403 awaiting_reply), so the example code leaves that request open for a later restock() run.

What if more fans reply HOLD than units came back?

Your store decides: hold units in the order HOLD replies arrive and tell the rest the size is gone. Because alerts go out oldest request first, the earliest requesters see the restock first.

Can the hosted AI agent handle NOTIFY and HOLD texts instead of my server?

Not for this flow. On a line group Managed by AI Agent (Beta), inbound texts are not posted to your webhook and your own API sends are refused with 403 managed_by_zerosettle.

What happens if an alert fails to send?

The send call returns an error code. A send the line couldn't complete, such as line_busy or a 503, is stored with status failed, while a refusal such as recipient_opted_out or awaiting_reply sends and stores nothing. Retry line_busy after 2 seconds and a 503 with backoff; never retry recipient_opted_out.

Sources

  1. ZeroSettle iMessage API docs: "Send a message", "Attachments", "Customer support", "Webhooks, step by step" and "Errors, limits, and compliance". Checked October 8, 2026.
  2. ZeroSettle iMessage API Terms of Service, section 13. Last modified September 28, 2026.
  3. Organize your emails into categories, Gmail Help (Google). Checked October 8, 2026.
  4. What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support. Checked October 8, 2026.
  5. 47 CFR § 64.1200, Delivery restrictions, the FCC's TCPA rules, Legal Information Institute (Cornell Law School). Checked October 8, 2026.

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 iMessage API Terms as of October 8, 2026. Outside facts come from the pages listed under Sources, checked the same day. Maren Studio is a fictional brand. 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.