Use cases

Customer feedback over iMessage: tapback surveys and voice notes

ZeroSettle lets a support or product team run one-tap customer feedback surveys over iMessage: after a ticket closes, your server asks in the existing thread how it went and invites a 👍 or 👎 tapback on that message. The tapback reaches your webhook as reaction.received with its type and the message it points at, so each survey gets a clean score. On a 👎, you ask one follow-up and invite a voice note, which arrives as an .m4a file you can transcribe. Because the survey goes into a thread that already exists, it uses none of your number's daily new-contact allowance.

Key takeaways

  • What it is: a one-question survey sent into an existing iMessage thread from your business's own number. The answer is a 👍 or 👎 tapback that your webhook scores, plus an optional voice note.
  • Who it fits: support teams, service businesses and product teams whose customers use iPhone and already text them.
  • Price: Pro is $49/month for one number, and Growth is from $599/mo. There are no per-message fees.
  • Main limit: surveys in existing threads are not limited. Recruiting research participants who have never texted your number counts against its new contacts: 5 a day per number on Pro, 50 per number on Growth.
  • What it does not do: no built-in forms or branching, no SMS or Android reach, and no anonymous answers.

Who uses iMessage for feedback surveys?

iMessage feedback surveys suit teams that already talk with customers in Messages and want one score per interaction instead of another form. ZeroSettle is an iMessage API: a real phone number on real iMessage, running on Apple hardware we operate for you in a server facility, that your code texts customers from and hears back on through a bearer token and signed webhooks.

  • Support teams scoring each closed ticket (CSAT). ZeroSettle's support docs put it plainly: A ❤️ on “Fixed, sorry about that” is your CSAT survey.
  • Service businesses asking how a visit or job went.
  • Product teams that would rather collect a 60-second voice note from opted-in users than book a 30-minute interview.

A tapback answer is one tap in a thread the customer already has open, with no link to open and no form to finish. More workflows are in the use-case hub.

How iMessage feedback surveys work, step by step

A closed ticket triggers one send, and the customer's tapback comes back to your webhook naming the survey message; a 👎 gets one follow-up that invites a voice note.

  1. Ask once, in the same thread. Call POST https://portal.zerosettle.io/api/imessage/send with “How did we do? Tap 👍 or 👎 on this message.” and store the returned id with the ticket.
  2. Score the tapback. ZeroSettle POSTs a signed reaction.received event whose message.reaction is like or dislike (count a love as a 👍) and whose message.target_id is the survey's id. The other tapback types are listed in what an iMessage API can send.
  3. Keep the latest answer. Messages allows one tapback per person per message, which they can change or remove. A retraction arrives with the remove_ prefix (remove_like), so keep the newest event by message id.
  4. Follow up on a 👎, once: “Sorry about that. What should we have done better? A quick voice note is fine.”
  5. Transcribe the voice note. It arrives as message.received with kind: "audio", an audio.url to an .m4a (AAC) file and duration_ms. Fetch it with the same bearer token and run it through your speech-to-text provider, as with technicians' voice-note job reports.
  6. Thank them with a ❤️ on the voice note (POST /api/imessage/reactions, "reaction": "love") and one closing text.

The same mechanics for user research

Product teams ask opted-in users “Got 60 seconds? Reply with a voice note: what almost made you quit last week?”, tag the transcripts and thank each person with a ❤️. The invite needs no link, which matters: a link in a first message to someone who has never texted your number is not tappable. Coaches use tapbacks the same way in client check-ins.

Answers that aren't a clean 👍 or 👎

