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.
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.
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.
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.
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.
| Property | Value |
|---|---|
| Environment | |
| Base URL | https://sms-dev.jirafix.net |
| All paths | are 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.
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.
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
| Name | Type | Required | Description |
|---|---|---|---|
username | string | Yes | The account's username, usually an email address. |
password | string | Yes | The account's password. Server-side only. |
privatetoken | string | Yes | Your account's private token, from the portal's configuration section. Note the spelling — one word, all lower case. |
validity | integer | No | How long the token should last, in seconds. Defaults to 3600. |
Responses
| Status | Meaning |
|---|---|
200 | Returns access_token, refresh_token, token_type and expires_in. |
400 | The body was missing or a required field was absent. |
401 | The 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 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.
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 this | When | Recipients |
|---|---|---|
/send | One person, plain text | One |
/single | One person, with a shortened link to a landing page or survey | One |
/batch | A list, or up to 1,000 numbers you supply | Up to 1,000 |
/predefinedsms | A template already built in the portal | One or many |
Send one message
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
| Name | Type | Required | Description |
|---|---|---|---|
Recipient | number | Yes | The destination number, in full international form as a number — 50688887777. |
SenderName | string | Yes | The sender ID shown on the handset. 5 to 11 characters. Outside that range the request is rejected. |
IsBodyEncrypted | boolean | Yes | Send false unless you have specifically agreed encrypted bodies with us. It is not optional — include it. |
Body | string | Yes | The message text. See the note on length and billing above — this is plain text, not Base64. |
PersonalizationSubstitutionTags | array | No | Values substituted into the body. Omit if you do not personalise. |
ScheduleDateTime | date-time | No | Send later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped, and a time in the past is rejected. |
WebHookUrl | string | No | We post delivery reports here as they happen, so you do not have to poll. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted and queued. Returns QueueId and QueuedTimestamp. |
400 | The 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. |
401 | Missing, expired or invalid bearer token. |
417 | A rule rejected the request — see Errors. The body carries status: 0 and a message. Retrying it unchanged will fail again. |
424 | We 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
Sends one message carrying a shortened link to a landing page or survey.
Find ids with List landing pages or List surveys.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
Recipient | number | Yes | The destination number as a number. Must be greater than 99999, otherwise the request fails validation with 400. |
SenderName | string | Yes | The sender ID shown on the handset. 5 to 11 characters. Outside that range the request is rejected. |
IsBodyEncrypted | boolean | Yes | Send false unless you have specifically agreed encrypted bodies with us. It is not optional — include it. |
Body | string | Yes | The message text. See the note on length and billing above — this is plain text, not Base64. |
LandingPageId | guid | No | The landing page or survey to link to. |
Shortner.ShortnerUrl | string | No | The short domain to build the link on. If you send a Shortner object at all, this field inside it is required. |
Shortner.IsUniqueUrl | boolean | No | Whether each recipient gets their own link, so clicks can be attributed individually. |
UnsubscribeContent | string | No | Opt-out text appended to the message. |
ScheduleDateTime | date-time | No | Send later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped, and a time in the past is rejected. |
WebHookUrl | string | No | We post delivery reports here as they happen, so you do not have to poll. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted and queued. Returns QueueId and QueuedTimestamp. |
400 | The 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. |
401 | Missing, expired or invalid bearer token. |
417 | A rule rejected the request — see Errors. The body carries status: 0 and a message. Retrying it unchanged will fail again. |
424 | We 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
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
| Name | Type | Required | Description |
|---|---|---|---|
ListId | array | No | Subscriber lists to send to. Required unless you supply Recipients. |
Recipients | array | No | Up to 1,000 per call. Each entry needs a numeric MobileNo, and every number must be unique. Required unless you supply a ListId. |
Recipients[].MobileNo | number | No | The destination number for that entry, as a number. |
Recipients[].PersonalizationSubstitutionTags | array | No | Per-recipient values substituted into the body. |
SenderName | string | Yes | The sender ID shown on the handset. 5 to 11 characters. Outside that range the request is rejected. |
IsBodyEncrypted | boolean | Yes | Send false unless you have specifically agreed encrypted bodies with us. It is not optional — include it. |
Body | string | Yes | The message text. See the note on length and billing above — this is plain text, not Base64. |
LandingPageId | guid | No | Link the batch to a landing page or survey. |
Shortner.ShortnerUrl | string | No | Required if you send a Shortner object. |
ScheduleDateTime | date-time | No | Send later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped, and a time in the past is rejected. |
WebHookUrl | string | No | We post delivery reports here as they happen, so you do not have to poll. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted and queued. Returns QueueId and QueuedTimestamp. |
400 | The 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. |
401 | Missing, expired or invalid bearer token. |
417 | A rule rejected the request — see Errors. The body carries status: 0 and a message. Retrying it unchanged will fail again. |
424 | We 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
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
| Name | Type | Required | Description |
|---|---|---|---|
PredefinedTemplateId | string | Yes | The template to send, e.g. TSMS-899. |
Recipients | array | No | Each entry needs a numeric MobileNo. Required unless you supply a ListId. |
Recipients[].MobileNo | number | No | The destination number for that entry, as a number. |
ListId | array | No | Send to saved lists instead of explicit numbers. |
ScheduleDateTime | date-time | No | Send later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped, and a time in the past is rejected. |
WebHookUrl | string | No | We post delivery reports here as they happen, so you do not have to poll. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted and queued. Returns QueueId and QueuedTimestamp. |
400 | The 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. |
401 | Missing, expired or invalid bearer token. |
417 | A rule rejected the request — see Errors. The body carries status: 0 and a message. Retrying it unchanged will fail again. |
424 | We 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.
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
| Name | Type | Required | Description |
|---|---|---|---|
id | guid | Yes | The QueueId returned by the send. |
Responses
| Status | Meaning |
|---|---|
200 | Current status, with a per-stage recipient breakdown in Details. |
400 | No send matches that id. Note this is a 400, not a 404. |
401 | Missing, 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
}
}
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
Lists the subscriber lists reachable by SMS, with contact counts.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
excludeCount | boolean | No | Set to true to skip counting, which returns faster on large lists. |
Responses
| Status | Meaning |
|---|---|
200 | The lists, with counts unless you excluded them. |
401 | Missing, expired or invalid bearer token. |
curl -X GET "https://sms-dev.jirafix.net/v1/sms/lists?excludeCount=true" \ -H "Authorization: Bearer <your-token>"
Contact count and segment breakdown for one subscriber list.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
listId | guid | Yes | The subscriber list to count. |
Responses
| Status | Meaning |
|---|---|
200 | The count and segment breakdown. An unknown list also answers 200, with nothing in it — check the payload, not the status code. |
401 | Missing, 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
Lists the landing pages you can link to from a message.
The ids here are what /single and /batch accept as LandingPageId.
Responses
| Status | Meaning |
|---|---|
200 | The available landing pages. |
400 | The request could not be read. |
401 | Missing, expired or invalid bearer token. |
curl -X GET https://sms-dev.jirafix.net/v1/sms/landingpages \ -H "Authorization: Bearer <your-token>"
Surveys
Lists the surveys you can link to from a message.
Responses
| Status | Meaning |
|---|---|
200 | The available surveys. |
400 | The request could not be read. |
401 | Missing, expired or invalid bearer token. |
curl -X GET https://sms-dev.jirafix.net/v1/sms/surveys \ -H "Authorization: Bearer <your-token>"
Trigger templates
Lists the saved templates you can send.
The ids here are what /predefinedsms expects.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
isCalledFromSignUp | boolean | No | Narrows the list to sign-up templates. |
Responses
| Status | Meaning |
|---|---|
200 | The available templates. |
401 | Missing, 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.
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.
| Status | What it means | What to do |
|---|---|---|
202 | Accepted and queued. | Track it with the status endpoint. |
400 | A 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. |
401 | The token is missing, expired or invalid. | Fetch a new token and retry once. |
417 | A rule rejected the request. | Fix the request. Retrying it unchanged will fail again. |
424 | We could not queue the message. Nothing was sent. | Retry. If it persists, contact support. |
What a 417 is telling you
| Message | Cause |
|---|---|
| sms body is required | Body was present but only whitespace. A missing body is a 400, not this. |
| Body is required | The same problem on a batch, worded differently. |
| Sender Name is required | SenderName was missing. |
| Sender From is required | The 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 required | PredefinedTemplateId was missing. |
| Either list or recipients is required to send | A batch had neither a ListId nor Recipients. |
| Aha! batch limit exceeded. Limit is per batch 1000 | More 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 email | A 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 required | You sent a Shortner object without a ShortnerUrl. |
Limits
| Limit | Value |
|---|---|
| Recipients per batch | 1,000 |
| Sender name | 5–11 characters |
| Message length before splitting | 160 GSM-7 characters, or 70 if the body contains Unicode |
| Schedule precision | One minute — seconds are dropped, and past times are rejected |