URI schemes: mailto, tel, sms and WhatsApp

Four of the generators on this site produce a payload that is not a document but an address: a tel: number, a mailto: recipient, an sms: number, or a WhatsApp click-to-chat link. Each is a URI — a short string whose scheme prefix says what kind of thing follows. This guide shows exactly what each builder emits, which published rule it follows, what the generator can and cannot check, and how to read the symbol facts under each worked example. Every payload, encoded character, rejection message, version and module count below is computed at build time by the engine that runs the generators; nothing is typed in by hand. The inputs are fictional — a 555 number and an example.com address — and labelled as such.

The scheme is the whole instruction

A QR code has two independent layers. The payload is the text encoded inside; the symbol is the black-and-white matrix produced to ISO/IEC 18004 by a vendored copy of the reference encoder. For these four types the payload layer is tiny — a scheme, a separator, the data. The generator's job ends when the string is correct. A tel: or mailto: URI names a destination, and the engine adds no instruction to call, send or message. It also validates format, not truth: it cannot know whether a number is reachable or a mailbox read.

The conventions are not ours. The builders were checked against their published sources on 2026-07-20: the ZXing Barcode Contents wiki for the general shapes, RFC 6068 for mailto:, RFC 3966 with E.164 numbers for tel:, RFC 5724 for sms: and its ?body= form, and the WhatsApp Help Center for the wa.me link. The broader picture — how any payload becomes a symbol — is in how a QR code encodes data.

tel: — one number, one spelling

The phone builder emits a tel: URI (RFC 3966) carrying an E.164 global number: a +, then the country code and the subscriber digits with nothing else. The builder normalizes human spellings first. It strips spaces, dots, dashes and parentheses, and it treats a leading 00 as an alias for +. These three inputs —

  • +1 (555) 019-0042
  • +1 555 019 0042
  • 001 555 019 0042

— all produce the same payload, and the build of this page fails if they ever stop doing so:

tel:+15550190042

Two of the things the builder refuses. A number with no country code, such as 555 019 0042, is rejected with the message “Include the country code in international format, e.g. +1 555 123 4567.” A number that is too short, such as +1 55, is rejected with “An international number has 7–15 digits and cannot start with 0 after the country code prefix.” Both messages come from the engine, so the rule you read here is the rule the form applies.

Worked example: tel:

The fictional number +1 (555) 019-0042 normalizes to:

tel:+15550190042

Rendered by the same engine as the phone generator — version 2, 25×25 modules, error correction Q.

mailto: — a recipient and up to two headers

The email builder emits a mailto: URI (RFC 6068). The recipient address comes first; a subject and a body, if you supply them, follow as subject= and body= query parameters. With only an address the payload is as short as it can be:

mailto:bookings@example.com

Add the subject Table for two? and the body Hi! We'd like Friday (7 pm). and the builder percent-encodes both values before appending them:

mailto:bookings@example.com?subject=Table%20for%20two%3F&body=Hi%21%20We%27d%20like%20Friday%20%287%20pm%29.

Inside the address itself RFC 6068 requires %, &, ; and = to be percent-encoded, which the builder does in a single pass so a literal % is encoded exactly once and never twice.

What percent-encoding actually does

Inside a subject, body or ?text= value, only a small set of characters is carried bare; every other character in that value is written as a percent sign and a hex code. For mailto: and sms: values the engine uses a strict RFC 3986 encoder, so only the unreserved set — letters, digits, -, ., _ and ~ — survives bare. The browser's own encodeURIComponent, which the wa.me ?text= value uses, also leaves these reserved characters bare: !, ', (, ) and *. The scheme, the separators and the address around a value are not encoded this way, which is why the payloads above still show a bare :, @, ? and =. The table is computed by calling both encoders on each character:

Character Strict (mailto:, sms:) encodeURIComponent (wa.me ?text=)
space %20 %20
! %21 !
' %27 '
( %28 (
) %29 )
* %2A *
? %3F %3F
& %26 %26
# %23 %23
% %25 %25

Applied to the sample text Hi! (demo) #42?, the strict encoder gives Hi%21%20%28demo%29%20%2342%3F while encodeURIComponent gives Hi!%20(demo)%20%2342%3F. One consequence worth knowing: type the text, never the encoding. If you paste %20 into a subject field hoping for a space, the encoder sees a percent sign and emits %2520.

Worked example: mailto:

Recipient bookings@example.com, subject Table for two?, body Hi! We'd like Friday (7 pm).:

mailto:bookings@example.com?subject=Table%20for%20two%3F&body=Hi%21%20We%27d%20like%20Friday%20%287%20pm%29.

Rendered by the same engine as the email generator — version 7, 45×45 modules, error correction M.

sms: — a number and an optional ?body=

The SMS builder emits an sms: URI (RFC 5724). The number goes through the same E.164 normalization as tel:, so the spelling rules above apply unchanged. On its own:

sms:+15550190042

With the body ROOM 12: towels & tea, please!, the builder appends a ?body= parameter using the strict encoder, because the RFC 5724 grammar does not allow bare sub-delimiters such as ! or ' in the body value:

sms:+15550190042?body=ROOM%2012%3A%20towels%20%26%20tea%2C%20please%21

The number part and the body part are not equally dependable, and the engine says so in a note it ships for the SMS generator to show beside every result: “Pre-filled message support varies by platform: some phones open the composer with the text, others ignore it. The number always works.”

Worked example: sms:

Number +1 555 019 0042, body ROOM 12: towels & tea, please!:

sms:+15550190042?body=ROOM%2012%3A%20towels%20%26%20tea%2C%20please%21

