Use cases

Coaching client check-ins over iMessage: photos, tapbacks and voice notes

ZeroSettle gives an online coach or coaching app its own iMessage number that each client texts once at kickoff, so every daily check-in after that (“Did you hit 140g protein today? 👍 or 👎”) is an ongoing conversation that no new-contact limit applies to. The client’s tapback reaches your webhook as a reaction.received event your app logs as that day’s answer, meal photos arrive with the file (iPhone’s HEIC converted to JPEG), and coaches answer with voice notes and tapbacks. Pro is $49/month, starting with one number; on Growth, from $599/mo, each coach on a team can have a number with their own name and photo.

Key takeaways

  • What it is: a coaching number on real iMessage. Your server sends the daily prompt through a REST API, and tapbacks, photos and voice notes come back on a signed webhook.
  • Who it fits: online personal trainers, nutrition and habit coaches, and coaching apps whose clients mostly use iPhone.
  • Price: Pro is $49/month, starting with one number. Growth, for several numbers working as one, is from $599/mo. No per-message fees.
  • Main limit: clients who text first never count against the new-contact limit. On every plan, a number can send a client at most 2 messages without a reply, and a tapback counts as a reply.
  • What it does not do: no group chats, no video, no SMS fallback for Android clients, and no health information without a signed BAA.

Who uses iMessage for coaching check-ins?

iMessage check-ins suit online personal trainers, nutrition and habit coaches, and the coaching apps they run on. 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 scatter: check-in forms get skipped, in-app chat needs the app open, and client messages split across Instagram, WhatsApp and SMS. Studios chasing intro-class visitors have a different job (fitness studio follow-up), as do tutors writing to parents (tutoring parent updates).

How iMessage coaching check-ins work, step by step

The client starts the thread once, your scheduler sends a prompt every day, and ZeroSettle’s webhook turns each reply into a row your app logs. ZeroSettle does not schedule sends; your job decides when.

  1. Kickoff. The welcome email carries a prefilled sms: link (“Hi Coach Dee, it’s Sam”). Once the client has texted, links in later messages are tappable. See getting clients to text first.
  2. Daily prompt. At the client’s chosen time, your job calls POST /api/imessage/send and stores the message id it returns.
  3. Tapback answer. The client double-taps the prompt (or touches and holds it) and picks thumbs-up or thumbs-down. Your webhook gets reaction.received with reaction set to like or dislike and target_id pointing at the prompt, so the day is logged as hit or missed, as in tapback surveys.
  4. Photos. A meal or progress photo arrives as message.received with kind: "image". Your server fetches it with the same bearer token for the client’s file or your own vision model.
  5. The coach answers with a voice note sent as audio_b64, a tapback through POST /api/imessage/reactions, or a text from the ZeroSettle console.
  6. Routine versus hard moments. Your own model can answer routine replies and flag the hard ones (a missed week, an injury) to the coach. ZeroSettle’s terms require disclosing AI replies unless that is evident from context, and rule out automating critical health advice without a qualified professional supervising, so injury and health questions belong with the coach.

Example conversation: a day-12 check-in

In this day-12 check-in the client misses the target with a thumbs-down, sends a dinner photo and gets a voice note back from the coach’s number.

  1. Today 7:30 PM
  2. Coach: Day 12 check-in: did you hit 140g protein today? 👍 or 👎👎
  3. Client: Photo: dinner plate
  4. Client: ran out of time at lunch
  5. Coach: Voice message, 0:24
  6. Client: ok greek yogurt tomorrow 💪❤️
Illustrative example with a fictional coach and client. Blue bubbles are sent from Coach Dee’s number; gray bubbles are the client’s. The thumbs-down was logged as a missed day.

Setup: a daily prompt job plus tapback logging

One script covers the loop: a daily send that stores the prompt’s id, a voice-note sender, and a receiver that verifies ZeroSettle’s signature (copied from the docs’ webhook steps), logs tapbacks and files photos.

python
# checkins.py: run with  uvicorn checkins:app --port 8100
import base64, hashlib, hmac, json, os, pathlib, time
import 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 = FastAPI()
seen: set[tuple[int, str]] = set()           # use your database in production

