Let subscribers skip, swap or reorder by replying on iMessage
ZeroSettle lets a coffee, pet-food or supplement brand text opted-in subscribers a link-free question a few days before each renewal ("Your box ships Thursday: keep, skip or swap?") and act on the reply through its subscription platform's API. One-time buyers who opted in can get a "running low?" nudge and reorder with a YES. Repeat customers are ongoing conversations, so only the first text you send each customer counts against ZeroSettle's daily new-contact allowance: 5 a day per number on the $49-a-month Pro plan, and 50 on Growth.
Key takeaways
- What it is: a renewal question sent from your brand's own iMessage number through ZeroSettle's REST API. Your webhook turns KEEP, SKIP, SWAP or a new date into a call to your subscription platform.
- Who it fits: coffee, pet-food, supplement, skincare and meal-kit brands whose subscription platform has an API to skip a cycle or move the next date.
- Main limit: 5 new contacts a day per number on Pro (50 on Growth). Only the first text you send a subscriber counts; every later renewal question is an ongoing conversation.
- Price: Pro is $49/month, starting with one number, with unlimited messages with people who message you first. Growth, for several numbers working as one, is from $599/mo. No per-message fees.
- What it does not do: reach subscribers on Android (there is no SMS fallback), or replace your online cancel button.
Who uses iMessage for subscription skips and reorders?
iMessage renewal and reorder texts fit brands that sell something customers use up: coffee, pet food, supplements, skincare and meal kits, whether customers subscribe or buy once. 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 timing: a subscriber with a full bag doesn't dislike the coffee, but when skipping means a portal login, cancelling can look simpler. One-time buyers who run out may buy elsewhere.
It doesn't suit brands that would use the thread to slow cancellations, or whose subscribers mostly use Android (see when ZeroSettle is not a fit). For app subscriptions, see trial-ending and renewal reminders; more jobs are on the ZeroSettle use-case hub.
How iMessage for subscription skips and reorders works, step by step
A daily job asks each opted-in subscriber a link-free question before the charge, and your webhook turns the reply into a call to your subscription platform.
- Opt-in at checkout: one checkbox for renewal texts, a separate one for running-low reminders. Keep the record.
- Ask: three days before each renewal, the job calls
POST /api/imessage/sendwith the product and ship day. The question carries no link, because iPhone shows a link from a number the subscriber has never texted as plain text they can't tap (why first-message links aren't tappable). - Act: the reply reaches your webhook as a signed
message.receivedevent about 1–2 seconds after it lands on the line. The parser maps KEEP, SKIP, SWAP, a new frequency or a push of a few weeks to your platform; on Shopify,subscriptionBillingCycleSkipskips a cycle andsubscriptionContractSetNextBillingDatemoves the date. - Confirm: a 👍 tapback on their message (
POST /api/imessage/reactionswith"like"), then the new ship date. Replies the parser can't read go to a person, who answers from the ZeroSettle console. - Reorder: buyers who opted in get a running-low nudge timed from their order (a 30-day bottle around day 24). A YES charges the card they saved and authorized for one-tap reorders; a 👍 confirms it.
- Cancel: a bare CANCEL cancels the subscription with no save offer. A text with the word STOP is different: ZeroSettle treats it as an opt-out from all texts from your numbers, not a cancellation, so a person checks whether the subscriber also wants to cancel (see "stop" and "cancel" replies). A subscriber who asks about cancelling gets at most one pause or smaller-size offer and is cancelled right away if they still want out. The chat is never a gate.
Example conversation: subscription skips and reorders
Jess's first answer matches no keyword, so it goes to a person, who answers from the console with two choices the parser understands.
- Mon 10:00 AM
- Business: Hi Jess, it's Ember Coffee. Your next bag (Guatemala Huehue, 12oz) ships Thursday. Keep it, skip, or swap?
- Customer: honestly I still have a full bag
- Business: Totally get it. Want me to push it 2 weeks, or switch you to every 3 weeks?
- Customer: every 3 weeks👍
- Business: Done. Next one ships Thursday, Oct 29.
Each option is one platform call. A swap to a sold-out roast can become a back-in-stock alert over iMessage.
Setup: a renewal job plus a reply parser
One daily job sends the question; one receiver verifies ZeroSettle's signature (copied from the docs' webhooks, step by step), parses the reply, calls your platform and confirms in the thread.
# renewals.py: run with uvicorn renewals:app --port 8100
# subscription_for, platform, swap, to_person and email_notice are your own code.
import hashlib, hmac, json, os, re, time, requests
from fastapi import BackgroundTasks, FastAPI, Request, Response
BASE = "https://portal.zerosettle.io"
AUTH = {"Authorization": f"Bearer {os.environ['ZEROSETTLE_TOKEN']}"}
SECRET = os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
app, seen = FastAPI(), set() # use your database in production
def zs(path, body):
return requests.post(BASE + path, json=body, headers=AUTH, timeout=90)
def ask(sub): # daily job: 3 days before each renewal
body = {"to": sub.phone, "text": f"Hi {sub.first_name}, it's Ember Coffee. "
f"Your next bag ({sub.product}) ships {sub.ship_day}. Keep it, skip, or swap?"}
while (r := zs("/api/imessage/send", body)).status_code == 429:
err = r.json()["error"]
if err.get("window") == "day":
return email_notice(sub) # today's new contacts are used up
time.sleep(err.get("retry_after_s", 2)) # Growth's 15-minute window, or line_busy
if r.status_code == 403: # recipient_opted_out, or awaiting_reply
email_notice(sub)
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)
def handle(msg):
sub = subscription_for(msg["handle"])
text = " ".join(re.sub(r"[^\w\s]", " ", (msg.get("text") or "").lower()).split())
if sub is None or re.search(r"\b(stop|cancel|end|quit|revoke|unsubscribe|opt ?out)\b", text):
return to_person(msg) # opt-outs and cancels: see below
if text in ("keep", "keep it"):
when = sub.ship_day
elif text in ("skip", "skip it"):
when = platform.skip_next(sub) # subscriptionBillingCycleSkip
elif m := re.search(r"every (\d+) weeks?", text):
when = platform.set_interval(sub, int(m[1]))
elif m := re.search(r"(\d+) weeks?", text):
when = platform.push(sub, int(m[1])) # subscriptionContractSetNextBillingDate
elif text == "swap" or text in sub.swap_options:
return swap(sub, msg) # send the menu, or apply the pick
else:
return to_person(msg) # "I still have a full bag"
zs("/api/imessage/reactions", {"message_id": msg["id"], "reaction": "like"})
zs("/api/imessage/send", {"to": msg["handle"], "text": f"Done. Next one ships {when}."})
@app.post("/webhook")
async def webhook(request: Request, tasks: BackgroundTasks):
raw = await request.body()
if not verify(request.headers.get("X-ZeroSettle-Signature", ""), raw):
return Response(status_code=401)
payload = json.loads(raw)
if payload["event"] != "message.received" or payload["message"]["id"] in seen:
return Response(status_code=200) # own sends echo as message.sent
seen.add(payload["message"]["id"])
tasks.add_task(handle, payload["message"])
return Response(status_code=200)
The receiver answers 2xx at once and does the work afterwards, because ZeroSettle allows a webhook 10 seconds to respond and retries after 1, 5 and 20 seconds, while a platform call plus two sends can take longer (see how to receive iMessages with a webhook). A running-low YES needs one more branch: look up the buyer and charge the card they saved for one-tap reorders, which Stripe's docs say needs consent collected "for this specific use". Declines belong to failed-payment recovery over iMessage.
Limits and consent for subscription skips and reorders
Only the first text you send a subscriber counts as a new contact; later renewal questions are an ongoing conversation, which the new-contact limit never counts. So Pro's 5 a day caps how fast subscribers move onto texts, not how many renewals you ask about.
| Limit | Pro | Growth |
|---|---|---|
| New contacts per number per day (3 AM to 3 AM Eastern) | 5 | 50 |
| New contacts per number per rolling 15 minutes | — | 3 |
| Unanswered messages to one subscriber | 2, then 403 awaiting_reply | 2, then 403 awaiting_reply |
| Outbound and inbound messages | Unlimited (counted) | Unlimited (counted) |
Source: ZeroSettle iMessage API docs, errors, limits, and compliance.
The limit math for 400 subscribers
Ember has 400 opted-in subscribers on a two-week cadence, about 29 renewals a day.
- On Pro: 5 a day get a first text; the other 24 get email (the job's
email_noticebranch). At 5 a day, moving all 400 onto texts takes about 80 days, roughly eleven to twelve weeks; after that, all 29 a day are ongoing conversations. - On Growth: 29 first texts fit in one day's 50, but at 3 per 15 minutes they take just over two hours on one number.
Running-low nudges to one-time buyers are first texts too, so on Pro they share the same 5 a day as new subscribers. A list this size is a reason to book a demo for Growth.
"Stop" and "cancel" replies
Any text with the word stop in it, or a clear request like "take me off your list", opts the sender out of every number on your account: later sends, tapbacks included, return 403 recipient_opted_out, and only START opts them back in. A plain STOP is an opt-out from texts, not a cancellation, so the subscription stays as it is unless they ask to cancel. "Stop my subscription" is both: process the cancellation, confirm it by email, and text again only after they text START (how STOP works).
"Cancel" is not on ZeroSettle's opt-out list, but as we read the FCC's TCPA rule, it names "cancel" and "end", along with "stop", "quit", "revoke", "opt out" and "unsubscribe", as replies to a text that generally revoke consent to further texts, and it allows one confirmation text with no marketing in it. So a bare CANCEL gets the subscription cancelled and one plain confirmation, with no save offer. ZeroSettle won't block later sends to that number, so mark it opted out in your own records and stop texting it. This isn't legal advice.
The thread can't be the only way to cancel
The federal Restore Online Shoppers' Confidence Act requires "simple mechanisms for a consumer to stop recurring charges". California lets a consumer who signed up online cancel online, at will, and allows a retention offer during online cancellation only alongside a "click to cancel" button. The thread is an extra way out, never the required one.
Renewal questions are generally treated as transactional; a save offer is likely marketing. Send one only to subscribers who gave marketing consent, only once, and only between 8 am and 9 pm their time. This isn't legal advice.
When ZeroSettle is not a fit for subscription skips and reorders
ZeroSettle is the wrong tool when the thread would stand between a subscriber and the exit, or when nothing can act on a reply.
- You'd make cancellation require a chat. Federal law requires a simple way to stop recurring charges, and California requires online cancellation for anyone who signed up online.
- Your subscription platform has no API to skip, swap or move a date, so every reply waits for a manual change.
- Many subscribers use Android. There is no SMS fallback; keep email and your account portal for them.
- You'd charge a card on a YES without prior authorization for one-tap reorders. Send a checkout link after the reply instead.
How much does iMessage for subscription skips and reorders cost?
ZeroSettle charges a plan price and nothing per message; outbound and inbound messages are unlimited (and counted).
| Plan | Price | Fits |
|---|---|---|
| Pro | $49/month | One number to start, first texts to 5 new contacts a day, unlimited messages with people who message you first, self-serve, no setup fee |
| Growth | From $599/mo | Several numbers working as one, 50 new contacts a day per number, set up after a demo |
Both include a 30-day money-back guarantee. See ZeroSettle pricing, or get started with Pro.
Frequently asked questions
What if a subscriber never answers the renewal question?
Treat silence as KEEP, and stop asking after two silent cycles. ZeroSettle refuses a third unanswered message with 403 awaiting_reply on every plan, and its docs warn that repeated messages to people who never answer are the fastest way to get a number flagged.
Can the hosted AI agent handle skips instead of my server?
Not on the same number. On a line group Managed by AI Agent, inbound texts are not posted to your webhook and your API sends are refused with 403 managed_by_zerosettle, so nothing would call your subscription platform.
Is a running-low reminder a marketing text?
Treat it as one: it asks for a new purchase, so it needs its own opt-in, and in the US marketing texts need prior express written consent. See iMessage marketing and TCPA consent. This isn't legal advice.
Sources
- ZeroSettle iMessage API docs: "Send a message", "Reactions (tapbacks)", "Webhooks, step by step", "AI agent" and "Errors, limits, and compliance". Checked October 12, 2026.
- subscriptionBillingCycleSkip, Shopify GraphQL Admin API reference. Checked October 8, 2026.
- subscriptionContractSetNextBillingDate, Shopify GraphQL Admin API reference. Checked October 8, 2026.
- Save a customer's payment method without making a payment, Stripe Docs, "Compliance". Checked October 8, 2026.
- 47 CFR § 64.1200, Delivery restrictions, paragraphs (a)(10), (a)(12) and (f)(9), the FCC's TCPA rule (Legal Information Institute, Cornell Law School). Checked October 8, 2026.
- 15 U.S.C. § 8403, Negative option marketing on the Internet (Restore Online Shoppers' Confidence Act), Legal Information Institute. Checked October 8, 2026.
- California Business and Professions Code § 17602, subdivisions (d)(1) and (e)(2), statute text via Public.Law. Checked October 8, 2026.