--- title: Open iMessage from your website | Sendblue Docs description: Add a button that opens iMessage with a pre-filled number and message | iMessage for Business --- This guide only works on devices that can send a text: iPhone, iPad, Android, and Mac. Visitors on desktop Windows or Linux will see the button but clicking it does nothing. An `sms:` link opens the visitor’s Messages app with the phone number and message already filled in. On an iPhone or Mac texting an iMessage-enabled number, the thread opens blue. This guide gives you two drop-in patterns: 1. **Plain button** — one `` tag, no form. 2. **Progressive form** — phone first, then first/last name and submit. Same flow Sendblue uses on its demo page. ## How the `sms:` URL works [`sms:+15551234567&body=Hello%20from%20Sendblue`](sms:+15551234567\&body=Hello%20from%20Sendblue) (Click it — on an Apple device this opens Messages with both fields filled in.) - The number must be in [E.164 format](https://en.wikipedia.org/wiki/E.164) (the `+` is an international-dialing prefix that virtually every API, including Sendblue, requires). - The body must be URL-encoded — use `encodeURIComponent()` in JavaScript. - **Separator quirk.** [RFC 5724](https://www.rfc-editor.org/rfc/rfc5724) says the separator should be `?body=`, and Android follows it. iOS has always used `&body=` instead. The hybrid `?&body=` is a community workaround that reportedly works on both, but no spec defines it — for iPhone visitors, use `&body=`. Apple’s [archived URL Scheme Reference](https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/SMSLinks/SMSLinks.html) for `sms:` says the URL must not include a body. Pre-filled bodies are *de facto* behavior — they’ve worked across iOS versions for years, but Apple has never officially documented them. Stable in practice, not guaranteed. ## Version 1: Plain button **Live demo** — click to open Messages with the number and body prefilled: [ Message us on iMessage](sms:+15551234567\&body=Hi%21%20I%27d%20like%20to%20learn%20more%20about%20Sendblue.) Drop this into any HTML page. Replace the number and message with your own. ``` Message us on iMessage ``` On iOS and macOS, Messages opens with your number and message prefilled. On Android, the default SMS app opens. Only want to show the button on Apple devices? Check the user agent client-side: `/iPhone|iPad|iPod|Mac/i.test(navigator.userAgent)`. Or run the visitor’s number through a [Sendblue lookup](/api/resources/lookups/methods/lookup_number/index.md) to check for iMessage support. Only want to show it on devices that can actually send a text? That’s a broader set — iPhone, iPad, Android, and Macs running macOS 10.10+. A check like `/iPhone|iPad|iPod|Android|Macintosh/i.test(navigator.userAgent)` covers them all and filters out desktop Windows and Linux where the `sms:` link does nothing useful. ## Version 2: Progressive form Ask for a phone number first. Once the visitor has typed a few digits, reveal the name fields and a Submit button — then validate the full number on submit before opening iMessage with a personalized message. One nice thing about the form is that it gets browser autofill for free. The `autocomplete="tel"`, `"given-name"`, and `"family-name"` attributes let Chrome, Safari, and most mobile browsers fill everything with a single tap from the visitor’s saved contact info. The prefilled iMessage body can then be personalized (“Hi, I’m Ada…”) using what they entered. **Live demo** — type a phone number (at least 5 digits) to reveal the name fields and submit button: Your phone number 🇺🇸+1🇺🇸 US +1 (US) (555) 000-0000 First name Last name Open iMessage The code below uses [`libphonenumber-js`](https://www.npmjs.com/package/libphonenumber-js) for parsing, validation, and as-you-type formatting — no phone-input wrapper component. The phone field is a plain `` paired with a native ` setCountry(e.target.value as CountryCode)} > {COUNTRIES.map((c) => ( ))} { if (digits && !phoneValid) setPhoneError(true); }} required /> {phoneError && Invalid phone number}
); } ``` ### Why it’s structured this way - **Plain ``.** The phone input has no library wrapper, which is what Chrome’s profile-autofill scanner expects — clicking it offers the whole contact (phone + first + last) in one tap. - **`formatIncompletePhoneNumber` for display.** We store raw national digits in state and format them at render time. Pasting `+19173599290` parses cleanly; typing `9173599290` formats to `(917) 359-9290` as the visitor goes. - **Progressive reveal without breaking autofill.** The name fields and Submit live inside a wrapper that sits at `position: absolute; left: -99999px` on first load — still rendered with real dimensions, so Chrome indexes them for autofill, but visually hidden until the visitor has typed a few digits. The wrapper flips to `position: static` when the reveal opens. ### Minimal styles ``` .imessage-form { display: flex; flex-direction: column; gap: 16px; max-width: 460px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } .field { display: flex; flex-direction: column; gap: 6px; font-size: 14px; color: #374151; } .phone-field { display: flex; align-items: stretch; height: 40px; border: 1px solid #d1d5db; border-radius: 12px; overflow: hidden; } .phone-field:focus-within { border-color: #007aff; box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15); } .country-pill { position: relative; display: inline-flex; align-items: center; gap: 6px; padding: 0 12px; border-right: 1px solid #d1d5db; cursor: pointer; user-select: none; } .country-pill select { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; appearance: none; -webkit-appearance: none; border: 0; } .phone-field input { flex: 1; min-width: 0; padding: 0 14px; border: 0; background: transparent; font-size: 16px; outline: none; } .field input[type="text"] { height: 40px; padding: 0 14px; border: 1px solid #d1d5db; border-radius: 12px; font-size: 16px; } .name-row { display: flex; gap: 12px; } .name-row .field { flex: 1; } .error { color: #dc2626; font-size: 13px; } /* Keep the name fields rendered at real dimensions off-screen so browser autofill still indexes them, even before the progressive reveal opens. */ .reveal { position: absolute; left: -99999px; opacity: 0; pointer-events: none; display: flex; flex-direction: column; gap: 16px; transition: opacity 0.22s ease; } .reveal.is-open { position: static; opacity: 1; pointer-events: auto; } .submit-btn { height: 40px; padding: 0 24px; background: linear-gradient(180deg, #34aadc 0%, #007aff 100%); color: white; border: 0; border-radius: 12px; font-size: 15px; font-weight: 600; cursor: pointer; } .submit-btn:disabled { opacity: 0.5; cursor: not-allowed; } ``` ### Optional: capture the contact before opening iMessage In `handleSubmit`, `POST` the phone and name to your backend or CRM **before** setting `window.location.href`. That way you keep the contact’s data even if the visitor never hits send in Messages. ``` await fetch("/api/contacts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone, firstName, lastName }), }); window.location.href = `sms:${SENDBLUE_NUMBER}&body=${encodeURIComponent(body)}`; ``` ## Gotchas - **Desktop browsers on non-Apple machines** won’t do anything useful with `sms:` links. Detect the platform and hide the button, or fall back to a different channel. - **Query-parameter separator.** See the RFC vs. iOS quirk above — if you want one link to work everywhere, `?&body=` is the workaround most sites use, but it isn’t standardized. - **Keep the prefilled body short.** Browsers and mobile WebViews enforce URL-length limits (often a few thousand characters), and very long encoded URLs can fail or be truncated before they ever reach the Messages app. A short, friendly opener is more reliable than a paragraph.