Voice messages over iMessage for language-learning apps: spoken lessons, spoken answers
ZeroSettle lets a language-learning app send spoken lessons as native iMessage voice bubbles, straight from text-to-speech output in mp3 or wav up to 6 MB, and receive learners' spoken answers through its webhook as .m4a recordings to transcribe and score. Learners join by texting your number from the app, so daily practice runs in an existing conversation and never counts against a new-contact limit. One gotcha: any text containing the word "stop" opts the sender out, so teach learners to type "pause" instead of "can we stop here for today".
Key takeaways
- What it is: your app's own iMessage number sends each phrase as a written text plus a voice clip, and learners' voice answers arrive through your webhook as
.m4a(AAC) recordings. - Who it fits: language-learning apps, online tutors and pronunciation coaches whose learners use iPhone.
- What you build: text-to-speech, transcription and scoring. ZeroSettle carries the audio both ways; it does not grade pronunciation.
- Price: Pro is $49/month for one number to start, with unlimited messages with people who message you first. Growth, for several numbers working as one, is from $599/mo. No per-message fees.
- Main limits: voice clips up to 6 MB, one send at a time per number, 2 unanswered messages per learner on every plan, and no SMS fallback for learners on Android.
- Watch out: a learner who texts any sentence with the word "stop" in it is opted out until they text START.
Who uses iMessage for language learning?
iMessage voice lessons fit language-learning apps, online tutors and pronunciation coaches who want learners to hear a phrase and say it back every day, inside Messages. 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.
Pronunciation doesn't survive text: a beginner reading "biblioteca" can't hear which syllable takes the stress, a link to an audio file adds a tap and a page load, and a daily push reminder is easy to switch off. A voice bubble plays in the thread the learner already reads, and they answer by recording one back (tap +, then Audio). Voice lessons are one of the engagement workflows in 50 ways businesses use iMessage.
How iMessage for language learning works, step by step
Your server sends the phrase as text and as a text-to-speech clip, the learner answers by voice, and your server scores the recording and replies. ZeroSettle carries both directions through one send endpoint and one webhook.
- The learner joins from the app. A "Practice on iMessage" button opens Messages with your number and a prefilled code ("Spanish practice, code 4F7K"). It reaches your webhook as
message.received, and because the learner texted first, they never count as a new contact (see how to get customers to text you first). - Send the written phrase, then the clip. Call
POST https://portal.zerosettle.io/api/imessage/sendonce withtext, then once with your TTS output asaudio_b64plusaudio_format; mp3 or wav work as-is. A voice bubble can't carry a caption. Keep clips under a minute and under 6 MB. - The learner gets Messages' native voice bubble: a waveform, a play button and, in supported languages, a transcript. Apple's guide says touching and holding Play adjusts the playback speed, so a learner can slow a phrase down.
- The learner answers by voice. ZeroSettle POSTs a signed
message.receivedwithkind: "audio"and anaudioobject that carriesduration_ms;GET /api/imessage/messages/{id}/audioreturns the recording as an.m4a(AAC) file. - Score and reply. A pass gets a
lovetapback fromPOST /api/imessage/reactions; a miss gets a correction text and the phrase again, slower.
Watch for the word "stop"
Any text containing the word "stop" opts the sender out of every number on your account, so a learner who types "can we stop here for today" is unsubscribed until they text START. Make "pause" your app's break keyword. English courses get hit twice: a typed answer like "The bus stop is on the corner" opts the learner out too, so ask for those answers by voice. Only a text counts, never a voice message or a tapback (details in how STOP and opt-outs work).
Example conversation: a pronunciation correction
One evening's practice from a Spanish app: the phrase, two attempts, a correction with a slower clip, and a tapback on the attempt that passed.
- Today 7:30 PM
- Business: Today's phrase: ¿Dónde está la biblioteca?
- Business: [Voice message, 0:04]
- Learner: [Voice message, 0:04]
- Business: So close! Stress the "te": bee-blee-oh-TEH-kah.
- Business: [Voice message, 0:05, slower]
- Learner: [Voice message, 0:04]❤️
- Business: Perfect. Same time tomorrow?
Setup: TTS out, voice in, a correction back
One small server sends the lesson and grades the answer. The signature check is copied from the ZeroSettle webhook docs; tts_mp3_b64(), score(), reply_to_text() and the learner table behind learner_for() and pause_learner() are yours.
# lessons.py: run with uvicorn lessons:app --port 8100
import asyncio, hashlib, hmac, json, os, time
import httpx
from fastapi import BackgroundTasks, FastAPI, Request, Response
TOKEN, SECRET = os.environ["ZEROSETTLE_TOKEN"], os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
zs = httpx.AsyncClient(base_url="https://portal.zerosettle.io", timeout=120,
headers={"Authorization": f"Bearer {TOKEN}"})
app = FastAPI()
seen: set[tuple[int, str]] = set() # use your database in production
def verify(header: str, raw: 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, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
async def send(to: str, **body) -> bool:
for _ in range(5):
r = await zs.post("/api/imessage/send", json={"to": to, **body})
code = r.json()["error"]["code"] if r.is_error else None
if code != "line_busy":
break
await asyncio.sleep(2) # one send at a time per number
if code in ("recipient_opted_out", "awaiting_reply"):
pause_learner(to, code) # STOP, or a lapsed streak
return code is None
async def send_lesson(learner) -> None: # your job runner calls this
if await send(learner.handle, text=f"Today's phrase: {learner.phrase}"):
await send(learner.handle, audio_b64=tts_mp3_b64(learner.phrase, 1.0), audio_format="mp3")
async def grade(m: dict) -> None:
learner = learner_for(m["handle"]) # your DB: handle -> today's phrase
if not m.get("audio"):
return await reply_to_text(learner, m) # a typed reply, or audio that never arrived
clip = (await zs.get(m["audio"]["url"])).content # .m4a (AAC)
result = await score(clip, reference=learner.phrase, language=learner.language)
if result.passed:
await zs.post("/api/imessage/reactions", json={"message_id": m["id"], "reaction": "love"})
await send(m["handle"], text="Perfect. Same time tomorrow?")
elif await send(m["handle"], text=result.tip):
await send(m["handle"], audio_b64=tts_mp3_b64(learner.phrase, 0.75), audio_format="mp3")
@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} # your own sends' echoes, typing, retries
seen.add((m["id"], ev["event"]))
tasks.add_task(grade, m) # answer 2xx now; score after
return {"ok": True}
Text-to-speech out: Google Cloud's text:synthesize returns audioContent already base64-encoded, so an MP3 result drops straight into audio_b64, and its speakingRate makes the slower clip ("0.5 is half as fast"). OpenAI's speech endpoint returns an MP3 file by default; base64-encode its bytes first.
Scoring in: OpenAI's transcription accepts .m4a and takes a language hint. Microsoft's Azure pronunciation assessment takes the target phrase as reference text and returns accuracy, fluency and completeness scores; its prosody score, which covers stress, is en-US only. Check either service's language support for yours.
Register the route with PUT /api/imessage/webhook and store the signing secret, which is shown once. ZeroSettle expects a 2xx within 10 seconds and retries after 1, 5 and 20 seconds, so scoring runs after the response. Formats are in the attachments docs and what an iMessage API can send; to let your own model hold a whole conversation in the target language, see give your AI agent an iMessage number.
Limits and consent for language learning
Daily practice doesn't hit ZeroSettle's new-contact limits, because each learner texted your number first. Two limits still shape the design: one send at a time per number, and a cap of 2 unanswered messages per learner on every plan.
| What the app's number handles | Pro | Growth |
|---|---|---|
| A learner's first text from the in-app button | Never a new contact | Never a new contact |
| Lessons, corrections and learners' voice answers | Unlimited | Unlimited |
| Messages to a learner who hasn't answered | 2, then 403 awaiting_reply | 2, then 403 awaiting_reply |
| A first message to someone who never texted | 5 a day | 50 a day, 3 per rolling 15 minutes |
Source: ZeroSettle iMessage API docs, "Attachments" and "Errors, limits, and compliance". Messages sent within 5 minutes of the first count as one unanswered message, so a phrase plus its clip is one.
The math for 500 learners: a lesson is two sends, so lessons alone are 1,000 sends a day, and a number sends one message at a time, normally 2–5 seconds each, and a voice clip can take longer than a text. That is roughly 33 to 83 minutes of sending before the extra time for clips, so schedule each lesson at the learner's chosen time instead of one 8 AM run, and retry 429 line_busy after 2 seconds, as send() does.
Lapsed streaks: on every plan, a learner can have at most 2 unanswered messages from your number, and the session's closing text is one of them. After "Perfect. Same time tomorrow?", the next lesson is the second, so if a learner skips one day, the day-three lesson is refused with 403 awaiting_reply until they send anything back, a tapback included. Your own tapbacks never count, so closing a pass with the tapback alone leaves room for two lessons. Pause after two silent days anyway: repeated messages to people who never answer can get a number flagged by Apple.
Consent: learners opt in by texting
Keep the learner's handle, join code and time as the consent record, and make your first reply say who you are. ZeroSettle enforces STOP across every number on your account, and only START (or unstop) brings a learner back. Joining covers practice, not marketing: send an upgrade offer only to learners who agreed to marketing texts (in writing, in the US), and only between 8 am and 9 pm in their time zone. Rules differ by country and situation; this isn't legal advice, so check your opt-in flow with counsel.
The same voice bubble carries a coach's note in coaching client check-ins and a director's thank-you in voice thank-yous for new donors.
When ZeroSettle is not a fit for language learning
ZeroSettle doesn't fit Android learners, video lessons, group classes, captioned voice bubbles or built-in grading.
- Learners on Android: there is no SMS fallback, and the in-app
sms:button opens the SMS app on Android. Keep in-app audio for them. - Video lessons: a line can't send video, and a video a learner sends arrives with no file.
- Group classes in one chat: conversations are one-to-one; group chats are on the roadmap.
- A caption on the voice bubble: Messages has none, so the phrase goes as a separate text.
- Grading done for you: ZeroSettle delivers the recording; transcription and scoring are your code.
How much does iMessage for language learning cost?
Voice lessons cost the plan price: ZeroSettle has no per-message fees, and messages, voice included, are unlimited (counted) in both directions.
| Plan | Price | Fits |
|---|---|---|
| Pro | $49/month, no setup fee | One number to start, self-serve, 5 new contacts a day, unlimited messages with people who message you first, 30-day money-back guarantee |
| Growth | From $599/mo | Several numbers working as one, dedicated support, set up after a demo |
Text-to-speech, transcription and scoring are billed by the providers you pick. See ZeroSettle plans and pricing, read the iMessage API docs, or get a number.
Frequently asked questions
Do TTS files need converting before I send them?
No. ZeroSettle accepts m4a, mp3, wav, aiff, caf, flac, aac, mp4 and amr as-is and delivers each as a native voice bubble. Size only matters for wav: 16-bit mono wav at 24 kHz, all that speech needs, is about 2.9 MB a minute against the 6 MB cap, while Google's MP3 output, at 32 kbps, is about 240 KB a minute.
Does the learner see a transcript of the voice messages?
Not reliably. Apple says audio message transcription is available only in select languages, and a learner's own answers are transcribed in the language their iPhone is set to, which may not be the one they're practicing. Keep sending the written phrase, and grade from the .m4a, not from what Messages shows.
A learner typed "let's stop here" and is now unsubscribed. What now?
They stay opted out until they text START (or unstop) as the whole message, and other texts don't reopen them; if they ask you another way, contact ZeroSettle to clear it. Their texts still reach your webhook, so the app can offer a "Resume practice" button that opens Messages with START prefilled.
Can learners replay yesterday's lesson in Messages?
Only if they keep it: Apple's guide says an audio message is deleted from the conversation 2 minutes after it is played unless the listener taps Keep or sets Expire to Never in Messages settings. ZeroSettle stores every clip you send (messages are kept indefinitely for now), and GET /api/imessage/messages/{id}/audio returns the exact file, so your app can offer a replay list.
Sources
- ZeroSettle iMessage API docs: "Send a message", "Attachments", "Webhooks, step by step" and "Errors, limits, and compliance". Checked October 8, 2026.
- Send and receive audio messages in Messages on iPhone, Apple Support (recording, transcription languages, playback speed, Keep and Expire). Checked October 8, 2026.
- Method: text.synthesize, Google Cloud Text-to-Speech reference (
audioContentis base64 in JSON). Checked October 8, 2026. - AudioConfig, Google Cloud Text-to-Speech reference (
speakingRate: "0.5 is half as fast"). Checked October 8, 2026. - AudioEncoding, Google Cloud Text-to-Speech reference (
MP3at 32 kbps;LINEAR16with a WAV header). Checked October 8, 2026. - Text to speech, OpenAI API docs (MP3 output by default). Checked October 8, 2026.
- File transcription, OpenAI API docs (m4a input; language hints). Checked October 8, 2026.
- Use pronunciation assessment, Microsoft Learn (reference text; accuracy, fluency and completeness scores; prosody assessment en-US only). Checked October 8, 2026.