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.
- Ask once, in the same thread. Call
POST https://portal.zerosettle.io/api/imessage/sendwith “How did we do? Tap 👍 or 👎 on this message.” and store the returnedidwith the ticket. - Score the tapback. ZeroSettle POSTs a signed
reaction.receivedevent whosemessage.reactionislikeordislike(count aloveas a 👍) and whosemessage.target_idis the survey's id. The other tapback types are listed in what an iMessage API can send. - 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. - Follow up on a 👎, once: “Sorry about that. What should we have done better? A quick voice note is fine.”
- Transcribe the voice note. It arrives as
message.receivedwithkind: "audio", anaudio.urlto an .m4a (AAC) file andduration_ms. Fetch it with the same bearer token and run it through your speech-to-text provider, as with technicians' voice-note job reports. - 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 👎
| The customer… | Arrives as | Survey rule |
|---|---|---|
| Taps ❤️ | reaction.received: love | Count as a 👍, as ZeroSettle's docs do |
| Taps 😂, ‼️ or ❓ | reaction.received: laugh, emphasize or question | Leave unscored |
| Reacts with another emoji or a sticker | Not one of the six documented tapback types, so don't expect a reaction.received | Name 👍 and 👎 in the question |
| Types an answer | message.received, kind: "message" | A person reads it |
| Sends a screenshot | message.received, kind: "image", with the file | Attach it to the ticket |
| Sends a screen recording | message.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.
- Today 3:41 PM
- Planwise Support: Glad we got that sorted! How did we do? Tap 👍 or 👎 on this message.👎
- Planwise Support: Sorry about that. What should we have done better? A quick voice note is fine.
- Customer: Voice message, 0:18❤️
- Planwise Support: Thank you, that's going straight to the team.
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
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
# 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.
| Action | Pro | Growth |
|---|---|---|
| Survey, follow-up or ❤️ in an existing thread | No new-contact cost | No new-contact cost |
| First invite to someone who has never texted the line or heard from it | 5 a day | 50 a day, 3 per rolling 15 min |
| Unanswered messages to one customer | 2 until they reply; a tapback counts as a reply | 2 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).
| Plan | Price | Fits |
|---|---|---|
| Pro | $49/month | One number to start, 5 new contacts a day per number, unlimited messages with people who message you first, self-serve, no setup fee |
| Growth | From $599/mo | Several 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
- 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.
- React with Tapbacks in Messages on iPhone, Apple Support (iPhone User Guide). Checked October 8, 2026.
- Send and receive audio messages in Messages on iPhone, Apple Support (iPhone User Guide). Checked October 8, 2026.
- What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support. Checked October 8, 2026.
- Prohibited and restricted content, Google Maps user-generated content policy (rating manipulation). Checked October 8, 2026.