npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

npm TypeScript dependencies license Discord

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/uri

Basic 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 InvalidPhoneNumberError

Recipient 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, and prompt: true to use the -prompt scheme 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 support body.
  • SMS URIs follow RFC 5724 (sms:+phone?body=text), not iOS-specific variants.
  • Groups are supported for iMessage and SMS only. Pass to as 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%203

Options

| 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=yo

Options

| 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:+14155551234

Options

| 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. |


WhatsApp

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=hi

Options

| 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=durov

Options

| 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:+14155551234

Parsing 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>.ts

Per-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

MIT


Author

Photon