An SMS QR code should do one thing: someone points a camera at it, their messaging app opens with your number and a message already typed, and they hit send.
Most of them do not do this. They work on the phone the person testing happened to be holding, get printed on five hundred flyers, and quietly fail for half of everyone who scans them.
Here is why, and what to encode instead.
The sms: URL scheme
The scheme itself is simple:
sms:+15551234567
Scan that and Messages opens, addressed to the number. No message body, but it works everywhere.
The trouble starts when you want to prefill what they send.
The syntax split nobody warns you about
To prefill the body you add a body parameter. And here the two platforms
diverge:
iOS: sms:+15551234567&body=Hi%20there
Android: sms:+15551234567?body=Hi%20there
Note the separator. iOS has historically wanted &; Android wants ?. A link
hard-coded with one is not reliably right on the other.
Worse, the whole feature is undocumented on iOS. Apple's archived URL Scheme Reference states the URL must not include a body at all. It has worked anyway for many years, which is not the same as being guaranteed to keep working.
You will find a widely-circulated hybrid — sms:+1555...?&body= — that claims to
satisfy both. It often does. It is also a guess about undocumented behaviour on
two independently-changing platforms, which is a poor thing to print onto
something you cannot take back.
The real problem: print is permanent
Software has an escape hatch. A broken link on a website is a five-minute fix.
A QR code on a shop window, a receipt roll, a vehicle wrap, or ten thousand flyers has no escape hatch. Whatever you encoded is what it does, for as long as that object exists.
So the rule is: never encode anything device-specific into a printed QR code.
What to encode instead
Encode an HTTPS URL you control, and resolve the platform difference on your server at scan time.
https://go.senderz.com/a7k2np
When that is opened, the server reads the User-Agent, decides which sms: form
that device wants, and redirects:
iPhone → sms:+15551230000&body=Hi!%20%5B%23a7k2np%5D
Android → sms:+15551230000?body=Hi!%20%5B%23a7k2np%5D
Desktop → a page showing the number and message to send by hand
Three properties fall out of this that the direct approach cannot offer:
The syntax stays fixable. If Apple changes behaviour in a future iOS, you change one line on the server. Every code already printed keeps working.
You get scan analytics. A direct sms: code is invisible — you have no idea
how many people scanned it, only how many completed. With a redirect you see
both, and the gap between them tells you whether the problem is placement or
copy.
Desktop and in-app browsers stop being dead ends. Instagram and X in-app
browsers frequently block sms: handoff entirely. A direct code just fails
silently. A redirect can serve a page with the number and message written out.
Use a 302, not a 301. A permanent redirect gets cached by browsers, which re-freezes exactly the thing the indirection exists to keep changeable.
Practical details for the code itself
Keep the payload short. Fewer characters means fewer modules, which means a
code that scans reliably from further away and survives being printed small. This
is a real argument for a short domain: go.senderz.com/a7k2np is meaningfully
easier to scan than a long URL with query parameters.
Error correction level M is right for most uses. L is too fragile for anything that will be handled; H bloats the code for no benefit unless you are overlaying a logo.
Test the physical object, not the screen. Print it at the size you will actually use, then scan from the distance a customer will actually stand. A code that scans perfectly on a monitor can fail on a receipt.
ASCII only in the prefilled message. A non-ASCII character flips an SMS from GSM-7 to UCS-2 encoding, halving the segment size and risking mangling. Emoji in an opener is a real-world failure, not a theoretical one.
Generating one
The API returns the link; render the QR from it client-side rather than generating images on your server.
curl -X POST https://api.senderz.com/v1/invites \
-H "Authorization: Bearer $SENDERZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label": "Counter sign", "opener": "Hi!"}'
{
"data": {
"ref": "a7k2np",
"link": "https://go.senderz.com/a7k2np",
"number": "+15551230000",
"opener": "Hi!"
}
}
Encode link. That string is what belongs on the poster.
FAQ
Questions
Can I just use a generic QR generator with an sms: link?
You can, and it will work on one platform and be unreliable on the other. For a screen that is a fixable mistake. For anything printed it is not, which is the entire argument for encoding a URL you control instead.
Does the scanner app matter?
Less than it used to. The native camera on both iOS and Android handles sms: and https: links directly. Third-party scanner apps sometimes open links in their own in-app browser, which is another reason an HTTPS URL with a fallback page is safer than a raw sms: URI.
Why does the prefilled message have a code in it?
So the inbound message can be attributed to the right business and the right placement. On a shared sending number it is the only thing distinguishing which account a stranger's first message belongs to.
What size should I print it?
As a rule of thumb the code should be at least a tenth of the scanning distance — 3cm for arm's length, 20cm for across a room. Shorter encoded URLs allow smaller codes, which is why payload length is worth caring about.