Use cases

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.

  1. 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).
  2. Send the written phrase, then the clip. Call POST https://portal.zerosettle.io/api/imessage/send once with text, then once with your TTS output as audio_b64 plus audio_format; mp3 or wav work as-is. A voice bubble can't carry a caption. Keep clips under a minute and under 6 MB.
  3. 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.
  4. The learner answers by voice. ZeroSettle POSTs a signed message.received with kind: "audio" and an audio object that carries duration_ms; GET /api/imessage/messages/{id}/audio returns the recording as an .m4a (AAC) file.
  5. Score and reply. A pass gets a love tapback from POST /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.

  1. Today 7:30 PM
  2. Business: Today's phrase: ¿Dónde está la biblioteca?
  3. Business: [Voice message, 0:04]
  4. Learner: [Voice message, 0:04]
  5. Business: So close! Stress the "te": bee-blee-oh-TEH-kah.
  6. Business: [Voice message, 0:05, slower]
  7. Learner: [Voice message, 0:04]❤️
  8. Business: Perfect. Same time tomorrow?
Illustrative example with a fictional Spanish app. Blue bubbles are sent from the app's number through the API; gray bubbles are the learner's. Bracketed lines stand for voice messages.

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.

python
# 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.

ZeroSettle limits that apply to voice lessons, per number (API docs, checked October 12, 2026)
What the app's number handlesProGrowth
A learner's first text from the in-app buttonNever a new contactNever a new contact
Lessons, corrections and learners' voice answersUnlimitedUnlimited
Messages to a learner who hasn't answered2, then 403 awaiting_reply2, then 403 awaiting_reply
A first message to someone who never texted5 a day50 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.

ZeroSettle plans for voice lessons (zerosettle.io, checked October 12, 2026)
PlanPriceFits
Pro$49/month, no setup feeOne number to start, self-serve, 5 new contacts a day, unlimited messages with people who message you first, 30-day money-back guarantee
GrowthFrom $599/moSeveral 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

  1. ZeroSettle iMessage API docs: "Send a message", "Attachments", "Webhooks, step by step" and "Errors, limits, and compliance". Checked October 8, 2026.
  2. Send and receive audio messages in Messages on iPhone, Apple Support (recording, transcription languages, playback speed, Keep and Expire). Checked October 8, 2026.
  3. Method: text.synthesize, Google Cloud Text-to-Speech reference (audioContent is base64 in JSON). Checked October 8, 2026.
  4. AudioConfig, Google Cloud Text-to-Speech reference (speakingRate: "0.5 is half as fast"). Checked October 8, 2026.
  5. AudioEncoding, Google Cloud Text-to-Speech reference (MP3 at 32 kbps; LINEAR16 with a WAV header). Checked October 8, 2026.
  6. Text to speech, OpenAI API docs (MP3 output by default). Checked October 8, 2026.
  7. File transcription, OpenAI API docs (m4a input; language hints). Checked October 8, 2026.
  8. Use pronunciation assessment, Microsoft Learn (reference text; accuracy, fluency and completeness scores; prosody assessment en-US only). 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.

Product facts on this page come from the ZeroSettle iMessage API docs and the zerosettle.io homepage as of October 12, 2026. Outside facts come from the pages listed under Sources, checked October 8, 2026. The Spanish app and Jamie K. are fictional, and nothing here is legal 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.