SMS API

Send SMS over HTTP — one message, a batch, or a saved template — optionally with a shortened link to a landing page or survey, then track what happened to it.

A recipient is a number, not a string

Recipient and MobileNo are numeric: send 50688887777, not "+506 8888 7777". Anything with a +, a space or a dash is not a number and will not bind.

Long messages are split, and billed per part

A message over 160 GSM-7 characters — or 70 if it contains Unicode — is split into multiple parts and concatenated on the handset. Each part is billed separately. A single accented character can push a message into Unicode and quarter your per-message length, so check the encoding before assuming a body fits.

Every send is accepted, not delivered

A successful send returns 202 Accepted and a QueueId. That means we have taken the message, not that it reached the handset. Delivery outcome comes from Check delivery status.

Download for AI review

Copy or download this guide as Markdown to paste into an AI assistant for help integrating against it.

Environment

You are reading the guide for the host that served this page. The badge in the header and every example below already point at it — nothing to substitute by hand, and no other environment's addresses appear on this page.

PropertyValue
Environment
Base URLhttps://sms-dev.jirafix.net
All pathsare under /v1/sms

Authentication

Every endpoint needs a bearer token, and this host does not issue one. Exchange your credentials at the Authenticate API — on this environment that is https://authenticate-dev.jirafix.net — then send what it returns as Authorization: Bearer <access_token> on each request here.

Keep your credentials on your server

Credentials embedded in a browser page or a mobile app are published credentials — anyone can read them and send messages billed to you. Request the token from your own backend and never ship it to a client.

POST https://authenticate-dev.jirafix.net/v1/token

Exchanges your credentials for an access token.

Tokens last 3600 seconds by default. Request a new one when it expires — there is no separate refresh call, though the response does include a refresh_token. Full detail is on the Authenticate guide at https://authenticate-dev.jirafix.net/docs.

Parameters

NameTypeRequiredDescription
usernamestringYesThe account's username, usually an email address.
passwordstringYesThe account's password. Server-side only.
privatetokenstringYesYour account's private token, from the portal's configuration section. Note the spelling — one word, all lower case.
validityintegerNoHow long the token should last, in seconds. Defaults to 3600.

Responses

StatusMeaning
200Returns access_token, refresh_token, token_type and expires_in.
400The body was missing or a required field was absent.
401The username, password or private token was not accepted.
# 1. get a token from the Authenticate host
ACCESS_TOKEN=$(curl -s -X POST https://authenticate-dev.jirafix.net/v1/token \
  -H "Content-Type: application/json" \
  -d '{"username":"you@yourcompany.com","password":"'"$OLANZO_PASSWORD"'","privatetoken":"'"$OLANZO_PRIVATE_TOKEN"'"}' \
  | jq -r .access_token)

# 2. spend it here
curl -X GET "https://sms-dev.jirafix.net/v1/sms/lists" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
using var auth = new HttpClient { BaseAddress = new Uri("https://authenticate-dev.jirafix.net") };

var tokenResponse = await auth.PostAsJsonAsync("/v1/token", new
{
    username = "you@yourcompany.com",
    password = Environment.GetEnvironmentVariable("OLANZO_PASSWORD"),
    privatetoken = Environment.GetEnvironmentVariable("OLANZO_PRIVATE_TOKEN"),
});

// the property is access_token, not accessToken
var payload = await tokenResponse.Content.ReadFromJsonAsync<JsonElement>();
var token = payload.GetProperty("access_token").GetString();

using var api = new HttpClient { BaseAddress = new Uri("https://sms-dev.jirafix.net") };
api.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);

var response = await api.SendAsync(
    new HttpRequestMessage(HttpMethod.Get, "/v1/sms/lists"));
// 1. get a token from the Authenticate host
const tokenResponse = await fetch("https://authenticate-dev.jirafix.net/v1/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    username: "you@yourcompany.com",
    password: process.env.OLANZO_PASSWORD,
    privatetoken: process.env.OLANZO_PRIVATE_TOKEN,
  }),
});