How other survey answers reach a ZeroSettle webhook (API docs, checked October 12, 2026)
The customer…Arrives asSurvey rule
Taps ❤️reaction.received: loveCount as a 👍, as ZeroSettle's docs do
Taps 😂, ‼️ or ❓reaction.received: laugh, emphasize or questionLeave unscored
Reacts with another emoji or a stickerNot one of the six documented tapback types, so don't expect a reaction.receivedName 👍 and 👎 in the question
Types an answermessage.received, kind: "message"A person reads it
Sends a screenshotmessage.received, kind: "image", with the fileAttach it to the ticket
Sends a screen recordingmessage.received with no file (video isn't delivered)Ask for a screenshot instead

A typed answer can opt the customer out

ZeroSettle treats a text with the word STOP in it, or a clear request like “leave me alone”, as an opt-out from every number on your account, so “the sync would just stop” ends the conversation: later sends, your ❤️ included, return 403 recipient_opted_out until they text START. Voice notes, photos and tapbacks never opt anyone out.

Example conversation: a support survey after a fix

This thread shows a 👎, the voice-note follow-up and the thank-you, from the support team's side.

  1. Today 3:41 PM
  2. Planwise Support: Glad we got that sorted! How did we do? Tap 👍 or 👎 on this message.👎
  3. Planwise Support: Sorry about that. What should we have done better? A quick voice note is fine.
  4. Customer: Voice message, 0:18❤️
  5. Planwise Support: Thank you, that's going straight to the team.
Illustrative example with a fictional software company, Planwise. Blue bubbles are sent from Planwise's number; the gray bubble is the customer's voice note. The 👎 reached the webhook as reaction.received with reaction: "dislike".

Setup: tapback scoring and voice-note transcription

The survey needs one send when the ticket closes and a receiver that scores tapbacks by target_id and fetches voice notes, built from the webhooks and attachments sections of the ZeroSettle docs.

Send the survey

bash
BASE=https://portal.zerosettle.io
curl -s -X POST -H "Authorization: Bearer $ZEROSETTLE_TOKEN" -H 'Content-Type: application/json' \
  -d '{"to":"+15555550142","text":"Glad we got that sorted! How did we do? Tap 👍 or 👎 on this message."}' \
  $BASE/api/imessage/send | jq .id    # store this id with the ticket

The call returns once the line hands the message to Apple, normally in 2–5 seconds, so send from a background job. A recipient_opted_out error means the customer texted STOP: skip the survey.

Score tapbacks and transcribe voice notes

python
# feedback.py: run with  uvicorn feedback:app --port 8100
import hashlib, hmac, json, os, time
import httpx
from fastapi import BackgroundTasks, FastAPI, Request, Response

app = FastAPI()
BASE = "https://portal.zerosettle.io"
AUTH = {"Authorization": f"Bearer {os.environ['ZEROSETTLE_TOKEN']}"}
SECRET = os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
SCORE = {"like": 1, "love": 1, "dislike": -1}  # a ❤️ on the survey counts as a 👍
seen = set()                                   # use your database in production

def verify(header, raw, tolerance=300):
    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 call(method, path, body=None):
    async with httpx.AsyncClient() as c:
        return await c.request(method, BASE + path, headers=AUTH, json=body, timeout=90)

async def on_tapback(m):
    s = surveys.by_message_id(m["target_id"])  # your DB: survey message id -> ticket
    if s is None or m["id"] < s.last_reaction_id:   # last_reaction_id starts at 0
        return                                 # not a survey, or an older event arriving late
    s.last_reaction_id, r = m["id"], m["reaction"]
    if not r.startswith("remove_"):
        s.reaction = r                         # one tapback per person: the newest replaces the last
    elif r[7:] == s.reaction:
        s.reaction = None                      # they took the answer back
    s.score = SCORE.get(s.reaction)            # 1, -1, or None (unscored)
    if s.score == -1 and not s.followed_up:
        s.followed_up = True
        await call("POST", "/api/imessage/send", {"to": m["handle"],
            "text": "Sorry about that. What should we have done better? A quick voice note is fine."})
    s.save()

async def on_voice(m):
    s = surveys.awaiting_voice(m["handle"])    # a 👎 survey we followed up on, else None
    if s is None or not m.get("audio"):
        return surveys.store_comment(m)        # not a survey answer, or the file never arrived
    r = await call("GET", m["audio"]["url"])   # .m4a (AAC), same bearer token
    r.raise_for_status()
    s.voice_text, s.voice_ms = transcribe(r.content), m["audio"]["duration_ms"]
    s.save()
    await call("POST", "/api/imessage/reactions", {"message_id": m["id"], "reaction": "love"})
    await call("POST", "/api/imessage/send", {"to": m["handle"],
        "text": "Thank you, that's going straight to the team."})

@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 m["direction"] != "in" or (m["id"], ev["event"]) in seen:
        return {"ok": True}                    # typing events, our own sends, retries
    seen.add((m["id"], ev["event"]))
    if ev["event"] == "reaction.received":
        tasks.add_task(on_tapback, m)
    elif m.get("kind") == "audio":
        tasks.add_task(on_voice, m)
    else:
        tasks.add_task(surveys.store_comment, m)   # typed answers: a person reads them
    return {"ok": True}

surveys and transcribe stand in for your database and speech-to-text provider; only a voice note answering your follow-up gets the thank-you. Register the receiver with PUT /api/imessage/webhook and keep the signing secret, which is shown once; how ZeroSettle webhooks are signed, retried and deduplicated covers the rest.

Limits and consent for feedback surveys

A survey sent into an existing thread uses none of ZeroSettle's new-contact allowance; only research participants who have never texted your number or heard from it count. Consent is the tighter limit.

What feedback surveys use of ZeroSettle's limits, per number (API docs, checked October 12, 2026)
ActionProGrowth
Survey, follow-up or ❤️ in an existing threadNo new-contact costNo new-contact cost
First invite to someone who has never texted the line or heard from it5 a day50 a day, 3 per rolling 15 min
Unanswered messages to one customer2 until they reply; a tapback counts as a reply2 until they reply; a tapback counts as a reply

Source: ZeroSettle iMessage API docs, “Errors, limits, and compliance”. The day runs 3 AM to 3 AM Eastern. A survey sent within 5 minutes of your closing reply counts as one message with it.

Inviting 60 opted-in users who have never texted the line takes twelve days on one Pro number (5 a day). On Growth each number takes 50 a day at 3 per 15 minutes, so an account with three numbers can invite all 60 the same day. A handful of new participants a day is a reasonable research pace anyway: each one is a recording someone has to hear.

Consent for surveys and recordings

Send one survey per interaction, only to people who gave you their number, and don't chase an unanswered one. Say how recordings will be used: by default, Messages deletes an audio message from the sender's own iPhone 2 minutes after it is sent unless they tap Keep, so people may assume a voice note is temporary, while ZeroSettle keeps messages indefinitely for now. Keep offers out of the survey. If you want public reviews, ask everyone, not only the 👍s: Google prohibits selectively soliciting positive reviews (see review requests without gating). This isn’t legal advice.

When ZeroSettle is not a fit for feedback surveys

ZeroSettle is the wrong tool when a survey needs more than one tap and one recording, or when the sample matters as much as the answers.

  • You need multi-question surveys with branching. Send a survey-tool link after the customer replies, when links in the thread are tappable.
  • You need a representative sample that includes Android users. ZeroSettle reaches iMessage handles only, with no SMS fallback, so results lean toward Apple users; non-Apple phones get SMS or RCS, not iMessage.
  • You would survey people with no existing relationship, such as a bought panel. That is unsolicited messaging, and ZeroSettle suspends lines used for it.
  • You need anonymous feedback. Every answer arrives with the customer's handle.

How much does iMessage for feedback surveys cost?

Feedback surveys cost the plan price. ZeroSettle charges no per-message fees, and messages in both directions are unlimited (counted).

ZeroSettle plans for feedback surveys (zerosettle.io, checked October 12, 2026)
PlanPriceFits
Pro$49/monthOne 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/moSeveral numbers working as one, 50 new contacts a day per number, set up after a demo

Both plans carry a 30-day money-back guarantee on the subscription, and this flow uses no AI credits. See the pricing page, read the ZeroSettle API docs, or get a number.

Frequently asked questions

What if a customer has two surveys open at once?

Each scores separately. Every reaction.received carries the target_id of the exact message it reacts to, so a 👍 on last week's survey and a 👎 on today's land on different tickets.

Can I measure a response rate?

Yes, as answered surveys over sent ones. ZeroSettle reports message.sent and message.failed but no delivered or read status, so there is no open rate.

Can I survey after an in-person visit instead of a ticket?

Yes, if the customer gave you their number for this. A cold survey counts as a new contact, should say who you are, and may be filed away unnotified by iOS 26's Screen Unknown Senders; asking them to text your number at checkout avoids both.

Can the hosted AI agent run the survey instead of my server?

Not this flow. On a line group set to Managed by AI Agent, the agent answers inbound messages without calling your webhook, and your own sends are refused with 403 managed_by_zerosettle. Run surveys from a number managed by API.

Sources

  1. ZeroSettle iMessage API docs: “Send a message” (reactions), “Attachments”, “Customer support”, “AI agent”, “Webhooks, step by step” and “Errors, limits, and compliance”. Checked October 12, 2026.
  2. React with Tapbacks in Messages on iPhone, Apple Support (iPhone User Guide). Checked October 8, 2026.
  3. Send and receive audio messages in Messages on iPhone, Apple Support (iPhone User Guide). Checked October 8, 2026.
  4. What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support. Checked October 8, 2026.
  5. Prohibited and restricted content, Google Maps user-generated content policy (rating manipulation). 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 in a server facility, 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 zerosettle.io as of October 12, 2026. Apple's and Google's behavior and policies come from their own pages, checked October 8, 2026.

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.