# Your app's storage and coach inbox: replace these stubs.
def save_checkin(client, message_id): ...
def checkin_for_message(message_id): ...     # the check-in whose prompt has this id, or None
def retry_later(client, after_s): ...
def flag_for_coach(client, code): ...
def notify_coach(m): ...
def add_to_client_file(handle, photo): ...
def route_reply(m): ...

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 send_prompt(client):                     # your scheduler: daily, one client at a time
    text = f"Day {client.day} check-in: did you hit {client.protein_g}g protein today? 👍 or 👎"
    r = requests.post(f"{BASE}/api/imessage/send", headers=AUTH,
                      json={"to": client.handle, "text": text}, timeout=90)
    if r.ok:
        save_checkin(client, message_id=r.json()["id"])    # the id a tapback points at
    elif r.status_code in (429, 503):                      # line busy or briefly unreachable
        retry_later(client, after_s=int(r.headers.get("Retry-After", 60)))
    else:
        flag_for_coach(client, r.json()["error"]["code"])  # recipient_opted_out, awaiting_reply

def send_voice_note(handle, path):           # recorded by the coach in your app
    audio = base64.b64encode(pathlib.Path(path).read_bytes()).decode()
    requests.post(f"{BASE}/api/imessage/send", headers=AUTH, timeout=120,
                  json={"to": handle, "audio_b64": audio, "audio_format": "m4a"})

def handle(event, m):
    if event == "reaction.received":
        checkin = checkin_for_message(m["target_id"])
        if checkin and m.get("reaction") in ("like", "dislike"):
            checkin.log(hit=m["reaction"] == "like")       # keep the latest per check-in
        else:
            notify_coach(m)
    elif m.get("kind") == "image" and m.get("image"):
        photo = requests.get(BASE + m["image"]["url"], headers=AUTH, timeout=30).content
        add_to_client_file(m["handle"], photo)
    else:
        route_reply(m)                       # routine: your model; hard moments: the coach

@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 m is None or ev["event"] not in ("message.received", "reaction.received"):
        return Response(status_code=200)     # your own sends echo back; typing has no message
    if (m["id"], ev["event"]) in seen:
        return Response(status_code=200)     # retries carry the same id and event
    seen.add((m["id"], ev["event"]))
    tasks.add_task(handle, ev["event"], m)
    return Response(status_code=200)

Register the receiver with PUT /api/imessage/webhook and store the signing secret; it is shown once. Answer with a 2xx within 10 seconds and do the work afterwards, as the background task does. On a timeout or any other status, ZeroSettle retries three more times, after 1, 5 and 20 seconds.

Details that matter for coaching

  • Emoji tapbacks. On recent iOS a client can react with any emoji, Memoji or sticker, but ZeroSettle’s docs name six reaction types (love, like, dislike, laugh, emphasize, question). Score only like and dislike; send anything else to the coach.
  • One photo per text. A message with three meal photos delivers only the first, so ask clients to send each meal on its own.
  • Voice notes expire for the client. By default Messages deletes an audio message 2 minutes after it is played unless the listener taps Keep. ZeroSettle keeps your copy at GET /api/imessage/messages/{id}/audio; formats and sizes are in what an iMessage API can send.

Limits and consent for coaching check-ins

After the kickoff text, every check-in is an ongoing conversation that ZeroSettle’s new-contact limit never touches, and tapbacks never count. The rule that matters is the cap on unanswered messages.

ZeroSettle limits for daily check-ins, per number (API docs, checked October 12, 2026)
LimitProGrowth
New-contact limit for clients who texted the number firstNoneNone
Clients the coach texts first, per day (3 AM to 3 AM Eastern)550
Clients the coach texts first, per rolling 15 minutes—3
Messages to a client who hasn’t replied2 (sends within 5 minutes of the first count as one), then 403 awaiting_reply2 (sends within 5 minutes of the first count as one), then 403 awaiting_reply

Source: ZeroSettle iMessage API docs, “Errors, limits, and compliance”. Message volume is unlimited on both plans.

The limit math for one coach’s roster

  • 80 clients on Pro, onboarded with the kickoff link: no limit applies. Sends go out one at a time per number, normally 2–5 seconds each, so if all 80 prompts share a 7:30 PM slot, the last goes out about 3 to 7 minutes later.
  • Moving 80 existing clients by texting them first: 16 days at Pro’s 5 a day, and on iOS 26 a cold first text can be filed under unknown senders with no notification. Asking them to text first uses none of the limit and avoids that.
  • A quiet client: if days 12 and 13 go unanswered, the day-14 prompt returns 403 awaiting_reply and nothing is sent. Any reply resets it, a 👍 included. This applies on every plan, and repeated messages to people who never answer get numbers flagged, so pause prompts after two quiet days.

