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: false means success; on failure data carries message and requestId (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.
  • successOnsent (default), delivered, or seen; determines when the platform considers the send successful (relevant for fallback cascades via POST /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 (CREATEDSTARTEDFINISHED, 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 202 only means the platform accepted the message. Delivery-level rejections such as REJECTD arrive asynchronously via the messageStatus webhook (status: "failed" with reason) or as mdnStatus + errorMessage in GET /edrs — see errors.md.