We use optional, privacy-safe analytics to understand which pages help visitors. Nothing optional runs until you choose. Privacy policy

Skip to content
Developer platform

Add customer messaging to your product with one API.

Send messages through managed lines, receive customer replies through webhooks, and build two-way messaging directly into your application. iMessage where available, SMS fallback, one REST contract.

Credentials are provisioned with your managed line by our team — no fake instant signup, no sandbox that pretends to deliver. What you see below is the real contract.

POST/v1/send-message
POST /v1/send-message
X-API-Key: key_••••••••••••••••
X-API-Secret: secret_••••••••••••

{
  "number": "+14155550123",
  "content": "Hi Sarah — your appointment
    is confirmed for tomorrow at 2 PM.",
  "line_handle": "8eb3ecaf-6487-485d-
    b01c-b18bdcb57180"
}
response
// Run the example to see the live shape

Demo conversation transcript: business sends “Hi Sarah — your appointment is confirmed for tomorrow at 2 PM.”.

webhook · receive
The customer reply arrives here as a signed event

Interactive preview with example data — nothing is sent. Shapes match the live-captured API fixtures.

Quickstart

Send your first message.

The smallest successful integration: credentials in, one POST out, one webhook back. Every response shape below is a live capture from the production API contract.

1 Get API access

You need three things: a SendiMessage organization, an active managed line, and an API key + secret. All three are provisioned by our team after a short use-case review — there is no self-serve signup, and your credentials are delivered securely, shown exactly once.

2 Set credentials

Export both values server-side, then hit GET /me — a read-only credential check that confirms the pair works before you send anything.

.env + verify
export SENDIMESSAGE_API_KEY="key_..."
export SENDIMESSAGE_API_SECRET="secret_..."
# Verify the pair without sending anything:
curl "https://api.sendimessage.com/v1/me" \
--header "X-API-Key: ${SENDIMESSAGE_API_KEY}" \
--header "X-API-Secret: ${SENDIMESSAGE_API_SECRET}"
Keep credentials server-side
Never expose the API secret in browser or mobile application code — the key pair grants full messaging access for your whole organization.

3 Send a message

One POST. The 201 response returns status: "QUEUED" and a message_handle — sending is asynchronous, and the channel actually used (iMessage or SMS) is reported after the send confirms, on GET /status or the outbound webhook.

send.sh
curl --request POST \
--url "https://api.sendimessage.com/v1/send-message" \
--max-time 15 \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-API-Key: ${SENDIMESSAGE_API_KEY}" \
--header "X-API-Secret: ${SENDIMESSAGE_API_SECRET}" \
--data '{
"number": "+15551234567",
"content": "Hello from the API",
"media_url": null,
"status_callback": "https://example.com/callbacks/status",
"line_handle": "8eb3ecaf-6487-485d-b01c-b18bdcb57180"
}'

4 Receive the reply

Register a webhook subscribed to the receive event. When the customer answers, the reply reaches your endpoint as one signed POST — flat JSON with from, content and the conversation_handle that groups the thread.

register-webhook.sh
curl --request POST \
--url "https://api.sendimessage.com/v1/account/webhooks" \
--header "Content-Type: application/json" \
--header "X-API-Key: ${SENDIMESSAGE_API_KEY}" \
--header "X-API-Secret: ${SENDIMESSAGE_API_SECRET}" \
--data '{
"url": "https://example.com/webhooks/sendimessage",
"secret": "your-signing-secret",
"events": ["receive", "outbound"]
}'

Verify the SendiMessage-Signature header before trusting any payload — the docs include a worked example you can check byte-for-byte, plus Node.js and Python verification code.

Playground

Try the SendiMessage flow.

Interactive API preview — build the real request, then run the delivery lifecycle. Nothing is sent; stage names are simulation stages, and the response panels use the live-captured contract shapes.

Routing is automatic (iMessage-first, SMS fallback) — there is no channel field on the real request.

curl --request POST \
  --url "https://api.sendimessage.com/v1/send-message" \
  --max-time 15 \
  --header "Accept: application/json" \
  --header "Content-Type: application/json" \
  --header "X-API-Key: ${SENDIMESSAGE_API_KEY}" \
  --header "X-API-Secret: ${SENDIMESSAGE_API_SECRET}" \
  --data '{
  "number": "+15555550123",
  "content": "Your order has shipped.",
  "media_url": null,
  "status_callback": "https://example.com/callbacks/status",
  "line_handle": "8eb3ecaf-6487-485d-b01c-b18bdcb57180"
}'
lifecycle — simulation stages
Validated
Queued
Routing
Sending
Sent
Delivered
response · 201 Created · live-captured shape
{
  "status": "QUEUED",
  "message_handle": "4e828182-a640-483f-bfe9-17ffcaa51d48",
  "to": "+15555550123",
  "content": "Your order has shipped.",
  "service": null,
  "is_outbound": true,
  "error_message": null,
  "date_sent": null
}
Managed lines

