Generate URI links for iMessage, SMS, FaceTime, WhatsApp, and Telegram. Zero dependencies, fully typed.
A tiny, zero-dependency TypeScript library for generating correct, encoded URI strings that open native messaging apps with pre-filled recipients and message bodies.
| Feature | Description | Method | Example |
|---|---|---|---|
| iMessage link | Build an imessage:// URI for phone or email |
createIMessageLink() |
imessage.ts |
| SMS link | Build an RFC 5724 sms: URI |
createSmsLink() |
sms.ts |
| FaceTime link | Build facetime: / facetime-audio: URI |
createFaceTimeLink() |
facetime.ts |
| WhatsApp link | Build wa.me universal or whatsapp:// scheme link |
createWhatsAppLink() |
whatsapp.ts |
| Telegram link | Build t.me universal or tg:// scheme link |
createTelegramLink() |
telegram.ts |
| Unified dispatcher | One function for all platforms via a discriminated union | createLink() |
unified.ts |
| Parse URIs back | Reverse operation: any supported URI to { platform, to, body, ... } |
parseLink() |
parse.ts |
bun add @photon-ai/uri
npm install @photon-ai/uriimport { createIMessageLink, createLink, parseLink } from "@photon-ai/uri";
createIMessageLink({ to: "+14155551234", body: "hello" });
// imessage://+14155551234?body=hello
createLink({ platform: "sms", to: "+14155551234" });
// sms:+14155551234
parseLink("imessage://+14155551234?body=hi%20%F0%9F%91%8B");
// { platform: "imessage", to: "+14155551234", body: "hi π" }All phone inputs must be in E.164 format: a leading +, country code, and subscriber number with no spaces or formatting. The library normalizes common formatting (spaces, dashes, parentheses, dots) internally, but the + prefix is required.
import { assertE164 } from "@photon-ai/uri";
assertE164("+1 (415) 555-1234"); // "+14155551234"
assertE164("415-555-1234"); // throws InvalidPhoneNumberError| Platform | Phone | Username | Group | Notes | |
|---|---|---|---|---|---|
| iMessage | yes | yes | β | yes | Phones and emails can be mixed in a group |
| SMS | yes | β | β | yes | Phone only; RFC 5724 comma-separated list |
| FaceTime | yes | yes | β | β | Email works for Apple IDs |
| yes | β | β | β | Strips + in URL path |
|
| Telegram | yes | β | yes | β | Pre-filled body only for usernames |
- FaceTime has no body support. Pass
mode: "audio"for audio calls, andprompt: trueto use the-promptscheme variants that ask for confirmation before dialing. - WhatsApp strips the
+from phone numbers in the URL; you still pass E.164 with+in options. - Telegram phone links (
t.me/+...) do not support pre-filled message text; only username links supportbody. - SMS URIs follow RFC 5724 (
sms:+phone?body=text), not iOS-specific variants. - Groups are supported for iMessage and SMS only. Pass
toas an array; duplicates dedupe after normalization. A single-element array produces the same output as the string form.
Message bodies embedded in query strings use encodeBody() (and round-trip with decodeBody()). Reserved characters, newlines, and Unicode are percent-encoded per RFC 3986.
import { decodeBody, encodeBody } from "@photon-ai/uri";
encodeBody("hi π"); // "hi%20%F0%9F%91%8B"
decodeBody(encodeBody("a\nb\nc")); // "a\nb\nc"Example: imessage.ts
Builds imessage:// URIs. Supports both phone numbers (E.164) and email addresses as recipients, including a mixed group of recipients.
import { createIMessageLink } from "@photon-ai/uri";
createIMessageLink({ to: "+14155551234" });
// imessage://+14155551234
createIMessageLink({ to: "+14155551234", body: "hi π" });
// imessage://+14155551234?body=hi%20%F0%9F%91%8B
createIMessageLink({ to: "user@example.com", body: "hello" });
// imessage://user@example.com?body=hello
// Group β phones and emails can be mixed
createIMessageLink({
to: ["+14155551234", "+14155556789", "user@example.com"],
body: "standup at 3",
});
// imessage://+14155551234,+14155556789,user@example.com?body=standup%20at%203Options
| Field | Type | Required | Description |
|---|---|---|---|
to |
string | string[] |
yes | One recipient, or a list. Each element is a phone (E.164) or email. Duplicates dedupe after normalization. |
body |
string |
no | Pre-filled message text |
Example: sms.ts
Builds RFC 5724 sms: URIs. Recipients must be E.164 phone numbers. Multiple recipients are supported per RFC 5724 via a comma-separated list.
import { createSmsLink } from "@photon-ai/uri";
createSmsLink({ to: "+14155551234" });
// sms:+14155551234
createSmsLink({ to: "+14155551234", body: "Hello" });
// sms:+14155551234?body=Hello
// Group β RFC 5724 comma-separated list
createSmsLink({ to: ["+14155551234", "+14155556789"], body: "yo" });
// sms:+14155551234,+14155556789?body=yoOptions
| Field | Type | Required | Description |
|---|---|---|---|
to |
string | string[] |
yes | One E.164 phone, or a list. Duplicates dedupe after normalization; first-occurrence order preserved. |
body |
string |
no | Pre-filled message text |
Example: facetime.ts
Builds facetime: (video) or facetime-audio: (audio) URIs. There is no message body; recipients are phone (E.164) or email (Apple ID).
import { createFaceTimeLink } from "@photon-ai/uri";
createFaceTimeLink({ to: "+14155551234" });
// facetime:+14155551234
createFaceTimeLink({ to: "+14155551234", mode: "audio" });
// facetime-audio:+14155551234
createFaceTimeLink({ to: "user@icloud.com", mode: "audio" });
// facetime-audio:user@icloud.com
// Prompt variants β iOS asks the user to confirm before dialing
createFaceTimeLink({ to: "+14155551234", prompt: true });
// facetime-prompt:+14155551234
createFaceTimeLink({ to: "+14155551234", mode: "audio", prompt: true });
// facetime-audio-prompt:+14155551234Options
| Field | Type | Required | Description |
|---|---|---|---|
to |
string |
yes | E.164 phone or email |
mode |
"video" | "audio" |
no | Defaults to "video" |
prompt |
boolean |
no | Emit the -prompt scheme variant that shows a confirmation dialog before dialing. Recommended for public-web-page links. Defaults to false. |
Example: whatsapp.ts
Builds https://wa.me/... (universal) or whatsapp://send?... (scheme) links. Phone numbers are normalized to E.164 in options; the + is omitted in the generated path or phone query value.
import { createWhatsAppLink } from "@photon-ai/uri";
createWhatsAppLink({ to: "+14155551234", body: "Hello" });
// https://wa.me/14155551234?text=Hello
createWhatsAppLink({ to: "+14155551234", variant: "scheme", body: "hi" });
// whatsapp://send?phone=14155551234&text=hiOptions
| Field | Type | Required | Description |
|---|---|---|---|
to |
string |
yes | E.164 phone |
body |
string |
no | Pre-filled message text |
variant |
"universal" | "scheme" |
no | Defaults to "universal" |
Example: telegram.ts
Builds https://t.me/... (universal) or tg://resolve?... (scheme) links. Recipients are either E.164 phones (+... or digits) or valid usernames (@ prefix is stripped). Pre-filled body is supported for usernames only; phone links throw UnsupportedFeatureError if body is non-empty.
import { createTelegramLink } from "@photon-ai/uri";
createTelegramLink({ to: "durov", body: "hi π" });
// https://t.me/durov?text=hi%20%F0%9F%91%8B
createTelegramLink({ to: "+14155551234" });
// https://t.me/+14155551234
createTelegramLink({ to: "durov", variant: "scheme" });
// tg://resolve?domain=durovOptions
| Field | Type | Required | Description |
|---|---|---|---|
to |
string |
yes | E.164 phone, digits, or username |
body |
string |
no | Pre-filled text (username only) |
variant |
"universal" | "scheme" |
no | Defaults to "universal" |
Example: unified.ts
createLink() dispatches to the correct builder from a single options object. The platform field discriminates the union so each platform only accepts its own options (for example, mode is only valid for FaceTime).
import { createLink } from "@photon-ai/uri";
createLink({ platform: "imessage", to: "+14155551234", body: "hi" });
// imessage://+14155551234?body=hi
createLink({ platform: "facetime", to: "+14155551234", mode: "audio" });
// facetime-audio:+14155551234Example: parse.ts
parseLink() is the inverse of the builders for supported schemes: imessage://, sms:, facetime: / facetime-audio: / facetime-prompt: / facetime-audio-prompt:, https://wa.me/, https://t.me/, whatsapp://send, and tg://resolve. It returns a ParsedLink discriminated union with platform, to, optional body, and platform-specific fields (mode and prompt for FaceTime, variant for WhatsApp and Telegram).
For iMessage and SMS, to is typed as string | string[]: a single-recipient URI parses to a plain string (unchanged), and a multi-recipient URI parses to an array in original order.
import { parseLink } from "@photon-ai/uri";
parseLink("sms:+14155551234?body=hello");
// { platform: "sms", to: "+14155551234", body: "hello" }
parseLink("sms:+14155551234,+14155556789?body=yo");
// { platform: "sms", to: ["+14155551234", "+14155556789"], body: "yo" }
parseLink("facetime-audio:user@icloud.com");
// { platform: "facetime", to: "user@icloud.com", mode: "audio", prompt: false }
parseLink("facetime-prompt:+14155551234");
// { platform: "facetime", to: "+14155551234", mode: "video", prompt: true }Round-trips are stable for values produced by the builders: build with a given option object, parse, then build again with the parsed fields and you get the same string where applicable.
Errors extend MessageUriError and are safe to catch with instanceof:
| Class | When |
|---|---|
InvalidPhoneNumberError |
Phone input is not valid E.164 after normalization |
InvalidRecipientError |
Email or Telegram username validation failed |
UnsupportedFeatureError |
Valid recipient but feature not available (e.g. Telegram phone + body) |
UnrecognizedLinkError |
parseLink() input is not a supported URI shape |
import {
InvalidPhoneNumberError,
parseLink,
UnrecognizedLinkError,
} from "@photon-ai/uri";
try {
parseLink("https://example.com/");
} catch (err) {
if (err instanceof UnrecognizedLinkError) {
// handle unknown link
}
}Run any example with Bun:
bun run examples/<filename>.ts| File | Description |
|---|---|
| imessage.ts | Build imessage:// URIs |
| sms.ts | Build RFC 5724 sms: URIs |
| facetime.ts | Build facetime: / facetime-audio: URIs |
| whatsapp.ts | Build wa.me / whatsapp:// URIs |
| telegram.ts | Build t.me / tg:// URIs |
| File | Description |
|---|---|
| unified.ts | Unified createLink dispatcher |
| parse.ts | Parse URIs back with parseLink |
Photon