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

imessage-sdk

v0.1.3

Published

Provider-neutral TypeScript SDK for iMessage infrastructure

Readme

imessage-sdk

A provider-neutral, type-safe TypeScript conversation layer for iMessage infrastructure.

The normalized v0.1 client is implemented here. Blooio v2 and Photon Cloud are published as separate provider packages so applications install only the provider dependencies they use.

The package is ESM-only and ships JavaScript plus TypeScript declarations.

Install

pnpm add imessage-sdk @imessage-sdk/blooio

Creating a client

Provider factories consume provider configuration and return a complete, concrete provider:

import { blooio } from '@imessage-sdk/blooio';
import { createIMessageClient } from 'imessage-sdk';

const client = createIMessageClient({
  provider: blooio(),
});

Provider options override environment defaults. blooio() reads BLOOIO_API_KEY, BLOOIO_FROM_NUMBER, and BLOOIO_WEBHOOK_SECRET.

connectionId is optional and defaults to the literal "default". Supply one when the application has multiple connections or sender lines:

const namedClient = createIMessageClient({
  connectionId: 'main-line',
  provider: blooio(),
});

namedClient.connectionId;
//          ^? "main-line"

blooio() can be constructed without explicit options for dependency wiring and type tests. If neither options nor environment credentials are available, API operations throw a typed AuthenticationError.

const client = createIMessageClient({
  provider: blooio(),
});

The provider and connection literals are inferred through the client and its results:

client.provider;
//    ^? "blooio"

client.connectionId;
//    ^? "default"

client.providers.blooio.name;
//                       ^? "blooio"

// @ts-expect-error Only the configured provider exists.
client.providers.photon;

const sent = await client.messages.send({
  to: { kind: 'phone', value: '+15551234567' },
  text: 'Hello from imessage-sdk',
});

sent.provider;
//   ^? "blooio"

sent.connectionId;
//   ^? "default"

Attachments and replies

Attachments declare what they contain independently from transport. The core model accepts URLs, Blob, and Uint8Array; Blooio v2 currently accepts only public URLs, while Photon uploads all three source types:

await client.messages.send({
  conversationId: 'provider-conversation-id',
  text: 'Three attachments',
  attachments: [
    {
      kind: 'image',
      source: {
        type: 'url',
        url: 'https://example.com/photo.jpg',
      },
      contentType: 'image/jpeg',
    },
    {
      kind: 'video',
      source: { type: 'url', url: 'https://example.com/clip.mp4' },
      filename: 'clip.mp4',
    },
    {
      kind: 'file',
      source: { type: 'url', url: 'https://example.com/document.pdf' },
      filename: 'document.pdf',
    },
  ],
});

Providers may also support authenticated downloads for inbound attachments that do not have a public URL:

if (client.capabilities.attachments?.download) {
  const data = await client.attachments.download('provider-attachment-id');
}

The normalized result is a Uint8Array. Photon supports this operation; Blooio inbound attachments use their normalized public URL instead.

Replies use the provider-native message ID and optional message-part index:

await client.messages.send({
  conversationId: 'provider-conversation-id',
  text: 'Replying in this thread',
  replyTo: {
    messageId: 'provider-message-id',
    partIndex: 0,
  },
});

Provider encapsulation

Everything implemented by an adapter lives on its provider. The client exposes the exact same instance under its typed provider namespace:

const provider = blooio();
const client = createIMessageClient({
  connectionId: 'main-line',
  provider,
});

client.providers.blooio === provider; // true

await client.providers.blooio.messages.send({
  to: { kind: 'phone', value: '+15551234567' },
  text: 'Send directly through Blooio',
});

The normalized top-level methods remain available:

await client.messages.send({
  to: { kind: 'phone', value: '+15551234567' },
  text: 'Send through the normalized client',
});

They add the connection ID to results, decorate events, verify capability support consistently, and produce normalized SDK errors.

Provider-specific methods are defined directly on the provider. Blooio exposes its status endpoint without adding it to every adapter's normalized contract:

const status = await client.providers.blooio.messages.getStatus({
  conversationId: sent.conversationId,
  messageId: sent.providerMessageId,
});

const linkedNumbers = await client.providers.blooio.numbers.list();

There is no separate extensions object.

Adapter implementation shape

defineProvider() is the generic adapter-authoring helper. It preserves the literal name, capability values, and complete concrete provider type.

Conceptually, provider factories are structured like this:

import { defineProvider } from 'imessage-sdk';

function blooio(options = {}) {
  return defineProvider({
    name: 'blooio' as const,
    capabilities: BLOOIO_CAPABILITIES,
    messages: {
      async send(input) {
        return transport.send(options, input);
      },
    },
    conversations: {
      async open(input) {
        return transport.openConversation(options, input);
      },
    },
    reactions: {
      async add(input) {
        await transport.addReaction(options, input);
      },
      async remove(input) {
        await transport.removeReaction(options, input);
      },
    },
    typing: {
      async start(conversationId) {
        await transport.startTyping(options, conversationId);
      },
    },
    webhooks: {
      verify(request) {
        return transport.verifyWebhook(options, request);
      },
      parse(request) {
        return transport.parseWebhook(options, request);
      },
    },
  });
}

Applications use blooio(options). They do not supply message, conversation, or webhook implementations themselves.

Custom providers

Provider names are not restricted to the built-in adapters. defineProvider() preserves a user-defined provider's literal name, capabilities, and concrete methods:

const custom = defineProvider({
  name: 'my-provider' as const,
  capabilities: MY_PROVIDER_CAPABILITIES,
  messages: {
    send: sendMessage,
    get: getMessage,
    edit: editMessage,
  },
  conversations: {
    open: openConversation,
  },
});

const customClient = createIMessageClient({ provider: custom });

customClient.provider;
//           ^? "my-provider"

// Required because this concrete provider defines it as required.
await customClient.providers['my-provider'].messages.edit(
  { conversationId: 'conversation-id', messageId: 'message-id' },
  { text: 'Corrected' },
);

One provider per client

v0.1 accepts exactly one provider. When an application has multiple providers or sender lines, it creates multiple clients and selects one explicitly:

const sales = createIMessageClient({
  connectionId: 'sales-line',
  provider: blooio(salesOptions),
});

const support = createIMessageClient({
  connectionId: 'support-line',
  provider: photon(supportOptions),
});

A multi-provider client would require explicit routing, sender ownership, idempotency, failure, and fallback policies. If needed later, that should be a separate router layered over multiple single-provider clients.

Conversation IDs

Provider-native conversation IDs are required for provider operations and must be treated as opaque values scoped by provider and connectionId.

If a provider message unexpectedly omits its native conversation ID, the SDK creates a diagnostic fallback from the provider message ID and timestamp:

imsg-sdk-v1-<base64url-json>

Fallback IDs keep normalized inbound messages structurally valid, but they are not routable provider conversation handles. Passing one to send, lookup, reaction, typing, or read operations throws a ValidationError with code non_routable_conversation_id.

Capabilities and lifecycle

The public client keeps a consistent normalized method set. Unsupported operations throw UnsupportedCapabilityError, while capabilities allow runtime feature checks:

if (client.capabilities.messages.edit) {
  await client.messages.edit(
    {
      conversationId: 'provider-conversation-id',
      messageId: 'provider-message-id',
    },
    { text: 'Corrected' },
  );
}

Concrete capability values remain literal types. For example, client.capabilities.messages.edit is typed as false for Blooio.

Providers do not have an initialize() hook. REST adapters are ready after construction, while stream providers connect lazily when subscribed.

client.close() is always available and idempotent. It is a no-op when the provider has no cleanup and releases resources for stream or local transports.

All available normalized v0.1 operations are stable, including client.webhooks, ProviderWebhooks, signed webhook verification, and webhook event normalization.

Blooio operations

The v0.1 Blooio adapter supports text and public URL attachments, inline replies, direct conversations, message lookup and status, reactions, typing start/stop, marking chats as read, and signed webhooks. Group conversations are experimental at the provider level and disabled in the normalized client.

