Why links in a first iMessage aren't tappable, and what to do about it
A link in a first iMessage from a business isn't tappable because an iPhone won't open links from an unknown sender: until the recipient replies to your number or saves it as a contact, the link shows as plain text they can't tap, and no sender can avoid it. ZeroSettle, an iMessage API, still sends a first message that carries a link but returns a link_not_tappable warning. The fix is to let customers text first, or to open with a plain question (“Reply YES for your link”) and send the link after they answer.
Key takeaways
- Who decides: the recipient's iPhone. A link from a number they have never replied to or saved stays plain text, whether it sits in a text or a photo caption.
- What ZeroSettle does: sends the message (
"status": "message_sent") and addswarning.code: "link_not_tappable"to the response. - Fix 1: let them text first with a prefilled
sms:link or QR code. Every link you send back works, and the conversation never counts toward a new-contact limit. - Fix 2: if you text first, ask a question with no link in it, then send the link in your answer to their reply.
- iOS 26: with Screen Unknown Senders on, a cold first text goes to a separate list without a notification.
- Price: Pro is $49/month for one number. Growth, for several numbers working as one, is from $599/mo.
Why are links in a first message not tappable?
Links in a first iMessage aren't tappable because the recipient's iPhone protects them from unknown senders. ZeroSettle's docs:
An iPhone won't open links from an unknown sender. Until the recipient replies to your number or saves it as a contact, a link in your text shows as plain text they can't tap. The recipient's phone does this, so no sender can avoid it.
ZeroSettle iMessage API docs, “Links in a first message”
Apple's iPhone User Guide gives the recipient's side: “if you can't open a link sent by an unknown sender, make them a known sender”, by tapping Mark as Known or adding the number to Contacts. The rule is per person, not per message: once someone has replied to your number or saved it, every later link to them is tappable, in texts and photo captions alike.
| Recipient's situation | Link tappable? | ZeroSettle warning? |
|---|---|---|
| Never texted or saved your number (a cold first text) | No | Yes |
| Saved your number or marked it as known, never texted | Yes | Yes |
| Texted you back after one of your texts | Yes | No |
Texted you first (sms: link, support, Sign in with iMessage) | Yes | No |
ZeroSettle can't tell whether someone saved your number, so it warns on any link to a person who has never texted the line.
How does ZeroSettle tell you a link won't be tappable?
ZeroSettle sends the message and flags it: a text with a link, sent to someone who has never texted the line, still goes out, and the POST /api/imessage/send response adds a warning. A photo, caption and all, can't go to them yet: ZeroSettle refuses photos to anyone who hasn't replied with 403 attachment_before_reply. The example from the send-a-message docs:
{"ok": true, "status": "message_sent", "id": 43, "record": {…},
"warning": {"code": "link_not_tappable",
"message": "They haven't texted this number yet, so iPhone shows your link as plain text …"}}
Branch on warning.code, not the message text, and log each link_not_tappable with the template that produced it: each one points at a flow that sends a link before the person has texted you.
As of October 2026, per their own public docs, two other iMessage APIs rule such links out instead of flagging them: Sendblue declines the send, and LoopMessage's rules for starting conversations forbid it.
| Provider | What happens |
|---|---|
| ZeroSettle | Sends it and returns the link_not_tappable warning |
| Sendblue | Its “Before the first reply” limits decline a message with links, media, phone numbers or email addresses until the contact first replies: HTTP 400, PRE_REPLY_CONTENT_BLOCKED. One pre_reply_override message per contact per day can go through. |
| LoopMessage | A rule of its “Init conversations” feature: a first message “will never contain” links, emails, phone numbers, currencies or attachments |
What about iOS 26's Screen Unknown Senders?
Screen Unknown Senders is a second hurdle: on iOS 26 it moves texts from unknown senders into a separate list without a notification, so a cold first text may never be seen. It is on for anyone who had the older unknown-sender filter turned on.
- What Apple says: “you don't get notified about them unless you want to be.” Recipients can allow notifications for some categories, such as verification codes.
- How long it lasts: “Until you mark a sender as known or add them as a contact, any future messages are filtered to Unknown Senders,” per Apple's guide.
- What it means if you text first: even a link-free opener is a cold first text, so it can sit unseen in Unknown Senders. Only letting them text first avoids the filter.
- What your code can see: not whether it was seen.
message.sentmeans the message left the line; ZeroSettle reports no delivered or read status.
Pattern 1: how does letting customers text first make links tappable?
When the customer sends the first message, your number is known from the start: every link you send back is tappable, Screen Unknown Senders is avoided, and the conversation never counts toward ZeroSettle's new-contact limit.
The tool is a prefilled sms: link: on iPhone it opens Messages addressed to your number with the text typed, and it goes as iMessage. Put it behind a button on your site, on a receipt, or in a QR code on signage:
<a href="sms:+18055550100&body=Hi%21%20Send%20me%20the%20class%20schedule">Text us for the schedule</a>
On Android the same link opens the SMS app, and ZeroSettle has no SMS fallback, so keep email or chat for those visitors. More in how to get customers to text you first.
Pattern 2: how do you send a link when you text first?
If you text first, open with plain text that says who you are and asks for a reply (“Reply YES for your link”), then send the link once they answer: after their reply, the link is tappable. Texting first also needs consent: generally, the person gave you their number for this purpose, and you can show the record (see consent for business iMessage; this isn't legal advice).
- Today 11:40 AM
- Business: Hi Priya, it's Kiln Street Studio. Your Saturday class seat is held until 6 PM.
- Business: Want the checkout link? Reply YES
- Customer: YES
- Business: Here you go: kilnstreet.example/pay/2291👍
- Customer: Paid! Do I bring an apron?
- Business: No need, we have aprons. See you Saturday!
ZeroSettle's retention-offers recipe sends three texts with the offer last and a tapback on it (“wait”, “we saved your spot 🥺”, “how about 30% off? reply YES and it's yours”), then the deep link with the confirmation after YES: “Three texts a few seconds apart read like a person typing; one paragraph reads like a campaign.” See recovering paywall abandonment, review requests over iMessage and writing a first iMessage customers reply to.
The code: hold the link until YES
The server stores the link with the opener and sends it when a message.received webhook carries a YES. The signature check is the docs' verifier: HMAC-SHA256 over "<t>." + raw body, a 300-second tolerance and a constant-time compare. Popping the stored link makes a retried webhook harmless, and the link goes out after the webhook has answered, well inside its 10-second timeout.
# ask_then_link.py: run with uvicorn ask_then_link:app --port 8100
import hmac, hashlib, json, os, time
import httpx
from fastapi import BackgroundTasks, FastAPI, Request, Response
app = FastAPI()
BASE = "https://portal.zerosettle.io"
TOKEN, SECRET = os.environ["ZEROSETTLE_TOKEN"], os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
pending: dict[str, str] = {} # E.164 handle -> link (use your database)
def send(to: str, text: str) -> dict:
body = httpx.post(f"{BASE}/api/imessage/send", headers={"Authorization": f"Bearer {TOKEN}"},
json={"to": to, "text": text}, timeout=90).json()
if (body.get("warning") or {}).get("code") == "link_not_tappable":
print(f"link sent too early to {to}") # a template leaked a link
return body
def open_offer(to: str, name: str, link: str) -> None: # your app calls this
pending[to] = link
if send(to, f"Hi {name}, it's Kiln Street Studio. Your Saturday class seat is held until 6 PM.").get("ok"):
send(to, "Want the checkout link? Reply YES") # skipped if the first was refused
def verify(secret: str, header: str, raw_body: 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_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
@app.post("/webhook")
async def webhook(req: Request, tasks: BackgroundTasks):
raw = await req.body()
if not verify(SECRET, req.headers.get("X-ZeroSettle-Signature", ""), raw):
return Response(status_code=401)
ev = json.loads(raw); m = ev.get("message") or {}
answer = (m.get("text") or "").strip(" .!").upper()
if ev.get("event") == "message.received" and answer in ("YES", "Y"):
link = pending.pop(m["handle"], None)
if link:
tasks.add_task(send, m["handle"], f"Here you go: {link}") # tappable now
return {"ok": True}
What ask-then-link costs against the limits
Ask-then-link uses one new contact per person; the link never counts, because the conversation is ongoing by then. On every plan, the unanswered-message rule caps nudges before a reply.
| Message | New contact? | Unanswered messages (every plan) |
|---|---|---|
| Opener and “Reply YES” (a burst within 5 minutes counts as one) | Yes, once | 1 of 2 |
| One nudge if they stay quiet | No | 2 of 2; a third send returns 403 awaiting_reply |
| Their reply | No | Resets the count |
| Your link | No | 1 of 2 |
New contacts per number: Pro 5 a day; Growth 50 a day and 3 per rolling 15 minutes. Both plans allow 2 unanswered messages per contact.
With the hosted AI agent
With ZeroSettle's hosted AI agent (Beta), two kinds of message can reach someone before they reply: the first_message (1–3 texts sent exactly as written; the docs say to end it on a question) and the agent's triggers (by default one follow-up the next morning if they haven't replied). Write the first message yourself with no link in it, and tell each trigger's instruction not to send a link before they reply. With first_message_enabled set to false, the agent writes the opener itself. Put your payment or booking page in the agent's link, with goal set to purchase, trial_start or demo_booked: it is the link the agent invites people to buy, start a trial or book a demo with. See how the hosted AI agent works.
What about link previews?
Don't count on a preview card. The ZeroSettle console shows rich preview cards for URLs, like Messages does, but ZeroSettle makes no promise about what preview the recipient sees, and a link they can't tap yet shows as plain text. Write each link message to make sense as a bare URL: say what it is (“Here's your checkout link:”) and keep it short.
Which ZeroSettle plan does this need?
Any plan: the warning and both patterns work the same on Pro and Growth, and only the limits above differ. 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. Pro is $49/month for one number, with a 30-day money-back guarantee; Growth is from $599/mo for several numbers working as one (plans and pricing). Read the ZeroSettle iMessage API docs, what customers see when a business texts them and more explainers on how ZeroSettle works, or get a number.
Frequently asked questions
Can a link shortener get around it?
No. Any link from an unknown sender shows as plain text, shortened or not; the recipient's iPhone does this, so no sender can avoid it.
Should I resend a message that came back with link_not_tappable?
No. The message already went out, and a resend delivers the same untappable link; on any plan it can also use up one of the two messages allowed before a reply. Ask a link-free question instead.
Does this affect Sign in with iMessage?
No. In Sign in with iMessage the user texts you first, so nothing you send them later is from an unknown sender.
Sources
- ZeroSettle iMessage API docs: “Links in a first message”, “Let them start the conversation”, “Retention offers”, “AI agent” and “Errors, limits, and compliance”. Checked October 12, 2026.
- Screen and filter text messages on iPhone, iPhone User Guide for iOS 26, Apple Support. Checked October 8, 2026.
- Rate Limits and Message Queues, Sendblue Docs. Checked October 8, 2026.
- Is it possible to send an outbound text first?, LoopMessage help center. Checked October 8, 2026.