// note the underscore — accessToken is undefined
const { access_token } = await tokenResponse.json();

// 2. spend it here
const response = await fetch("https://sms-dev.jirafix.net/v1/sms/lists", {
  method: "GET",
  headers: { Authorization: `Bearer ${access_token}` },
});
import os, requests

# 1. get a token from the Authenticate host
token_response = requests.post(
    "https://authenticate-dev.jirafix.net/v1/token",
    json={
        "username": "you@yourcompany.com",
        "password": os.environ["OLANZO_PASSWORD"],
        "privatetoken": os.environ["OLANZO_PRIVATE_TOKEN"],
    },
)

# note the underscore — "accessToken" raises KeyError
access_token = token_response.json()["access_token"]

# 2. spend it here
response = requests.get(
    "https://sms-dev.jirafix.net/v1/sms/lists",
    headers={"Authorization": f"Bearer {access_token}"},
)
200 OK

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}
The response is snake_case

The fields are access_token, refresh_token, token_type and expires_in — not accessToken or expiresIn. Reading the camelCase spelling gives you nothing, with no error to explain it.

Use the Authenticate host for this same environment

A token carries the API domains it is allowed to reach. One issued by a different environment's Authenticate host is rejected here with a 401 that reads like bad credentials, so check the pair before you check your password: this page is https://sms-dev.jirafix.net and its Authenticate host is https://authenticate-dev.jirafix.net.

Send your first message

Four fields and you are done. The response gives you a QueueId to track the send.

curl -X POST https://sms-dev.jirafix.net/v1/sms/send \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "Recipient": 50688887777,
        "SenderName": "YOURBRAND",
        "IsBodyEncrypted": false,
        "Body": "Your order is confirmed."
      }'
202 Accepted

{
  "QueueId": "9f1c7d2e-4b3a-4c1d-9e8f-2a7b6c5d4e3f",
  "QueuedTimestamp": 1755590400000
}

Sending messages

Use thisWhenRecipients
/sendOne person, plain textOne
/singleOne person, with a shortened link to a landing page or surveyOne
/batchA list, or up to 1,000 numbers you supplyUp to 1,000
/predefinedsmsA template already built in the portalOne or many

Send one message

POST /v1/sms/send

Sends one plain-text message to one number.

The right default for order confirmations, one-time codes and alerts. If you need a tracked link, use /single instead.

Parameters

NameTypeRequiredDescription
RecipientnumberYesThe destination number, in full international form as a number — 50688887777.
SenderNamestringYesThe sender ID shown on the handset. 5 to 11 characters. Outside that range the request is rejected.
IsBodyEncryptedbooleanYesSend false unless you have specifically agreed encrypted bodies with us. It is not optional — include it.
BodystringYesThe message text. See the note on length and billing above — this is plain text, not Base64.
PersonalizationSubstitutionTagsarrayNoValues substituted into the body. Omit if you do not personalise.
ScheduleDateTimedate-timeNoSend later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped, and a time in the past is rejected.
WebHookUrlstringNoWe post delivery reports here as they happen, so you do not have to poll.

Responses

StatusMeaning
202Accepted and queued. Returns QueueId and QueuedTimestamp.
400The request failed field validation — a missing required field, a sender name outside 5–11 characters, or a value of the wrong type such as a quoted phone number. The body names the offending fields. Rules applied after the request binds come back as 417 instead.
401Missing, expired or invalid bearer token.
417A rule rejected the request — see Errors. The body carries status: 0 and a message. Retrying it unchanged will fail again.
424We could not queue the message. Nothing was sent and no QueueId exists — this one is worth retrying.
curl -X POST https://sms-dev.jirafix.net/v1/sms/send \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "Recipient": 50688887777,
        "SenderName": "YOURBRAND",
        "IsBodyEncrypted": false,
        "Body": "Your code is 4821."
      }'