const locator = {
  conversationId: sent.conversationId,
  messageId: sent.providerMessageId,
};

await client.messages.get(locator);
await client.reactions.add({ ...locator, reaction: 'like' });
await client.typing.start(sent.conversationId);
await client.typing.stop(sent.conversationId);
await client.conversations.markRead(sent.conversationId);

Webhook handling verifies X-Blooio-Signature before parsing and returns an array because one provider delivery can represent zero, one, or multiple normalized events:

const events = await client.webhooks.handle(request);

Live integration test

The live test sends real messages and is disabled during normal test runs. Set these environment variables locally:

export BLOOIO_API_KEY="..."
export BLOOIO_TEST_RECIPIENT="+15551234567"
export BLOOIO_FROM_NUMBER="+15557654321" # optional
export BLOOIO_TEST_IMAGE_URL="https://..."
export BLOOIO_TEST_VIDEO_URL="https://..."
export BLOOIO_TEST_FILE_URL="https://..."

pnpm --filter @imessage-sdk/blooio test:integration

It sends three messages and exercises lookup/status, reactions, typing, and mark-read. Webhook verification and parsing are covered deterministically by the mocked test suite; a real inbound webhook requires a public test endpoint.

Photon Cloud operations

Photon authenticates with Spectrum Cloud lazily. A dedicated phone is optional when the project owns exactly one line and required when it owns multiple:

import { photon } from '@imessage-sdk/photon';
import { createIMessageClient } from 'imessage-sdk';

const client = createIMessageClient({
  connectionId: 'photon-main',
  provider: photon(),
});

const line = await client.providers.photon.connection.getLine();

photon() reads PHOTON_PROJECT_ID, PHOTON_PROJECT_SECRET, PHOTON_PHONE_NUMBER, and PHOTON_WEBHOOK_SECRET. Explicit options override the environment.

The provider renews temporary line credentials internally and supports direct chats, all attachment source types, replies, lookup, reactions, typing, mark-read, and signed Spectrum webhooks. Normalized editing, deletion, group conversations, and event streaming are conservatively disabled in v0.1. Their capabilities are false, and normalized calls throw UnsupportedCapabilityError.

Photon streaming remains available as an experimental provider-level API. Cursors are durable numeric event sequences represented as strings:

for await (const event of client.providers.photon.events.subscribe({
  cursor: '123',
})) {
  console.log(event.providerEventId, event.type);
}

await client.close();

Group-conversation implementations also remain experimental at the provider level. Normalized client.conversations.open(...) accepts one participant in v0.1 and rejects multiple participants with UnsupportedCapabilityError.

Photon live integration test

export PHOTON_PROJECT_ID="..."
export PHOTON_PROJECT_SECRET="..."
export PHOTON_PHONE_NUMBER="+15557654321" # optional with one line
export PHOTON_TEST_RECIPIENT="+15551234567"
export PHOTON_TEST_IMAGE_URL="https://..."
export PHOTON_TEST_VIDEO_URL="https://..."
export PHOTON_TEST_FILE_URL="https://..."

pnpm --filter @imessage-sdk/photon test:integration

On Photon Free and Pro shared lines, add the recipient under the project's Users tab first. New contacts have a temporary reply allowance until they have sent enough inbound messages, so use an opted-in recipient and avoid repeated live runs against a fresh contact.

The outbound test exercises line discovery, idempotency, attachments, replies, lookup, reactions, typing, mark-read, and cleanup. To test the experimental persistent stream, run the command below and send an iMessage to the line within 60 seconds:

pnpm --filter @imessage-sdk/photon test:integration:stream

v0.1 scope and stability

The initial model includes text, URL/blob/byte attachments, thread replies, direct conversations, statuses, reactions, typing, typed capabilities, and typed errors. These available normalized operations, including webhooks, are stable. Photon streams and both providers' group implementations remain experimental provider-level APIs.

It intentionally excludes FaceTime, polls, location sharing, contacts, message effects, scheduling, provisioning, and automatic provider fallback.

License

MIT

Release history is tracked in the repository changelog.