Rendered by the same engine as the SMS generator — version 5, 37×37 modules, error correction M.

WhatsApp — a link, not a scheme

The WhatsApp builder does not emit a URI scheme of its own. It emits the click-to-chat link the WhatsApp Help Center publishes: https://wa.me/ followed by the number, and the rule quoted in the engine's source is to omit any zeroes, brackets or dashes when adding the phone number in international format. The builder goes through E.164 normalization first and then drops the plus sign too, leaving digits only:

https://wa.me/15550190042

Pre-filled text travels as ?text= with URL-encoded text, which is the right-hand column of the table above: the published rule asks only for URL-encoded text, and for that the builder uses the browser's encodeURIComponent rather than the strict RFC 3986 encoder it applies to mailto: and sms: values. With the text Hi! Is the blue bike (demo) still for sale?:

https://wa.me/15550190042?text=Hi!%20Is%20the%20blue%20bike%20(demo)%20still%20for%20sale%3F

Because this payload is an ordinary web link, it behaves differently in the generator's delivery row. “Share to WhatsApp” sends text, so for a wa.me or URL code the engine shares the link itself, which is equivalent to scanning the code. For a tel:, sms: or mailto: code the same control shows this note instead: “WhatsApp sharing sends text, not images — and this code’s content is not a link, so pasting it into a chat would be meaningless. Download the PNG and attach it in WhatsApp instead.”

Worked example: wa.me

Number +1 (555) 019-0042, text Hi! Is the blue bike (demo) still for sale?:

https://wa.me/15550190042?text=Hi!%20Is%20the%20blue%20bike%20(demo)%20still%20for%20sale%3F

Rendered by the same engine as the WhatsApp generator — version 6, 41×41 modules, error correction M.

Reading the symbol facts

Each worked example above was requested at error correction M, and the encoder chose the version — and therefore the number of modules — that fits the payload at that level. The level actually encoded is never lower than requested but may be higher, because the reference encoder raises it whenever doing so does not increase the version. Side by side:

Payload Version Modules With quiet zone EC level
tel: 2 25×25 33×33 Q
mailto: 7 45×45 53×53 M
sms: 5 37×37 45×45 M
wa.me 6 41×41 49×49 M

The “with quiet zone” column adds the 4-module white border the standard requires on every side. The renderer always draws it into every download and offers no way to switch it off. Longer payloads mean more and smaller modules at the same print size. The PNG export presets are 512, 1024, 2048 px; the smallest, 512 px, carries the readout “1.71 in × 1.71 in (43.3 mm × 43.3 mm) at 300 DPI”, and every preset's readout is computed for 300 DPI the same way. Error correction helps a damaged code scan, but print size and contrast still matter. The details are in quiet zone and print size and contrast and inverted codes.

Common misreadings

  • Cropping the quiet zone. The white border in the download is part of the code, not padding around it. Trimming it, or printing the symbol flush against artwork, is the most common self-inflicted scan failure.
  • Inverting the colors. The style panel computes the contrast of your color pair with the WCAG relative-luminance formula and warns separately for light-on-dark codes, which many scanners reject. Dark modules on a light background is the convention the warning protects.
  • A wrong number encodes fine. The builder checks that +1 555 019 0042 is a well-formed international number, not that anyone answers it. A mistyped digit produces a valid, scannable, useless code. The only test is a real device reaching the real destination.
  • Encoding by hand. As shown above, a %20 typed into a subject field becomes %2520. Type plain text and let the builder encode it once.
  • Expecting the code to act. A URI names a destination; the engine adds no instruction to call, send or message.
  • Expecting to edit it later. These are static codes: the data lives in the image itself. They never expire and cannot be held hostage, but they also cannot be edited after printing. See static vs dynamic QR codes.

URI scheme FAQ

Does the generator check that the number or address really exists?

No. The engine validates format, not truth: it checks that the number is a well-formed international number and that the address has the shape of an email address, but it cannot know whether a phone number is reachable or whether anyone reads the mailbox. Treat a freshly generated code as untested until a real device has scanned it and reached you.

Why is a local-format number rejected?

Because the payload string is all a scanner gets, and a number without a country code only means something inside one country. The builder accepts a leading + or the 00 international prefix and rejects anything else with this message: Include the country code in international format, e.g. +1 555 123 4567.

Why is the pre-filled SMS text sometimes missing after a scan?

The ?body= parameter is RFC 5724 syntax, but the engine ships this disclosure and the SMS generator shows it beside every result: Pre-filled message support varies by platform: some phones open the composer with the text, others ignore it. The number always works.

Why does the WhatsApp payload start with https:// rather than a whatsapp: scheme?

Because the published click-to-chat format is a web link: https://wa.me/ followed by the full international number with any zeroes, brackets or dashes omitted; the builder also drops the plus sign, leaving digits only, and an optional ?text= parameter carries URL-encoded text. The generator emits exactly that link, which is also why the delivery row can share a WhatsApp code as a link but offers a download note for tel:, sms: and mailto: codes.

Can one code carry both a phone number and an email address?

Not as URI schemes: the engine encodes exactly one payload string per symbol, and a tel: URI has no room for a mailto: inside it. Either print one code per channel, or use a vCard, which carries TEL and EMAIL fields inside one contact card that the person scanning saves to their address book.

Can I change the number after printing?

No. Every code from this site is static: the data lives in the image itself, with no redirect through our servers, so it never expires and cannot be held hostage, but it also cannot be edited after printing. A new number means a new code.

Build any of these with the phone, SMS, email or WhatsApp generator — each runs in your browser — and for several contact fields in one code see the vCard guide. More guides are listed at /guides/. How the encoding is built and verified is on the methodology page.