Send with a tracked link

POST /v1/sms/single

Sends one message carrying a shortened link to a landing page or survey.

Find ids with List landing pages or List surveys.

Parameters

NameTypeRequiredDescription
RecipientnumberYesThe destination number as a number. Must be greater than 99999, otherwise the request fails validation with 400.
SenderNamestringYesThe sender ID shown on the handset. 5 to 11 characters. Outside that range the request is rejected.
IsBodyEncryptedbooleanYesSend false unless you have specifically agreed encrypted bodies with us. It is not optional — include it.
BodystringYesThe message text. See the note on length and billing above — this is plain text, not Base64.
LandingPageIdguidNoThe landing page or survey to link to.
Shortner.ShortnerUrlstringNoThe short domain to build the link on. If you send a Shortner object at all, this field inside it is required.
Shortner.IsUniqueUrlbooleanNoWhether each recipient gets their own link, so clicks can be attributed individually.
UnsubscribeContentstringNoOpt-out text appended to the message.
ScheduleDateTimedate-timeNoSend later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped, and a time in the past is rejected.
WebHookUrlstringNoWe post delivery reports here as they happen, so you do not have to poll.

Responses

StatusMeaning
202Accepted and queued. Returns QueueId and QueuedTimestamp.
400The request failed field validation — a missing required field, a sender name outside 5–11 characters, a value of the wrong type, or a recipient number that is not greater than 99999. The body names the offending fields. This endpoint checks the recipient specifically; the others do not.
401Missing, expired or invalid bearer token.
417A rule rejected the request — see Errors. The body carries status: 0 and a message. Retrying it unchanged will fail again.
424We could not queue the message. Nothing was sent and no QueueId exists — this one is worth retrying.
curl -X POST https://sms-dev.jirafix.net/v1/sms/single \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "Recipient": 50688887777,
        "SenderName": "YOURBRAND",
        "IsBodyEncrypted": false,
        "Body": "Tell us how we did:",
        "LandingPageId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "Shortner": { "ShortnerUrl": "go.yourbrand.cr", "IsUniqueUrl": true }
      }'
400 Bad Request

{
  "error": { "Recipient": ["The Recipient must be a valid number"] },
  "Status": 400,
  "Title": "One or more validation errors occurred.",
  "TraceId": "1f0c..."
}

Send a batch

POST /v1/sms/batch

Sends to a saved list, or to numbers you supply — each personalised individually.

Give either ListId or a Recipients array. With neither, the request is rejected. Unlike the Email API, the sending fields sit at the top level here — there is no wrapper object.

Parameters

NameTypeRequiredDescription
ListIdarrayNoSubscriber lists to send to. Required unless you supply Recipients.
RecipientsarrayNoUp to 1,000 per call. Each entry needs a numeric MobileNo, and every number must be unique. Required unless you supply a ListId.
Recipients[].MobileNonumberNoThe destination number for that entry, as a number.
Recipients[].PersonalizationSubstitutionTagsarrayNoPer-recipient values substituted into the body.
SenderNamestringYesThe sender ID shown on the handset. 5 to 11 characters. Outside that range the request is rejected.
IsBodyEncryptedbooleanYesSend false unless you have specifically agreed encrypted bodies with us. It is not optional — include it.
BodystringYesThe message text. See the note on length and billing above — this is plain text, not Base64.
LandingPageIdguidNoLink the batch to a landing page or survey.
Shortner.ShortnerUrlstringNoRequired if you send a Shortner object.
ScheduleDateTimedate-timeNoSend later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped, and a time in the past is rejected.
WebHookUrlstringNoWe post delivery reports here as they happen, so you do not have to poll.

Responses

