@photon-ai/uri
v0.2.1
Published
Deep link builder for iMessage, SMS, FaceTime, WhatsApp, and Telegram. TypeScript, zero deps.
Readme
@photon-ai/uri
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.
Features
| 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 |
Quick Start
Installation
bun add @photon-ai/uri
npm install @photon-ai/uriBasic usage
import { 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 👋" }Core Concepts
Phone numbers
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 InvalidPhoneNumberErrorRecipient types per platform
| Platform | Phone | Email | 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 |
| WhatsApp | yes | — | — | — | Strips + in URL path |
| Telegram | yes | — | yes | — | Pre-filled body only for usernames |
Platform quirks
- 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.
Body encoding
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"iMessage
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: "[email protected]", body: "hello" });
// imessage://[email protected]?body=hello
// Group — phones and emails can be mixed
createIMessageLink({
to: ["+14155551234", "+14155556789", "[email protected]"],
body: "standup at 3",
});
// imessage://+14155551234,+14155556789,[email protected]?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 |
SMS
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 |
FaceTime
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: "[email protected]", mode: "audio" });
// facetime-audio:[email protected]
// 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" |
Telegram
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" |
Unified dispatcher
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:+14155551234Parsing URIs
Example: 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:[email protected]");
// { platform: "facetime", to: "[email protected]", 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.
Error handling
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
}
}Examples
Run any example with Bun:
bun run examples/<filename>.tsPer-platform
| 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 |
Advanced
| File | Description |
| --- | --- |
| unified.ts | Unified createLink dispatcher |
| parse.ts | Parse URIs back with parseLink |
License
Author
Photon