A line is your sending identity.

A managed line is the messaging identity provisioned for your organization — a real number we operate for you. Your API credential sees every line in your organization; a send picks one by its line_handle, or omits it to let any active line send.

API credential (key + secret)
Your organization
Managed line — line_handle
Customer conversation
  • limits count new conversations only — replies inside existing threads are never limited.
  • Limit values are per-line configuration set at provisioning: read them from GET /lines, don't hardcode them.
  • A blocked line carries blocked_at / block_reason and answers 422 to sends until the block lifts (you get line_blocked / line_unblocked webhooks).
GET /v1/lines · 200
{
"status": "ok",
"lines": [
{
"number": "+15555550100",
"label": "Support line",
"is_active": true,
"blocked_at": null,
"block_reason": null,
"limits": {
"hourly": { "limit": 25, "remaining": 24 },
"daily": { "limit": 120, "remaining": 119 }
},
"line_handle": "8eb3ecaf-6487-485d-b01c-b18bdcb57180"
}
]
}
Message lifecycle

Five statuses, no surprises.

StatusMeaning
QUEUEDAccepted, waiting for the sending device. The state of every 201 response.
SENTThe device sent it — service now says iMessage or SMS.
DELIVEREDHistory rows only, once the device confirms delivery.
RECEIVEDAn inbound message — history rows and the receive webhook.
ERRORThe send failed — error_message says why.

SENT ≠ DELIVERED. SENT means the device handed the message off; DELIVERED requires a confirmation and only ever appears on history rows.

one message, end to end — example data, not measurements
12:01:02
201 Created — QUEUED
POST /send-message accepted; message_handle returned; service still null.
12:01:04
SENT · service: iMessage
The sending device confirms; GET /status flips to SENT and the outbound webhook fires with the channel used.
12:01:09
DELIVERED (history)
Delivery confirmation lands on the history row in GET /v2/messages. GET /status itself never reports DELIVERED.
12:03:41
RECEIVED — the reply
The customer answers; the inbound row appears in history and the receive webhook fires.
iMessage / SMS routing

You send. The line picks the channel.

POST /send-message (no channel field)
Line checks iMessage availability
iMessage where available · one-shot SMS fallback otherwise
service reported on GET /status + outbound webhook
  • There is no channel selector on the send request — routing is automatic, iMessage-first with SMS fallback. Fallback is one-way (never SMS → iMessage) and can't be toggled per request.
  • service is null on the 201 — the channel used comes back after the device confirms, as iMessage or SMS (history rows can also carry RCS).
  • Want to know ahead of time? GET /lookup?number=… predicts the recipient's service (7-day cache, 202 while a live check runs), and contacts learn a sticky service from real traffic.
Two-way by default

Receive customer replies.

Replies arrive on your line, thread into a conversation, and reach your application as one signed receive event — the exact payload below is the webhook for the reply on the phone.

Demo conversation transcript: business sends “Hi Sarah — your appointment is confirmed for tomorrow at 2 PM.”. Message delivered. Customer replies “Can we move it to 3 PM?”. Reply event processed.

Customer replies
Managed line
SendiMessage records + threads it
receive webhook → your application
receive event · live-captured
{
"event": "receive",
"message_handle": "96699B14-AFDB-4B33-AA66-C4B093533DA3",
"from": "+14155550123",
"to": "+15555550100",
"content": "Can we move it to 3 PM?",
"media_url": null,
"service": "iMessage",
"is_outbound": false,
"status": "RECEIVED",
"date_sent": "2026-08-21T09:07:35Z",
"conversation_handle": "ee60f9b5-2ddb-4ea8-8ad7-bc70cf85727f"
}

conversation_handle groups the whole thread — page it with GET /v2/messages when your application needs context. STOP-family keywords set durable opt-out state automatically.

Incoming Messages guide →

Webhooks

Four events. Real payloads.

Every payload below matches the current dispatch code exactly — id, type, created_at, and a data envelope. No invented fields.

A per-message status_callback URL is accepted on send but not currently delivered — use an account-level webhook or poll GET /status for status updates.

receive · application/json
{
  "id": "evt_01j6x6z9k3f8v9m2n4p5q6r7s8",
  "type": "receive",
  "created_at": "2026-08-21T09:07:35+00:00",
  "data": {
    "message": {
      "message_handle": "msg_01j6x6z6k3f8v9m2n4p5q6r7s8",
      "direction": "inbound",
      "from": "+14155550123",
      "to": "+15555550100",
      "text": "Can we move it to 3 PM?",
      "channel": "imessage",
      "status": "received",
      "line_handle": "line_01j6x6yak3f8v9m2n4p5q6r7s8"
    }
  }
}

Fires once per new inbound message, the moment the line reports it.

Delivery semantics — the honest version