StatusMeaning
202Accepted and queued. Returns QueueId and QueuedTimestamp.
400The request failed field validation — a missing required field, a sender name outside 5–11 characters, or a value of the wrong type such as a quoted phone number. The body names the offending fields. Rules applied after the request binds come back as 417 instead.
401Missing, expired or invalid bearer token.
417A rule rejected the request — see Errors. The body carries status: 0 and a message. Retrying it unchanged will fail again.
424We could not queue the message. Nothing was sent and no QueueId exists — this one is worth retrying.
curl -X POST https://sms-dev.jirafix.net/v1/sms/batch \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "SenderName": "YOURBRAND",
        "IsBodyEncrypted": false,
        "Body": "Your statement is ready.",
        "Recipients": [
          { "MobileNo": 50688887777 },
          { "MobileNo": 50688886666 }
        ]
      }'

Send a saved template

POST /v1/sms/predefinedsms

Sends a template already built in the portal.

The template owns the body and sender, so you supply only who it goes to. Find ids with List trigger templates.

Parameters

NameTypeRequiredDescription
PredefinedTemplateIdstringYesThe template to send, e.g. TSMS-899.
RecipientsarrayNoEach entry needs a numeric MobileNo. Required unless you supply a ListId.
Recipients[].MobileNonumberNoThe destination number for that entry, as a number.
ListIdarrayNoSend to saved lists instead of explicit numbers.
ScheduleDateTimedate-timeNoSend later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped, and a time in the past is rejected.
WebHookUrlstringNoWe post delivery reports here as they happen, so you do not have to poll.

Responses

StatusMeaning
202Accepted and queued. Returns QueueId and QueuedTimestamp.
400The request failed field validation — a missing required field, a sender name outside 5–11 characters, or a value of the wrong type such as a quoted phone number. The body names the offending fields. Rules applied after the request binds come back as 417 instead.
401Missing, expired or invalid bearer token.
417A rule rejected the request — see Errors. The body carries status: 0 and a message. Retrying it unchanged will fail again.
424We could not queue the message. Nothing was sent and no QueueId exists — this one is worth retrying.
curl -X POST https://sms-dev.jirafix.net/v1/sms/predefinedsms \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "PredefinedTemplateId": "TSMS-899",
        "Recipients": [{ "MobileNo": 50688887777 }]
      }'

Check delivery status

Take the QueueId from any send and ask what happened to it. This is the only authoritative answer on delivery.

GET /v1/sms/{id}/Status

Reads the outcome of a previously queued send.

Details counts recipients by stage — Total, Queued, Submitted, Delivered and Failed — so a batch can be tracked as it drains.

Parameters

NameTypeRequiredDescription
idguidYesThe QueueId returned by the send.

Responses

StatusMeaning
200Current status, with a per-stage recipient breakdown in Details.
400No send matches that id. Note this is a 400, not a 404.
401Missing, expired or invalid bearer token.
curl -X GET https://sms-dev.jirafix.net/v1/sms/9f1c7d2e-4b3a-4c1d-9e8f-2a7b6c5d4e3f/Status \
  -H "Authorization: Bearer <your-token>"
200 OK

{
  "QueueId": "9f1c7d2e-4b3a-4c1d-9e8f-2a7b6c5d4e3f",
  "Status": "Completed",
  "Message": "Sent",
  "Details": {
    "Total": 2,
    "Queued": 0,
    "Submitted": 2,
    "Delivered": 2,
    "Failed": 0
  }
}
Out of funds

If the account has run out of credit the status reads InSufficient Fund and a Balance object is included. Top up and send again — the original send does not resume on its own.

Lists and contact counts

GET /v1/sms/lists

Lists the subscriber lists reachable by SMS, with contact counts.

Parameters

NameTypeRequiredDescription
excludeCountbooleanNoSet to true to skip counting, which returns faster on large lists.

Responses

StatusMeaning
200The lists, with counts unless you excluded them.
401Missing, expired or invalid bearer token.
curl -X GET "https://sms-dev.jirafix.net/v1/sms/lists?excludeCount=true" \
  -H "Authorization: Bearer <your-token>"
GET /v1/sms/{listId}/contacts/count

