Search by

SendiMessage SDK — send iMessage/SMS through the SendiMessage API

Maintainers

Package info

github.com/sendimessage/sendimessage-php

Homepage

pkg:composer/sendimessage/sdk

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-09-07 12:08 UTC

This package is auto-updated.

Last update: 2026-09-07 12:09:35 UTC


README

Version License: MIT PHP

The official PHP client for the SendiMessage API — send and receive iMessage and SMS programmatically. No dependencies beyond ext-curl and ext-json.

Server-side only. The API key pair grants full messaging access to your account — never expose it in front-end code.

Contents

Requirements

  • PHP ≥ 8.1 with ext-curl and ext-json
  • A SendiMessage account with an API key pair

Installation

composer require sendimessage/sdk

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.

use SendImessage\Client;

$client = new Client(
    apiKey: getenv('SENDIMESSAGE_KEY'),
    apiSecret: getenv('SENDIMESSAGE_SECRET'),
);

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

Quick start

use SendImessage\Client;

$client = new Client(apiKey: '...', apiSecret: '...');

$msg = $client->sendMessage(number: '+15551234567', content: 'Hello from SendiMessage!');

echo $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

$msg = $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:

$lines = $client->listLines();
$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.

$url = $client->uploadMedia('./photo.jpg');

$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

$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:

$status = $client->getStatus($msg['message_handle']);
echo $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.

$before = null;
do {
    $page = $client->listMessages(['number' => '+15551234567', 'limit' => 50, 'before' => $before]);
    foreach ($page['messages'] as $m) {
        echo $m['date_sent'], $m['is_outbound'] ? '' : '', $m['content'], PHP_EOL;
    }
    $before = $page['next_before'];
} while ($before !== null);

Filters: number, direction (in / out), line_handle, conversation_handle, service, since, until, limit.

$one = $client->getMessage($handle); // single history message

iMessage lookup

Check whether a number is reachable over iMessage before sending:

$result = $client->lookup('+15551234567');
echo $result['service']; // "iMessage" | "SMS"
echo $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.

$client->createContact(['number' => '+15551234567', 'first_name' => 'Jane', 'last_name' => 'Doe']);
$client->updateContact('+15551234567', ['company_name' => 'Acme Inc.']);
$contact = $client->getContact('+15551234567'); // number or contact_handle
$all = $client->listContacts(['limit' => 100]);
$client->deleteContact('+15551234567');
$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:

$client->setOptOut('+15551234567', true);   // opt out
$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.

$lines = $client->listLines();
$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:

$client->setCallForwarding($lineHandle, '+15559876543'); // request forwarding
$client->setCallForwarding($lineHandle, null);           // request turning it off
$state = $client->getCallForwarding($lineHandle);        // current + pending

Conversations

Conversations group the message history per contact:

$convos = $client->listConversations(['per_page' => 20]);
$msgs = $client->listConversationMessages($conversationHandle, ['per_page' => 50]);
$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
$client->createWebhook(
    url: 'https://example.com/hooks/sendimessage',
    events: ['receive', 'outbound'],        // default: every event
    secret: getenv('WEBHOOK_SECRET'),       // enables the signature header
);

$hooks = $client->listWebhooks();
$client->replaceWebhooks([...]);            // swap the whole set atomically
$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:

use SendImessage\Client;

// The raw body is required — re-serialized JSON differs byte-for-byte.
$rawBody = file_get_contents('php://input');

$ok = Client::verifyWebhookSignature(
    secret: getenv('WEBHOOK_SECRET'),
    rawBody: $rawBody,
    signature: $_SERVER['HTTP_X_SMSBRIDGE_SIGNATURE'] ?? '',
);
if (!$ok) {
    http_response_code(401);
    exit;
}

$event = json_decode($rawBody, true);
// ... handle event

Error handling

Every non-2xx response throws SendImessage\ApiException with the HTTP code as getCode(), the API error text as getMessage(), and the parsed response body on the body property.

use SendImessage\ApiException;

try {
    $client->sendMessage(number: '+15551234567', content: 'hi');
} catch (ApiException $e) {
    error_log($e->getCode() . ' ' . $e->getMessage());
}

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

$client = new Client(
    apiKey: '...',
    apiSecret: '...',
    baseUrl: 'https://api.sendimessage.com/v1', // default; override for testing
    timeout: 30,                                // seconds, per request
);

API reference

Method Endpoint
sendMessage(number, content?, mediaUrl?, lineHandle?, statusCallback?, scheduledAt?) POST /send-message
getStatus(messageHandle) GET /status
listMessages(filters?) GET /v2/messages
getMessage(messageHandle) GET /v2/messages/{handle}
uploadMedia(path, 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(lineHandle, label?, isActive?) PUT /lines/{handle}
getCallForwarding(lineHandle) GET /lines/{handle}/call-forwarding
setCallForwarding(lineHandle, forwardingNumber) 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(url, events?, secret?) POST /account/webhooks
replaceWebhooks(webhooks) PUT /account/webhooks
deleteWebhook(url) DELETE /account/webhooks

Full HTTP-level reference: the Postman collection.

License

MIT

Forcing SMS

By default the Mac tries iMessage and falls back to SMS when Apple reports the number has no iMessage. Pass service: 'sms' to send a plain text message straight away (or 'imessage' to insist on the default path):

$client->sendMessage('+15551234567', content: 'Your code is 1234', service: 'sms');

$client->me() returns the user + company behind the key pair — the cheapest way to verify credentials.