How to fill last-minute cancellations from a waitlist over iMessage
ZeroSettle lets a salon, studio or trainer offer a cancelled slot to waitlisted clients one at a time over iMessage, from the business's own number, and book the first client who replies YES. Clients join by texting WAITLIST to your number, so offers are ongoing conversations that never count against Pro's 5 new contacts a day. Your booking system runs the queue; ZeroSettle carries the texts and delivers each reply to your webhook in about 1–2 seconds.
Key takeaways
- What it is: 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 workflow: on a cancellation, your code texts the best-matched waitlisted client, waits ten minutes, then asks the next one. The first YES books the slot.
- Main limit: on every plan, ZeroSettle refuses a third unanswered message to one client, and the join confirmation counts, so a client who never replied gets one offer.
- Price: Pro is $49/month, starting with one number; Growth is from $599/mo for several numbers working as one. No per-message fees.
- What it does not do: no SMS fallback for Android clients, no group texts, and no built-in queue. Your code decides who is asked and when.
Who uses iMessage for last-minute cancellations?
iMessage waitlist offers fit salons, barbers, tattoo studios, massage therapists, lash and brow artists and personal trainers who keep a list of clients wanting an earlier slot, because for them one cancellation is one empty chair or hour.
The cancellation that lands the night before is the problem. An Instagram story reaches followers rather than the clients who asked, phoning the list ties up the front desk, and a group text shows every client's number to everyone in it. A one-by-one offer reaches only the client who fits, from the number that can also send your iMessage appointment reminders. A restaurant's walk-in list is a different job (restaurant waitlists by iMessage); others are in the index of iMessage use cases.
How iMessage waitlist offers work, step by step
Your booking system spots the cancellation and picks who to ask; ZeroSettle sends each offer from your number and posts each reply to your webhook.
- Clients text WAITLIST to your number from an
sms:link, which opens Messages, or its QR code on the booking page (how to get customers to text you first). Your webhook stores the handle and replies with a confirmation that asks which stylist and times suit them. A "text me if something earlier opens" checkbox works too, with one catch. - A cancellation starts the queue. Square's Bookings API, for example, publishes
booking.updated"when a booking is updated or cancelled". Your handler ranks the waitlist by staff, service and time. - The best match gets the offer through
POST https://portal.zerosettle.io/api/imessage/send, which normally returns in 2–5 seconds. - After ten minutes with no YES, the next client is asked.
- The first YES wins. It reaches your webhook as
message.receivedin about 1–2 seconds; your handler claims the slot, books it and confirms. A later YES gets "it's taken, you're still on the list". - Clients who don't answer are skipped. On every plan, ZeroSettle refuses a third unanswered message to one client (
403 awaiting_reply), and the join confirmation counts as the first, so your loop moves on to the next client.
The same pattern fills open shifts by iMessage.
Example conversation: one cancelled 3:30
One cancelled 3:30 at a fictional studio: Priya, the best match, had the first ten minutes; Mei was next.
- Tue 6:12 PM
- Business: Hi Priya, it's Studio Ana! A 3:30 just opened with Ana tomorrow (Wed). Want it? Reply YES and it's yours.
- Tue 7:40 PM
- Client: oh is the 3:30 still open?
- Business: Just taken, sorry! You're still first in line for the next one.
- Tue 6:22 PM
- Business: Hi Mei, it's Studio Ana! A 3:30 just opened with Ana tomorrow (Wed). Want it? Reply YES and it's yours.
- Client: YES
- Business: Booked! Balayage refresh, Wed 3:30 with Ana. See you then ✨❤️
message.received; her ❤️ tapback arrives as reaction.received.Setup: an offer queue that texts one client at a time
The code is an offer loop started by your booking system's cancellation webhook, plus a ZeroSettle receiver that verifies the signature, drops retries and books the first YES. The verifier is copied from the ZeroSettle webhook docs; the functions it calls but doesn't define, such as rank_waitlist and book, are yours to write against your booking system.
# waitlist.py: run with uvicorn waitlist:app --port 8100
import asyncio, hashlib, hmac, json, os, re, time
import httpx
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"]
app = FastAPI()
seen: set[tuple[int, str]] = set() # your database in production
offers: dict[str, dict] = {} # client handle -> slot offered
def verify(header, raw):
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) > 300:
return False
expected = hmac.new(SECRET.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
async def send(to, text):
async with httpx.AsyncClient(timeout=90) as c:
for _ in range(5):
r = await c.post(f"{BASE}/api/imessage/send", json={"to": to, "text": text},
headers={"Authorization": f"Bearer {TOKEN}"})
if r.status_code == 200:
return "ok"
code = r.json()["error"]["code"]
if code != "line_busy" and r.status_code != 503:
return code # awaiting_reply, recipient_opted_out, ...
await asyncio.sleep(2 if code == "line_busy" else 30) # 503: back off
return code
async def offer_slot(slot):
for c in rank_waitlist(slot): # your DB: best match first, skip rule applied
if slot["taken_by"]:
return
text = (f"Hi {c['name']}, it's Studio Ana! A {slot['when']} just opened. "
"Want it? Reply YES and it's yours.")
if await send(c["handle"], text) != "ok":
continue # e.g. awaiting_reply: next client
offers[c["handle"]] = slot
for _ in range(120): # ten minutes, checked every 5 s
if slot["taken_by"]:
return
await asyncio.sleep(5)
async def handle(m):
who, text = m["handle"], (m.get("text") or "").strip()
slot = offers.get(who)
if text.upper() == "WAITLIST":
join_waitlist(who) # your DB: the opt-in, timestamped
await send(who, "You're on the Studio Ana waitlist. Which stylist and times suit you?")
elif slot and re.match(r"yes\b", text, re.I):
del offers[who]
if slot["taken_by"]:
await send(who, "Just taken, sorry! You're still on the list for the next one.")
else:
slot["taken_by"] = who # production: UPDATE ... WHERE taken_by IS NULL
book(slot, who) # your booking system's API
await send(who, f"Booked! {slot['when']}. See you then.")
else:
notify_front_desk(m) # times, questions, STOP: a person reads it
@app.post("/booking-cancelled") # your booking system's webhook: verify it first
async def booking_cancelled(req: Request, tasks: BackgroundTasks):
slot = slot_from_booking(await req.json()) # your code: None unless cancelled
if slot:
tasks.add_task(offer_slot, slot)
return {"ok": True}
@app.post("/webhook")
async def webhook(req: Request, tasks: BackgroundTasks):
raw = await req.body()
if not verify(req.headers.get("X-ZeroSettle-Signature", ""), raw):
return Response(status_code=401)
ev = json.loads(raw); m = ev.get("message")
if ev["event"] != "message.received" or m is None or (m["id"], ev["event"]) in seen:
return {"ok": True} # echoes of your sends, or a retry
seen.add((m["id"], ev["event"]))
tasks.add_task(handle, m)
return {"ok": True}
Register the receiver with PUT /api/imessage/webhook and keep the signing secret, shown once. Retries, after 1, 5 and 20 seconds, repeat the same message.id and event, so dedupe before booking and make the slot claim atomic. Events can arrive out of order under retries; if strict first-come matters, order by message.id. More in how iMessage webhooks are signed and retried.
Limits and consent for waitlist offers
Offers to clients who joined by texting WAITLIST never touch ZeroSettle's new-contact limits, because anyone who texted the line first is never a new contact. What matters is the unanswered-message rule and the answer window you give each client.
| Limit | Pro | Growth | For waitlist offers |
|---|---|---|---|
| New contacts per day | 5 | 50 | Only clients who never texted you, once each |
| New contacts per rolling 15 minutes | — | 3 | Same |
| Unanswered messages to one client | 2, then 403 awaiting_reply | 2, then 403 awaiting_reply | The join confirmation counts; skip to the next client |
| Sends at a time | One per line | One per line | Queue them; retry line_busy after 2 s |
Source: ZeroSettle iMessage API docs, "Errors, limits, and compliance". A new contact is a handle the line delivers a text, voice message or photo to first.
The limit math for one cancellation
- One slot, five clients: ten-minute windows put the fifth offer 40 minutes after the first; 2–5 seconds per send barely registers.
- Two cancellations at once: sends run seconds apart, well under the ~12 seconds of waiting that returns
429 line_busy. - On every plan: the join confirmation is the first unanswered message, so a client who never replied to it gets one offer; the next returns
403 awaiting_replyuntil they send anything back, even a tapback. Ending the confirmation with a question invites that reply. Messages within 5 minutes of the first count as one. - Why the cap exists: ZeroSettle's docs warn that "repeated messages to people who never answer are the fastest way to get a number flagged by Apple". Skip a client who stays quiet.
Why the checkbox route is weaker
A client who only ticked a booking-form box has never texted your number, so their first offer counts as a new contact, must say who it's from, and comes from an unknown sender. With Screen Unknown Senders on, Apple says such messages "are filtered into another folder, and you don't get notified about them unless you want to be", so a ten-minute offer can expire unseen. Show the WAITLIST link on the booking confirmation too.
Consent and STOP
Treat the WAITLIST text or the ticked box as the client's opt-in: store it with a timestamp, name the business in the confirmation, and use the list only for real openings, never promotions. ZeroSettle's docs ask that nothing promotional go out before 8 am or after 9 pm in the recipient's time zone. Even when an offer isn't promotional, a ten-minute window at 11 pm is wasted, so hold overnight cancellations until morning. This isn't legal advice; consent rules vary by place and purpose.
A text with the word STOP in it, or a clear request like "take me off your list", opts the client out of every number on your account, and sends to them return 403 recipient_opted_out. "I could stop by at 4 instead" counts too, and that client can't get your answer, so have the front desk review every STOP your webhook receives. Only the client undoes it: by texting START (or unstop) as the whole message, or by asking you another way, in which case ZeroSettle can clear it. See how STOP works on a ZeroSettle line.
When ZeroSettle is not a fit for last-minute cancellations
ZeroSettle is the wrong tool for waitlist offers in four cases.
- You want to text the whole waitlist at once and let clients race. There are no group chats, so that is one text per client for every slot, and the unanswered ones put the number at risk (see the limit math).
- Many waitlisted clients use Android. Apple says iMessage goes to another iPhone or other Apple product, with RCS or SMS for the rest, and ZeroSettle has no SMS fallback. Keep another route onto the list, such as email.
- Your booking software already has a waitlist that your clients use and answer.
- You can't connect to your booking system's API: no developer and no Zapier, n8n or Make workflow to call ZeroSettle (see iMessage automations with Zapier, n8n or Make). The console can still send offers by hand.
How much do iMessage waitlist offers cost?
Waitlist offers cost the plan price and nothing per message: ZeroSettle's messages are unlimited (counted) on both plans.
| Plan | Price | Fits |
|---|---|---|
| Pro | $49/month | One number to start, 5 new contacts a day per number, unlimited messages with people who message you first, no setup fee, self-serve |
| Growth | From $599/mo | 3+ numbers working as one, 50 new contacts a day per number, after a demo |
Both plans bill monthly with a 30-day money-back guarantee on the subscription, and a queue your own code runs uses no AI credits. Compare ZeroSettle's plans, read the iMessage API docs, or get a number.
Frequently asked questions
How long should each client get to answer?
Ten minutes suits a next-day slot; shorten it for a same-day opening. Your handler sets the window, not ZeroSettle: in the code above it is the 120 five-second checks in offer_slot.
Can the offer include a booking link instead of asking for YES?
For clients who joined by texting WAITLIST, yes: once someone has texted your number, links you send them are tappable. In a first message to a checkbox client, a link shows as plain text they can't tap, so ask for YES there.
Will clients see the studio's name on the offer?
Usually. ZeroSettle shares your brand name and photo with the first message you send each client, so iOS can show "Maybe: Studio Ana" and your photo before they save your number. The exact display depends on their iPhone and contact settings.
Sources
- ZeroSettle iMessage API docs: "Send a message", "Customer support" (Let them start the conversation), "Webhooks, step by step" and "Errors, limits, and compliance". Checked October 8, 2026.
- booking.updated Webhook, Square API Reference. Checked October 8, 2026.
- SMS Links, Apple URL Scheme Reference, Apple Developer. Checked October 8, 2026.
- Screen and filter text messages on iPhone, iPhone User Guide, Apple Support. Checked October 8, 2026.
- What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support. Checked October 8, 2026.