Contact count and segment breakdown for one subscriber list.

Parameters

NameTypeRequiredDescription
listIdguidYesThe subscriber list to count.

Responses

StatusMeaning
200The count and segment breakdown. An unknown list also answers 200, with nothing in it — check the payload, not the status code.
401Missing, expired or invalid bearer token.
curl -X GET https://sms-dev.jirafix.net/v1/sms/3fa85f64-5717-4562-b3fc-2c963f66afa6/contacts/count \
  -H "Authorization: Bearer <your-token>"

Landing pages

GET /v1/sms/landingpages

Lists the landing pages you can link to from a message.

The ids here are what /single and /batch accept as LandingPageId.

Responses

StatusMeaning
200The available landing pages.
400The request could not be read.
401Missing, expired or invalid bearer token.
curl -X GET https://sms-dev.jirafix.net/v1/sms/landingpages \
  -H "Authorization: Bearer <your-token>"

Surveys

GET /v1/sms/surveys

Lists the surveys you can link to from a message.

Responses

StatusMeaning
200The available surveys.
400The request could not be read.
401Missing, expired or invalid bearer token.
curl -X GET https://sms-dev.jirafix.net/v1/sms/surveys \
  -H "Authorization: Bearer <your-token>"

Trigger templates

GET /v1/sms/trigger-templates

Lists the saved templates you can send.

The ids here are what /predefinedsms expects.

Parameters

NameTypeRequiredDescription
isCalledFromSignUpbooleanNoNarrows the list to sign-up templates.

Responses

StatusMeaning
200The available templates.
401Missing, expired or invalid bearer token.
curl -X GET https://sms-dev.jirafix.net/v1/sms/trigger-templates \
  -H "Authorization: Bearer <your-token>"

Errors

This API answers 417 where many APIs would answer 400. A 417 means the request was understood and a rule rejected it; the body carries status: 0 and a message saying which.

400 and 417 split the work between them

A 400 comes from binding and field validation, before the request is really looked at: a missing required field, a sender name outside 5–11 characters, or a value of the wrong type — a quoted phone number lands here. A 417 comes from a rule applied after the request binds successfully, such as a body that is only whitespace or a schedule date in the past. /single additionally checks that the recipient number is greater than 99999, and answers 400 if not; the other three sends do not run that check.

StatusWhat it meansWhat to do
202Accepted and queued.Track it with the status endpoint.
400A required field is missing, a field has the wrong type, the sender name is outside 5–11 characters, or the id you asked about does not exist.Read the body — it names the offending fields.
401The token is missing, expired or invalid.Fetch a new token and retry once.
417A rule rejected the request.Fix the request. Retrying it unchanged will fail again.
424We could not queue the message. Nothing was sent.Retry. If it persists, contact support.

What a 417 is telling you

MessageCause
sms body is requiredBody was present but only whitespace. A missing body is a 400, not this.
Body is requiredThe same problem on a batch, worded differently.
Sender Name is requiredSenderName was missing.
Sender From is requiredThe same problem on a batch, worded differently.
Recipient is required.Recipient was missing or zero.
Scheduled Date should be future date.ScheduleDateTime was in the past.
Predefined Template Id is requiredPredefinedTemplateId was missing.
Either list or recipients is required to sendA batch had neither a ListId nor Recipients.
Aha! batch limit exceeded. Limit is per batch 1000More than 1,000 recipients in one batch. Split it.
Each recipients must have mobile no.A recipient entry had no MobileNo.
Each recipients must have unique emailA number appears twice in the batch. The wording says email; on this API it means the mobile number, and the response lists the duplicates.
Shortner Url is requiredYou sent a Shortner object without a ShortnerUrl.

Limits

LimitValue
Recipients per batch1,000
Sender name5–11 characters
Message length before splitting160 GSM-7 characters, or 70 if the body contains Unicode
Schedule precisionOne minute — seconds are dropped, and past times are rejected