The single endpoint for outbound delivery. The same request handles iMessage, RCS, and SMS — pick channel: "auto" and we route through the fallback chain automatically.
Endpoint
POST https://api.senderz.com/v1/messages
Authorization: Bearer tf_live_<your_key>
Content-Type: application/json
Sandbox keys (tf_test_*) hit the same endpoint with synthetic delivery — see Testing.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
to | string (E.164) | yes | Recipient phone, e.g. +15551234567 |
body | string | one of body/template | Raw message text, max 1600 chars |
template | string | one of body/template | Template name; combine with data for variable substitution |
data | object | no | Template variable values, e.g. { "code": "1234" } |
channel | enum | no, default auto | auto / imessage / sms / rcs |
from_number | string (E.164) | no | Pin the send to a tenant-owned phone; default uses the routing pool |
message_type | enum | no, default alert | otp / alert / marketing — drives quiet-hours + warming policy |
Examples
curl -X POST https://api.senderz.com/v1/messages \
-H "Authorization: Bearer tf_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+15551234567",
"body": "Hello from senderZ!",
"channel": "auto"
}' import { SenderZ } from '@senderz/sdk'
const client = new SenderZ({ apiKey: process.env.SENDERZ_API_KEY })
const result = await client.messages.send({
to: '+15551234567',
body: 'Hello from senderZ!',
})
console.log(result.message_id) import os
import requests
r = requests.post(
'https://api.senderz.com/v1/messages',
headers={
'Authorization': f'Bearer {os.environ["SENDERZ_API_KEY"]}',
'Content-Type': 'application/json',
},
json={'to': '+15551234567', 'body': 'Hello from senderZ!'},
)
r.raise_for_status()
print(r.json()['message_id']) Successful response
HTTP/1.1 201 Created
Content-Type: application/json
{
"message_id": "01HXY3M0KEXAMPLEMSGID",
"status": "queued",
"from": "+15555550100",
"channel": "imessage",
"estimated_delivery_ms": 1200
}
The id is at message_id (snake_case ULID). Store it; you’ll need it to correlate the delivery callback with the original send.
Errors
Body shape: { "error": string, "code": SCREAMING_CASE }.
| HTTP | code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or malformed body field |
| 401 | INVALID_API_KEY | No / wrong Bearer token |
| 403 | OPTED_OUT | Recipient is on the tenant’s opt-out registry |
| 403 | QUOTA_EXCEEDED | Monthly new-contact cap hit |
| 429 | RATE_LIMITED | Per-key rate cap exceeded; obey Retry-After |
Field-name conventions
We use to (not destination/recipient), body (not text/message), from_number (not source/sender/from). If your client library exposes different names, map them on your side.
Pricing model
senderZ is flat-plan, not per-message. Plans charge by new contacts per month (unique numbers you’ve never messaged before). Messages to existing contacts are unlimited regardless of plan. See pricing for the plan tiers.
Related
- Webhooks — register your endpoint for delivery callbacks and inbound replies
- Rate limits — per-key burst behavior and the 429 contract
- Testing — sandbox keys and magic numbers
- Compliance — STOP/HELP keywords and 10DLC stance