All docs

Send a Message

Endpoint reference for POST /v1/messages with full request and response shapes.

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 | | 403 | TRIAL_EXPIRED | 14-day trial ended with no active subscription — subscribe to resume sending | | 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.

  • 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