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

sendimessage

v0.1.0

Published

SendiMessage SDK — send iMessage/SMS through the SendiMessage API

Readme

SendiMessage SDK for JavaScript / TypeScript

Version License: MIT Node

The official JavaScript / TypeScript client for the SendiMessage API — send and receive iMessage and SMS programmatically. Zero runtime dependencies, full TypeScript definitions included.

Server-side only. The API key pair grants full messaging access to your account — never ship it to a browser or mobile app.

Contents

Requirements

  • Node.js ≥ 18.13 (uses the built-in fetch)
  • A SendiMessage account with an API key pair

Installation

npm install sendimessage

Or straight from GitHub:

npm install github:sendimessage/sendimessage-js

Authentication

Create a key pair in the account portal under API credentials. You get a key_id and a secret — the secret is shown once, store it securely (e.g. in your secret manager or environment variables). Every request the SDK makes carries them as the X-API-Key / X-API-Secret headers.

import SendImessage from 'sendimessage';

const client = new SendImessage({
  apiKey: process.env.SENDIMESSAGE_KEY,
  apiSecret: process.env.SENDIMESSAGE_SECRET,
});

You can verify a key pair at any time with GET /me (curl example in the account portal).

Quick start

import SendImessage from 'sendimessage';

const client = new SendImessage({
  apiKey: process.env.SENDIMESSAGE_KEY,
  apiSecret: process.env.SENDIMESSAGE_SECRET,
});

const msg = await client.sendMessage({
  number: '+15551234567',
  content: 'Hello from SendiMessage!',
});

console.log(msg.message_handle, msg.status); // "…" "QUEUED"

The API delivers over iMessage first and falls back to SMS when the recipient is not reachable over iMessage.

Usage

Sending messages

const msg = await client.sendMessage({
  number: '+15551234567',   // E.164 phone number, or an email for iMessage
  content: 'Your order has shipped 🎉',
});

If your account has more than one line (sending number), route via a specific one with lineHandle:

const lines = await client.listLines();
await client.sendMessage({
  number: '+15551234567',
  content: 'Hi!',
  lineHandle: lines.lines[0].line_handle,
});

Sending media

Upload the file first — uploadMedia returns a public URL you pass as mediaUrl. Images, video, audio and PDF are supported, up to 50 MiB.

const url = await client.uploadMedia('./photo.jpg');           // path
// or: await client.uploadMedia(buffer, 'photo.jpg');          // Uint8Array/Blob + filename

await client.sendMessage({ number: '+15551234567', mediaUrl: url });

content is optional when mediaUrl is set; provide both to send a caption with the attachment. Any public http(s) URL also works as mediaUrl.

Scheduling and status callbacks

await client.sendMessage({
  number: '+15551234567',
  content: 'Reminder: appointment at 3pm',
  scheduledAt: '2026-08-10T14:30:00Z',                  // ISO 8601, deliver later
  statusCallback: 'https://example.com/hooks/status',   // POSTed the final status
});

Message status and history

Every send returns a message_handle. Poll it — or better, use a webhook / statusCallback:

const status = await client.getStatus(msg.message_handle);
console.log(status.status); // QUEUED | SENT | ERROR

History is cursor-paginated: pass next_before from one page as before on the next. next_before is null on the last page.

let page = await client.listMessages({ number: '+15551234567', limit: 50 });
while (true) {
  for (const m of page.messages) console.log(m.date_sent, m.is_outbound ? '→' : '←', m.content);
  if (!page.next_before) break;
  page = await client.listMessages({ number: '+15551234567', limit: 50, before: page.next_before });
}

Filters: number, direction ('in' | 'out'), lineHandle, conversationHandle, service, since, until, limit.

const one = await client.getMessage(handle); // single history message

iMessage lookup

Check whether a number is reachable over iMessage before sending:

const result = await client.lookup('+15551234567');
console.log(result.service); // "iMessage" | "SMS"
console.log(result.cached);  // true when answered from the capability cache

Lookups are answered by a live Apple device, so the API is cache-first: a fresh cached answer returns immediately, otherwise the question is queued and the call waits briefly for a device. If no answer arrives in time you get a pending response — retry shortly; a 503 means no device is currently online for your account.

Contacts and opt-out

Contacts are your address book: names shown in conversations, the detected service (iMessage/SMS) and the opt-out flag.

await client.createContact({ number: '+15551234567', first_name: 'Jane', last_name: 'Doe' });
await client.updateContact('+15551234567', { company_name: 'Acme Inc.' });
const contact = await client.getContact('+15551234567'); // number or contact_handle
const all = await client.listContacts({ limit: 100 });
await client.deleteContact('+15551234567');
await client.recheckContact('+15551234567'); // re-run the iMessage/SMS check now

