sendimessage
v0.1.0
Published
SendiMessage SDK — send iMessage/SMS through the SendiMessage API
Maintainers
Readme
SendiMessage SDK for JavaScript / TypeScript
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
- Installation
- Authentication
- Quick start
- Usage
- Error handling
- Configuration
- API reference
- License
Requirements
- Node.js ≥ 18.13 (uses the built-in
fetch) - A SendiMessage account with an API key pair
Installation
npm install sendimessageOr straight from GitHub:
npm install github:sendimessage/sendimessage-jsAuthentication
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 | ERRORHistory 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 messageiMessage 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 cacheLookups 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 nowOpt-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 inLines 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 + pendingConversations
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.
