curl Examples
Copy-pasteable curl snippets for the most common Routeon SMS operations. Every example targets the production API and becomes runnable once you substitute <API_KEY> (created in the client portal under API Accesses) and your own values (sender, phone numbers).
Setup
All requests go to the same base URL and authenticate with the X-API-Key header:
export ROUTEON_API_KEY="<API_KEY>"
export BASE_URL="https://client.routeon.io/api/v1"
Conventions used throughout:
- Phone numbers are digits with country code, without
+(e.g.905063565285). - SMS is channel
1. - Every response uses the envelope
{ "error": <bool>, "data": ... }.error: falsemeans success; on failuredatacarriesmessageandrequestId(see Error handling below).
Pre-flight: channels, senders, balance
Before your first send, confirm the channel is enabled for your account, you have an Active SMS sender, and your balance is positive.
# 1. Which channels are enabled for this API key?
curl -sS "$BASE_URL/channels"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Accept: application/json'
Expected response:
{
"error": false,
"data": [
{ "id": 1, "type": "SMS", "description": "Sms channel type" }
]
}
# 2. Which senders can I use? Only status 2 (Active) can send.
curl -sS "$BASE_URL/senders"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Accept: application/json'
| jq '.data[] | select(.status == 2) | {senderId, displayName, channel, webhookUrl}'
# 3. Do I have balance to send?
curl -sS "$BASE_URL/balance"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Accept: application/json'
Expected response:
{
"error": false,
"data": [
{ "agreementId": 12, "balanceId": 34, "name": "Main", "amount": 12345.67, "currency": "USD" }
]
}
Send an SMS
POST /messages sends one message to one recipient. It returns 202 Accepted — delivery happens asynchronously and is reported via webhook and/or GET /edrs.
curl -sS -X POST "$BASE_URL/messages"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Content-Type: application/json'
-d '{
"contact": "905063565285",
"channel": 1,
"senderId": "Routeon",
"payload": { "text": "Hello from Routeon" },
"webhook": "https://your.app/hooks/routeon",
"clientInfo": "batchId=2026-07-14-001",
"successOn": "sent"
}'
Expected response (202 Accepted):
{
"error": false,
"data": {
"channel": 1,
"transactionId": "95e3e0da-9684-4ba4-88a3-e9207ddeca05",
"messageId": "6719b5f4-36af-4fba-b111-49ff07f4f96b"
}
}
Store messageId — it is the key for delivery tracking (webhooks and EDRs) — and keep transactionId for support requests.
Field notes:
payload.text— the SMS body (free-form text).webhook— optional; overrides the webhook configured on the Sender or the API key for this message.clientInfo— optional free-form tag preserved in the EDR row; use it to correlate messages with your own batch/order IDs.successOn—sent(default),delivered, orseen; determines when the platform considers the send successful (relevant for fallback cascades viaPOST /omnimessages).
Send an OTP SMS
For one-time codes, set isOtp: true so the message body is masked in logs and reports:
curl -sS -X POST "$BASE_URL/messages"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Content-Type: application/json'
-d '{
"contact": "905063565285",
"channel": 1,
"senderId": "Routeon",
"isOtp": true,
"payload": { "text": "Your verification code is 123456" },
"clientInfo": "otp;userId=42"
}'
The response shape is the same 202 envelope with messageId / transactionId.
Track delivery: GET /edrs
Poll delivery status by messageIds (comma-separated, 1–10 IDs per call):
curl -sS -G "$BASE_URL/edrs"
--data-urlencode "messageIds=6719b5f4-36af-4fba-b111-49ff07f4f96b"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Accept: application/json'
| jq '.data[] | {messageId, channelType, mdnStatus, isSent, isDelivered, rate, finalCost, errorMessage, timeSent, timeDelivered, clientInfo}'
Each row carries mdnStatus (operator-level status such as DELIVRD, UNDELIV, EXPIRED, REJECTD), the boolean flags isSent / isDelivered / isSeen, billing fields rate and finalCost, errorMessage, timestamps, and your clientInfo tag.
For reconciliation over a period, query by time range instead (both bounds required; limit is capped at 100 — page with offset):
curl -sS -G "$BASE_URL/edrs"
--data-urlencode "timeFrom=2026-07-14T00:00:00Z"
--data-urlencode "timeTo=2026-07-14T23:59:59Z"
--data-urlencode "limit=100"
--data-urlencode "offset=0"
-H "X-API-Key: $ROUTEON_API_KEY"
Create an SMS campaign template
Bulk campaigns run on the Broadcast API (API-key scope Broadcasts) and launch from a template. Variables use the {{attribute.name}} syntax and are filled from contact attributes.
curl -sS -X POST "$BASE_URL/broadcast/templates"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Content-Type: application/json'
-d '{
"name": "Summer promo SMS",
"description": "July promotion",
"extraRecipientList": [],
"filterList": [],
"tags": [],
"contentType": 1,
"content": {
"flowSteps": [
{
"channelType": 1,
"senderId": "Routeon",
"contentPattern": "Hello {{attribute.name}}, our summer sale starts today.",
"deliveryCondition": "DELIVERY_SUCCESS",
"timeout": 172800
}
]
}
}'
The response returns the created template object — keep its numeric id for the launch. You can also look ids up later:
curl -sS "$BASE_URL/broadcast/templates?limit=10&offset=0"
-H "X-API-Key: $ROUTEON_API_KEY"
Note: marketing campaign texts are approved in advance on most destinations. Have your template approved before launching — an unapproved template is a common cause of REJECTD.
Launch a campaign
POST /broadcast/broadcasts launches by templateId. An empty (or omitted) launchTime means "launch now"; an RFC 3339 timestamp schedules the run. The webhook field is required — it receives campaign-level status updates.
curl -sS -X POST "$BASE_URL/broadcast/broadcasts"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Content-Type: application/json'
-w 'nHTTP %{http_code}n'
-d '{
"templateId": 13265,
"launchTime": "",
"webhook": "https://your.app/hooks/broadcast",
"recipientsList": [
{ "phoneNumber": "905063565285" },
{ "phoneNumber": "4915735987904" }
]
}'
A successful launch returns 201 (or 200) — the body may be empty. Campaign progress arrives on your webhook as status transitions (CREATED → STARTED → FINISHED, or FAILED / CANCELED with a message explaining why):
{
"status": "FINISHED",
"webhook": "https://your.app/hooks/broadcast",
"broadcastId": 12345,
"message": null
}
There is no endpoint to poll a broadcast by id, so keep the webhook. For per-recipient results, pull EDRs by time range or by the broadcastRunId filter:
curl -sS -G "$BASE_URL/edrs"
--data-urlencode "timeFrom=2026-07-14T00:00:00Z"
--data-urlencode "timeTo=2026-07-14T23:59:59Z"
--data-urlencode "broadcastRunId=1001"
--data-urlencode "limit=100"
-H "X-API-Key: $ROUTEON_API_KEY"
Import contacts
Contacts live in the Recipients API (API-key scope Recipients). type controls how existing contacts are merged: ENRICH (add/merge fields), OVERWRITE (replace), or NOT_IMPORT (skip existing).
curl -sS -X POST "$BASE_URL/recipient/recipients"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Content-Type: application/json'
-d '{
"recipients": [
{ "name": "Alice", "phoneNumber": "48732231255", "tags": ["demo"], "attributes": { "Age": 30 } },
{ "name": "Bob", "phoneNumber": "48732231256", "tags": ["demo"], "attributes": { "Age": 41 } }
],
"tag": "demo",
"type": "ENRICH"
}'
Verify the import:
curl -sS "$BASE_URL/recipient/recipients?limit=10&offset=0&tag=demo"
-H "X-API-Key: $ROUTEON_API_KEY"
Webhook payloads
The platform POSTs JSON to your webhook URL. There are two event families on the message webhook (discriminated by the event field) plus the campaign-level broadcast payload shown above.
Delivery lifecycle — messageStatus:
{
"event": "messageStatus",
"message": {
"messageId": "4aab1947-7a34-4efe-8c51-e9c8f9b1412b",
"omniTransactionId": "57f29fdd-1127-45b3-a7d5-260a831a4662",
"channel": 1,
"senderId": "Routeon",
"status": "delivered",
"timestamp": "2026-07-14T15:30:00Z",
"contact": "905324546496",
"cost": "1.00"
},
"reason": { "code": 0, "text": "" }
}
status is one of sent, delivered, displayed, failed, undeliverable, unknown. On failures, reason.code / reason.text explain why.
Inbound reply (MO) — message:
{
"event": "message",
"message": {
"messageId": "4aab1947-7a34-4efe-8c51-e9c8f9b1412b",
"timestamp": "2026-07-14T15:30:00Z",
"channel": "SMS",
"senderId": "2813380676",
"contact": "8172958708",
"payload": { "text": "test SMS" }
}
}
Your receiver must respond 2xx quickly and be idempotent on messageId — duplicates are possible after retries. A complete runnable receiver is in node.md.
You can simulate an incoming webhook locally:
curl -X POST http://localhost:8080/hooks/routeon
-H 'Content-Type: application/json'
-d '{"event":"messageStatus","message":{"messageId":"test-1","channel":1,"status":"delivered","contact":"905063565285"}}'
Error handling
Every failure response uses the same envelope:
{
"error": true,
"data": {
"message": "something went wrong",
"requestId": "af8c5065-5849-455b-ae1a-6a0b9f4b7209"
}
}
Always log data.requestId and include it in support requests. A resilient shell client checks both the HTTP code and the error flag, and retries only 5xx with exponential backoff:
send_sms() {
local attempt=1 max_attempts=4 delay=1
while :; do
local body_file; body_file=$(mktemp)
local http_code
http_code=$(curl -sS -o "$body_file" -w '%{http_code}'
-X POST "$BASE_URL/messages"
-H "X-API-Key: $ROUTEON_API_KEY"
-H 'Content-Type: application/json'
-d "$1")
if [[ "$http_code" =~ ^2 ]] && [[ "$(jq -r '.error' "$body_file")" == "false" ]]; then
jq . "$body_file"; rm -f "$body_file"; return 0
fi
if [[ "$http_code" =~ ^5 ]] && (( attempt < max_attempts )); then
echo "HTTP $http_code, retrying in ${delay}s (attempt $attempt/$max_attempts)..." >&2
sleep "$delay"; delay=$((delay * 4)); attempt=$((attempt + 1))
rm -f "$body_file"; continue
fi
# 4xx: do not retry — fix the request. Log requestId for support.
echo "Failed: HTTP $http_code, requestId=$(jq -r '.data.requestId // "n/a"' "$body_file"), message=$(jq -r '.data.message // "n/a"' "$body_file")" >&2
rm -f "$body_file"; return 1
done
}
send_sms '{"contact":"905063565285","channel":1,"senderId":"Routeon","payload":{"text":"Hello"}}'
Rules of thumb:
400/401/404— do not retry; fix the payload, the key/scope, or the sender.5xx— retry with exponential backoff (e.g. 1s / 4s / 16s, max 3–4 attempts).- A
202only means the platform accepted the message. Delivery-level rejections such asREJECTDarrive asynchronously via themessageStatuswebhook (status: "failed"withreason) or asmdnStatus+errorMessageinGET /edrs— see errors.md.