Opt-out is enforced on every send path — a send to an opted-out number fails with HTTP 422. Inbound STOP / UNSUBSCRIBE / CANCEL / END / QUIT opts the sender out automatically; START / UNSTOP / YES opts back in. You can also manage it explicitly:

await client.setOptOut('+15551234567', true);   // opt out
await client.setOptOut('+15551234567', false);  // opt back in

Lines and call forwarding

A line is a sending identity (phone number or iMessage address) on your account.

const { lines } = await client.listLines();
await client.updateLine(lineHandle, { label: 'Support line', isActive: true });

Calls to your line's number can be forwarded to any number you choose. Forwarding is configured by the SendiMessage operator, so changes are asynchronous — the API answers 202 and the request shows as pending until fulfilled:

await client.setCallForwarding(lineHandle, '+15559876543'); // request forwarding
await client.setCallForwarding(lineHandle, null);           // request turning it off
const state = await client.getCallForwarding(lineHandle);   // current + pending

Conversations

Conversations group the message history per contact:

const convos = await client.listConversations({ limit: 20 });
const msgs = await client.listConversationMessages(conversationHandle, { limit: 50 });
await client.markConversationRead(conversationHandle);

Webhooks

Get pushed events instead of polling. Events:

| Event | Fired when | |---|---| | receive | An inbound message arrives | | outbound | An outbound message reaches its final status (sent/failed) | | line_blocked | A line was blocked by Apple (sends answer 422 until unblocked) | | line_unblocked | The block was lifted |

await client.createWebhook({
  url: 'https://example.com/hooks/sendimessage',
  events: ['receive', 'outbound'],       // default: every event
  secret: process.env.WEBHOOK_SECRET,    // enables the signature header
});

const hooks = await client.listWebhooks();
await client.replaceWebhooks([...]);     // swap the whole set atomically
await client.deleteWebhook('https://example.com/hooks/sendimessage');

When the webhook has a secret, every delivery carries an X-SMSBridge-Signature header — the hex HMAC-SHA256 of the raw request body. Always verify it:

import express from 'express';
import { verifyWebhookSignature } from 'sendimessage';

const app = express();

// The raw body is required — re-serialized JSON differs byte-for-byte.
app.post('/hooks/sendimessage', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyWebhookSignature({
    secret: process.env.WEBHOOK_SECRET,
    rawBody: req.body,
    signature: req.get('X-SMSBridge-Signature') ?? '',
  });
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  // ... handle event
  res.sendStatus(200);
});

Error handling

Every non-2xx response throws SendImessageError with status (HTTP code), message (API error text) and body (the parsed response).

import SendImessage, { SendImessageError } from 'sendimessage';

try {
  await client.sendMessage({ number: '+15551234567', content: 'hi' });
} catch (err) {
  if (err instanceof SendImessageError) {
    console.error(err.status, err.message);
  } else {
    throw err; // network error etc.
  }
}

Common statuses:

| Status | Meaning | |---|---| | 401 | Bad or missing key pair | | 404 | Unknown handle (message, contact, line, conversation) | | 422 | Validation failed — including sends to an opted-out contact or via a blocked line | | 429 | Rate limited — back off and retry | | 503 | Lookup: no device online for your account |

Configuration

const client = new SendImessage({
  apiKey: '…',
  apiSecret: '…',
  baseUrl: 'https://api.sendimessage.com/v1', // default; override for testing
});

API reference

| Method | Endpoint | |---|---| | sendMessage(params) | POST /send-message | | getStatus(handle) | GET /status | | listMessages(params) | GET /v2/messages | | getMessage(handle) | GET /v2/messages/{handle} | | uploadMedia(file, filename?) | POST /media | | lookup(number) | GET /lookup | | listContacts(params?) | GET /contacts | | createContact(props) | POST /contacts | | getContact(number) | GET /contacts/{number} | | updateContact(number, props) | PUT /contacts/{number} | | deleteContact(number) | DELETE /contacts/{number} | | setOptOut(number, optedOut?) | POST /contacts/opt-out | | recheckContact(number) | POST /contacts/{number}/lookup | | listLines() | GET /lines | | updateLine(handle, props) | PUT /lines/{handle} | | getCallForwarding(handle) | GET /lines/{handle}/call-forwarding | | setCallForwarding(handle, number) | PUT /lines/{handle}/call-forwarding | | listConversations(params?) | GET /conversations | | listConversationMessages(handle, params?) | GET /conversations/{handle}/messages | | markConversationRead(handle) | POST /conversations/{handle}/read | | listWebhooks() | GET /account/webhooks | | createWebhook(params) | POST /account/webhooks | | replaceWebhooks(webhooks) | PUT /account/webhooks | | deleteWebhook(url) | DELETE /account/webhooks |

Full HTTP-level reference: the Postman collection.

License

MIT