Consent and the word STOP

Text only clients who opted in through the coaching agreement, keep that record, and send between 8 am and 9 pm in their time zone, the window ZeroSettle’s docs set for anything promotional. 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 “I can’t stop snacking at night” counts. Sends, tapbacks and typing to that client then return 403 recipient_opted_out until they text START as the whole message (or ask, and ZeroSettle can clear it), so explain this at kickoff (how STOP works). This isn’t legal advice; consent rules vary by place and purpose.

When ZeroSettle is not a fit for coaching check-ins

ZeroSettle is the wrong tool for check-ins in these cases:

  • You are a licensed dietitian or clinician who may be covered by HIPAA and check-ins carry patient health information. ZeroSettle’s terms prohibit that without a signed Business Associate Agreement, the written agreement HIPAA generally requires before a vendor handles that information for you. Whether HIPAA applies to you is a question for counsel; this isn’t legal advice.
  • You run group coaching chats. Conversations are one to one; group chats are on the roadmap.
  • Most clients use Android. There is no SMS fallback, and the kickoff sms: link opens the SMS app on Android.
  • You coach from video form checks. Video isn’t supported in either direction.
  • You need proof a client played a voice note. The API reports that a message left the line, not that it was read.

How much does iMessage for coaching check-ins cost?

Check-ins cost the plan price: ZeroSettle charges no per-message fees, and message volume is unlimited on both plans.

ZeroSettle plans for coaching check-ins (zerosettle.io, checked October 8, 2026)
PlanPriceFits
Pro$49/monthOne coach, one number to start, 5 new contacts a day per number, unlimited messages with people who message you first, self-serve, no setup fee
GrowthFrom $599/moA coaching team: 3+ numbers working as one, each coach’s number with its own name and photo; book a demo and ZeroSettle sends a signup link

Both plans are billed monthly with a 30-day money-back guarantee on the subscription, and your own model on the webhook uses no ZeroSettle AI credits. Compare ZeroSettle plans, browse more iMessage use cases, or get a number.

Frequently asked questions

Can ZeroSettle’s hosted AI agent run the check-ins instead of my own model?

Not with a coach in the loop. On a line group managed by the hosted AI agent (in beta), the agent answers every inbound text, your webhook is not called, and your own sends, the console included, are refused with 403 managed_by_zerosettle. Its triggers can send a daily follow-up, but they are the agent’s, and the coach can’t step into the thread. Running your own model on the webhook keeps both the daily prompts and the coach.

Can clients send progress videos?

No. A video arrives as a message row with its text and no file. Ask for photos instead, one per text.

Can clients reply with voice notes?

Yes. A client’s voice note arrives as kind: "audio", converted to .m4a (AAC) with duration_ms filled in; fetch it with the same bearer token. Scoring voice replies shows one use.

What happens when a client moves to another coach on a Growth team?

Easiest: send the client the new coach’s sms: link, so they text that number first and it is never a new contact. Otherwise send from the new coach’s number by passing its line_id; that first message counts as a new contact for that number (50 a day, 3 per 15 minutes on Growth) and shares the new coach’s name and photo. Either way, pass that line_id on every later send to the client.

Sources

  1. ZeroSettle iMessage API docs: “Your iMessage number”, “Send a message”, “Attachments”, “AI agent”, “Webhooks, step by step” and “Errors, limits, and compliance”. Checked October 8, 2026.
  2. ZeroSettle iMessage API Terms of Service, section 4 (AI disclosure and supervised health advice) and section 13 (protected health information). Last modified September 28, 2026.
  3. React with Tapbacks in Messages on iPhone, Apple Support. Checked October 8, 2026.
  4. Send and receive audio messages in Messages on iPhone, Apple Support. Checked October 8, 2026.
  5. 45 CFR 164.502, Uses and disclosures of protected health information, paragraph (e) on business associates (eCFR). 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.

Coach Dee and Sam are fictional. This page is not legal or medical 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.