Delivery
One signed HTTP POST per event, retried on failure with exponential backoff (up to 6 attempts, capped at 12h between attempts)
Timeout
5 seconds
Your response
Not inspected — respond 2xx fast and process asynchronously
Signature
SendiMessage-Signature: t={timestamp},v1={hex HMAC-SHA256 of "{timestamp}.{raw body}"} (when a secret is set)
Endpoint disabling
An endpoint is disabled after 10 cumulative delivery failures
status_callback
Accepted on send but not currently delivered — use webhooks or poll GET /status instead
Missed events
Recover by reconciling from GET /v2/messages
Design for eventual, not instant, delivery
Failed deliveries retry with backoff, but design webhooks as a low-latency hint and the API as the source of truth: everything an event carries is queryable from GET /v2/messages, so a missed or delayed delivery is always recoverable by reconciliation.

Signature you can verify right now

SendiMessage-Signature
secret:    example-signing-secret
timestamp: 1755766055
body:      {"id":"evt_01j6x6z9k3f8v9m2n4p5q6r7s8","type":"receive","created_at":"2026-08-21T09:07:35+00:00"…
header:    t=1755766055,v1=3205662b519f85a6ce7bd748e52d419ed03730876448e7a7c9d50dd4efd4b3d8

# hex HMAC-SHA256 of "{timestamp}.{raw body}" — verify against the exact
# raw bytes received; a re-serialized copy will not match.

Also check that the timestamp is recent — verification should reject a header older than a few minutes to guard against replay. Verification code in Node.js and Python (and the SDK helper verifyWebhookSignature) lives in Webhook Signatures.

Debug a webhook event →

SDKs

Official SDKs, or plain HTTP.

Four dependency-free official clients cover the full public API — including the verifyWebhookSignature helper. Every other ecosystem gets raw-HTTP examples against the same contract, so nothing you build depends on an SDK.

JavaScript / TypeScript
npm install sendimessage
Official SDK
Python
pip install sendimessage
Official SDK
PHP
composer require sendimessage/sdk
Official SDK
Go
go get github.com/sendimessage/sendimessage-go
Official SDK
Ruby · Java · C# · everything else
Plain HTTP — copy-paste examples in the reference
HTTP examples
Errors

The errors you'll actually meet.

The API returns human-readable messages, not machine codes — branch on the HTTP status first. These six cover almost every integration mistake; the Errors guide documents both response shapes and the full message list.

401Unauthenticated.

X-API-Key / X-API-Secret missing, wrong, or revoked. Check both headers; verify with GET /me.

422content or media_url required

Send at least one of content / media_url on POST /send-message.

422Unknown or inactive line

line_handle doesn't match an active line — list your lines with GET /lines.

422Contact has opted out of messages

The recipient opted out. Treat as a stop signal, never retry; they can reply START to opt back in.

422Line is blocked

The line is blocked (see line_blocked webhook). Wait for line_unblocked or send via another line.

429Too Many Attempts.

Rate limit hit — honor the Retry-After header, then retry.

Limits

API rate limits ≠ line limits.

HTTP request limits

All requests
300 / min per credential — all key pairs issued to the same account admin share one bucket
Sends
60 / min per account (POST /send-message)
Media uploads
30 / min per account (POST /media)
Lookups
20 / min per account (GET /lookup)
Headers
X-RateLimit-Limit + X-RateLimit-Remaining on every response; 429 adds Retry-After

Messaging line limits

What they cap
New conversations started by a line — replies inside existing threads are never limited
Where to read them
GET /lines → limits.hourly / limits.daily with live remaining counters
Values
Per-line configuration set at provisioning — read them from the API, don't hardcode
When exceeded
Sending is paced/held by the platform — you never get a 429 from a line limit
Video tutorials

Learn by watching.

Short walkthroughs that pair with the documentation — every request uses the verified API contract.

API tutorial
Send Your First Message with the SendiMessage API

A hands-on tutorial: dual-header authentication, finding your line, POST /send-message, the 201 QUEUED response, and checking status.

Read the documentation →
Messaging lines
How Managed Messaging Lines Work

What a managed messaging line is, how line_handle selects it, active and blocked states, limits, routing, replies, webhooks, and reviewed production activation.

Read the documentation →
Webhooks
Webhooks Explained

The receive, outbound, line_blocked, and line_unblocked events, and how to verify the SendiMessage-Signature header.

Read the documentation →
Messages
Message Status and History

QUEUED, SENT, and ERROR statuses, DELIVERED on history rows, and paging history with the before cursor.

Read the documentation →
Lookup
Number Lookup

Checking whether a number is reachable over iMessage or SMS, cache behavior, and 202/503 responses.

Read the documentation →

Ready when you are.

Evaluating? The pricing is public. Ready? Request a line and our team provisions it with you. Unsure? Book a technical demo and watch the API → message → reply → webhook loop live.