Founder-led SaaS onboarding over iMessage
ZeroSettle lets an early-stage B2B SaaS text each new trial user who opts in one personal, link-free question ("What are you hoping to automate first?") from an iMessage number set up with the founder's name and photo. Replies reach the founder in the ZeroSettle console, and when product analytics show a user is stuck, such as "stalled at import for 24 hours", your backend sends one helpful nudge into the same thread at no extra new-contact cost. On the $49-a-month Pro plan, one number's 5 new contacts a day fits about 150 opted-in signups a month; Growth's 50 a day per number fits more.
Key takeaways
- What it is: a founder's welcome question and one stall-triggered nudge, sent by your backend through ZeroSettle's REST API from a number that shows the founder's name and photo.
- Who it fits: early-stage B2B SaaS and product-led tools where activation matters more than raw signups, and whose users carry iPhones.
- Main limit: 5 new contacts a day per number on Pro (50 on Growth). Only the welcome counts; nudges and replies in the thread do not.
- 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. There are no per-message fees.
- What it does not do: reach Android users (there is no SMS fallback), make a link tappable before the user replies or saves the number, or report whether a text was read.
Who uses iMessage for SaaS onboarding?
Founder-led onboarding over iMessage suits early-stage B2B SaaS and product-led tools where activation matters more than raw signups and the founder still talks to new users. 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.
It targets the trial that dies at step two: a user signs up, stalls at the import or the first integration, and quietly leaves. An onboarding email is easy to skip, and in-app chat only reaches them while they are in the app. Related jobs have their own pages: for the end of a trial, trial-ending reminders and a "text the founder" step in the cancel flow; for sales-led signups, an AI SDR for demo requests. More are on the use-case hub.
How iMessage for SaaS onboarding works, step by step
Your signup form collects an opt-in, your backend sends the founder's question through ZeroSettle, the founder answers from the console, and a stall in your analytics triggers at most one nudge in the same thread.
- Ask at signup. Add an optional phone field with an unchecked box: "Want a text if you get stuck? The founder may text you to help you set up." Store the wording and when it was ticked.
- Put the founder on the card. Set the line's name, such as "Nina from Tallyroom" (up to 64 characters), and a square JPEG or PNG of the founder under 3 MB with
PUT /api/imessage/profileor in the console. ZeroSettle shares the card with each first message; what the user sees depends on their device and contact settings. - Send one question with
POST /api/imessage/send, and no link: an iPhone shows a link from an unknown sender as plain text until the user replies or saves the number. See how to write a first iMessage people reply to. - The founder answers in the ZeroSettle console. Each reply also reaches your webhook as a signed
message.receivedevent about 1–2 seconds after it lands. - Nudge on a stall. When analytics show "import started 24 hours ago, never finished" or "no integration by day 3", send one nudge into the same thread. It is not a new contact.
- Show, don't link. Once the user has replied, answer with a screenshot (
image_b64, up to 5 MB, caption in the same bubble) or a 20-second voice note (audio_b64, up to 6 MB, arriving with a waveform and the transcript the user's phone generates) rather than a help-doc link. Photos and voice notes go only to users who have replied (until then ZeroSettle returns403 attachment_before_reply), and a settings link shows as plain text until then too, so a nudge to a silent user is a short text.
Or let the user text first
The welcome screen can offer your ZeroSettle number as an sms: link with "Hi Nina" prefilled. A user who texts first is never a new contact, links work at once, and the thread avoids iOS 26's Screen Unknown Senders list, which Apple turns on at upgrade for anyone who had Filter Unknown Senders on in iOS 18. On Android the link opens SMS, so keep email as the fallback (more on getting customers to text first).
Example conversation: SaaS onboarding
Here the welcome question gets a reply naming the blocker, and one screenshot from the founder clears it the same morning.
- Tue 10:12 AM
- Nina from Tallyroom: Hey Sam, Nina here, I built Tallyroom. Saw you signed up today. What are you hoping to automate first?
- Tue 10:40 AM
- Sam: syncing invoices from QuickBooks but the connect button errors
- Nina from Tallyroom: Ugh, sorry. That's usually the sandbox vs production toggle. Here's where it hides:
- Nina from Tallyroom: Photo: screenshot of Settings with the toggle circled
- Sam: that worked!! 🙌
Setup: a signup opt-in and analytics-triggered nudges
One small service covers it: welcome() for your signup handler, nudge() for your stall job, and a webhook that verifies ZeroSettle's signature and records who replied. It follows the docs on sending and webhooks.
# onboarding.py: run with uvicorn onboarding:app --port 8100
import base64, hashlib, hmac, json, os, time
import requests
from fastapi import FastAPI, Request, Response
BASE = os.environ.get("ZEROSETTLE_BASE_URL", "https://portal.zerosettle.io")
TOKEN, SECRET = os.environ["ZEROSETTLE_TOKEN"], os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
app = FastAPI()
# Yours to write. Store user.phone in E.164 (+1...) so it matches the webhook's handle.
def queue_welcome(user, after_s: int) -> None: ... # welcome(user, queued=True), never at night
def mark_replied(handle: str) -> None: ... # set replied_at for that phone; idempotent
def send(to: str, **fields) -> dict:
return requests.post(f"{BASE}/api/imessage/send", json={"to": to, **fields},
headers={"Authorization": f"Bearer {TOKEN}"}, timeout=120).json()
# Run from one background worker after signup, only if the box was ticked:
# each send waits until it is handed to Apple, and sends go one at a time per line.
def welcome(user, queued: bool = False) -> None:
if queued and user.activated: # they got going without help
return
when = "recently" if queued else "today"
r = send(user.phone, text=f"Hey {user.first_name}, Nina here, I built Tallyroom. "
f"Saw you signed up {when}. What are you hoping to automate first?")
err = r.get("error") or {}
if err.get("code") == "new_contact_limit": # the day resets at 3 AM Eastern
queue_welcome(user, after_s=err["retry_after_s"])
def nudge(user) -> None: # your stall job calls this once per user
caption = "Nina again. Imports usually stall on the sandbox toggle: set it to Production and retry."
if not user.replied_at: # photos and links wait for a reply
send(user.phone, text=caption)
return
caption += " https://app.tallyroom.example/settings/integrations"
with open("shots/import-toggle.png", "rb") as f:
png = base64.b64encode(f.read()).decode()
send(user.phone, image_b64=png, image_format="png", text=caption)
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)
@app.post("/webhook")
async def webhook(request: Request):
raw = await request.body()
if not verify(SECRET, request.headers.get("X-ZeroSettle-Signature", ""), raw):
return Response(status_code=401)
payload = json.loads(raw)
if payload["event"] == "message.received": # your own sends echo back as message.sent
mark_replied(payload["message"]["handle"]) # retries repeat it harmlessly
return Response(status_code=200)
Register the receiver with PUT /api/imessage/webhook and store the secret; it is shown once. A voice note sends audio_b64 (m4a, mp3, wav and more) with no text. Keep screenshots within 2048 px on the long edge: a PNG or JPEG that size goes out exactly as sent, while anything larger is re-encoded to JPEG (which also strips EXIF and GPS).
Limits and consent for SaaS onboarding
Each opted-in signup uses one new contact, for the founder's question; nudges and replies in that thread use none. On Pro that means 5 welcomes a day per number, about 150 a month at an even pace.
| Limit | Pro | Growth |
|---|---|---|
| Welcomes (new contacts) per day, 3 AM to 3 AM Eastern | 5 | 50 |
| Welcomes per rolling 15 minutes | — | 3 |
| Unanswered messages to one user | 2, then 403 awaiting_reply | 2, then 403 awaiting_reply |
| Photos or voice notes before the user replies | Refused (403 attachment_before_reply) | Refused |
| New contacts used by nudges and replies in an existing thread | 0 | 0 |
Source: ZeroSettle iMessage API docs, "Errors, limits, and compliance". Anyone who texted the line first is never limited.
Launch-day math
Suppose launch day brings 200 signups and 120 tick the box. On Pro, 5 welcomes go out that day; the other 115 sends return 429 new_contact_limit with retry_after_s, and at 5 a day the last would wait more than three weeks. Re-check activation before each queued welcome, drop "today" from the wording, and hold any that would land at night, since the day resets at 3 AM Eastern. For a user facing a wait of several days, your onboarding email is the better first touch, and a launch this size is a reason to book a demo for Growth.
On Growth with three numbers (150 a day, 9 per 15 minutes), all 120 go out in about three and a quarter hours. But a silent user's welcome plus one nudge a day or more later uses the 2 unanswered messages every plan allows (only messages within 5 minutes of each other count as one), so a second nudge returns 403 awaiting_reply.
Consent comes first
ZeroSettle's terms require explicit opt-in, and its docs spell it out: the user gave their number for this purpose and you can show the record, and the first text says who you are. The docs also keep anything promotional between 8 am and 9 pm in the user's time zone; hold night-time welcomes until morning too. A text with the word STOP, or a clear request like "take me off your list", opts the user out: ZeroSettle blocks every number on your account from reaching them, sends return 403 recipient_opted_out, and only START opts them back in. In the US, the TCPA (47 U.S.C. § 227) generally lets a person recover $500 per violation, and a court can triple that if the violation was willful or knowing; see the TCPA consent guide. Keep the thread to onboarding help, not upgrade pitches. This isn't legal advice; have counsel review your program.
When ZeroSettle is not a fit for SaaS onboarding
ZeroSettle is the wrong tool for onboarding texts when:
- Opted-in signups exceed about 5 a day on one number and you won't move to Growth. The rest wait days for a signup-day welcome.
- You want sales pressure. Upgrade pushes need marketing consent, and ZeroSettle suspends lines used for unsolicited messaging.
- Your users rarely carry iPhones, such as an Android-heavy or desktop-only audience. SMS fallback is only on ZeroSettle's roadmap. ZeroSettle does check each first message with Apple: a number not on iMessage gets
422 not_on_imessage, nothing is sent and no new contact is used. - You need in-app guided tours. That is a different tool; ZeroSettle carries messages outside your app, not walkthroughs inside it.
How much does iMessage for SaaS onboarding cost?
Onboarding texts cost the plan price: ZeroSettle has no per-message fees, and messages are unlimited (counted).
| Plan | Price | Fits |
|---|---|---|
| Pro | $49/month | One number to start, 5 welcomes a day, unlimited messages with people who message you first, self-serve |
| Growth | From $599/mo | 3+ numbers working as one, after a demo |
Neither plan has a setup fee, and the subscription carries a 30-day money-back guarantee. AI credits (1,000 a month on Pro, 5,000 on Growth) are only for the hosted AI agent; welcomes, nudges and the founder's answers use none. See pricing, get a Pro number, or book a Growth demo.
Frequently asked questions
Can the hosted AI agent answer onboarding replies instead of the founder?
Yes, in Beta, but it is either-or per line group: while the agent manages the number, your welcome and nudge jobs and the founder's console replies are refused with 403 managed_by_zerosettle, and conversations start with POST /api/imessage/agent/start instead. Founder-led onboarding stays Managed by API.
Can we use the founder's existing mobile number?
No. ZeroSettle sets up a number that exists only for iMessage; bringing or porting your own number isn't offered. Put the founder's name and photo on it instead.
Can we see whether a trial user read the founder's text?
No. The API reports message.sent (the text left the line) and message.failed, not delivered or read status. Judge the welcome by replies and by the product events that follow.
Sources
- ZeroSettle iMessage API docs: "Your iMessage number", "Send a message", "Attachments", "Customer support", "AI agent", "Webhooks, step by step" and "Errors, limits, and compliance". Checked October 12, 2026.
- ZeroSettle homepage: Plans. Checked October 12, 2026.
- ZeroSettle iMessage API Terms of Service, section 13 (opt-in and prohibited uses). Last modified September 28, 2026.
- View conversations from unknown senders in Messages on your iPhone, Apple Support. Checked October 8, 2026.
- 47 U.S. Code § 227, Restrictions on use of telephone equipment (the TCPA), Legal Information Institute, Cornell Law School. Checked October 8, 2026.