Kilo SMS API
من نفس الـ API تقدر تبعت لرقم واحد أو لحد 500,000 رقم مصري. هات رقم السيندر من حسابك، اعمل معاينة، وبعد مراجعة عدد الرسائل والتكلفة أكّد الإرسال باستخدام رقم الدفعة. تابع الحالة بنفس الرقم. لو طلب التأكيد انقطع، ابعت نفس رقم الدفعة تاني.
Authentication
Use HTTPS and send your 64-character hexadecimal API key in the Authorization header on every request. Create or replace the key at /KiloSmsApi/Key. Copy it when issued. Replacing it revokes the previous key.
A frozen account can call Senders and Status with its existing key. Preview, Send, and Discard return 403 account_restricted. A blocked account receives 401 unauthorized on all endpoints. Password and email changes revoke browser sessions. Manage API keys separately on the key page.
Authorization: Bearer YOUR_API_KEY
Accept: application/json
The base URL in the examples is your Kilo SMS installation. If the app is under a virtual directory, include that directory in the base URL. The API returns UTF-8 JSON.
Endpoints
| Method and path | Request | Successful response |
|---|---|---|
| GET /KiloSmsApi/Senders | Bearer header | 200 with the sender IDs enabled for your account |
| POST /KiloSmsApi/Preview | Form fields SenderId, Body and Recipients, or a multipart upload | 201 with a draft ID, validation counts and estimated cost |
| POST /KiloSmsApi/Send | URL-encoded BatchId | 202 while queued or processing. 200 if already complete |
| GET /KiloSmsApi/Status | batchId query parameter | 200 with the draft or sending state |
| POST /KiloSmsApi/Discard | URL-encoded BatchId | 200 with success true for an unconfirmed draft |
Complete sending example
This Bash example uses cURL 7.76 or newer and jq. Set KILOSMS_BASE_URL and KILOSMS_API_KEY in your environment. Set KILOSMS_SENDER_ID to an approved sender ID and KILOSMS_RECIPIENT to an Egyptian mobile number you control. The same workflow applies when you preview a file of recipients.
#!/usr/bin/env bash
set -euo pipefail
: "${KILOSMS_BASE_URL:?Set your HTTPS Kilo SMS installation URL}"
: "${KILOSMS_API_KEY:?Set your API key}"
: "${KILOSMS_SENDER_ID:?Set your enabled sender ID}"
: "${KILOSMS_RECIPIENT:?Set an Egyptian test number you control}"
api="${KILOSMS_BASE_URL%/}/KiloSmsApi"
api_json() {
local response
if response="$(curl --silent --show-error --fail-with-body \
--header "Authorization: Bearer $KILOSMS_API_KEY" \
--header "Accept: application/json" "$@")"; then
printf '%s\n' "$response"
else
printf '%s\n' "$response" >&2
return 1
fi
}
api_json "$api/Senders" | jq .
preview="$(api_json "$api/Preview" \
--data-urlencode "SenderId=$KILOSMS_SENDER_ID" \
--data-urlencode "Body=Kilo SMS test" \
--data-urlencode "Recipients=$KILOSMS_RECIPIENT")"
printf '%s\n' "$preview" | jq .
batch_id="$(jq -er '.batchId' <<< "$preview")"
printf 'Save this preview ID for any confirmation retry: %s\n' "$batch_id"
read -r -p 'Queue this preview? [y/N] ' answer
if [[ "$answer" != "y" && "$answer" != "Y" ]]; then
api_json "$api/Discard" --data-urlencode "BatchId=$batch_id" | jq .
exit 0
fi
sending="$(api_json "$api/Send" --data-urlencode "BatchId=$batch_id")"
printf '%s\n' "$sending" | jq .
batch_id="$(jq -er '.batchId' <<< "$sending")"
for ((attempt=1; attempt<=30; attempt++)); do
status="$(api_json --get "$api/Status" --data-urlencode "batchId=$batch_id")"
printf '%s\n' "$status" | jq .
if [[ "$(jq -er '.state' <<< "$status")" == "complete" ]]; then
exit 0
fi
sleep 3
done
printf 'Still queued or processing. Continue polling batch %s.\n' "$batch_id"
Preview a recipient file
Upload one candidate per line in a UTF-8 text file. Comma-separated entries are also accepted. A file can contain up to 500,000 candidate entries before filtering. Use the returned batchId in the same Send, Status and Discard requests.
curl --silent --show-error --fail-with-body \
--header "Authorization: Bearer $KILOSMS_API_KEY" \
--header "Accept: application/json" \
--form-string "SenderId=$KILOSMS_SENDER_ID" \
--form "BodyFile=@message.txt;type=text/plain" \
--form "RecipientsFile=@recipients.txt;type=text/plain" \
"${KILOSMS_BASE_URL%/}/KiloSmsApi/Preview"
| Preview field | Value |
|---|---|
| SenderId | Positive integer from Senders |
| Body or BodyFile | UTF-8 SMS text. One field per request. Limit 1,000 UTF-16 units after converting line breaks to CRLF. BodyFile is a multipart upload |
| Recipients or RecipientsFile | Egyptian mobile numbers in text, from one to 500,000 candidate entries. One field per request. RecipientsFile is a multipart UTF-8 upload |
| Content-Type | Use application/x-www-form-urlencoded for text fields, or multipart/form-data for files. Let your HTTP client generate the multipart boundary |
Use Recipients with a newline or comma between numbers in a small request. For large inputs use RecipientsFile. The recipient text limit is 12,000,000 UTF-16 units and the recipient file limit is 36,000,000 UTF-8 bytes. IIS or a proxy may reject a request before it reaches the API.
Response examples
Senders · 200
{
"senders": [
{"senderId": 17, "senderName": "KILO SMS"}
]
}
Preview · 201
This example represents a preview with two eligible recipients, one duplicate, one invalid entry and one recipient blocked by sender permissions. A successful preview reserves zero credits and expires after 30 minutes.
{
"batchId": "11223344-5566-4788-99aa-bbccddeeff00",
"entered": 5,
"invalid": 1,
"duplicates": 1,
"legacyConverted": 0,
"blocked": 1,
"eligible": 2,
"unitsPerRecipient": 1,
"totalUnits": 2,
"totalPrice": 0.50,
"walletUnits": 10000,
"networks": [
{"name": "Vodafone", "prefix": "2010", "eligible": 1, "blocked": 0},
{"name": "e& / Etisalat", "prefix": "2011", "eligible": 1, "blocked": 0},
{"name": "WE", "prefix": "2015", "eligible": 0, "blocked": 1}
],
"expiresUtc": "2026-11-08T13:00:00.0000000Z",
"textAnalysis": {
"language": "english",
"encoding": "gsm7",
"characters": 13,
"units": 1,
"encodingUnits": 13,
"lineBreaks": 0,
"issueCount": 0,
"issues": [],
"error": null
},
"error": null
}
- entered counts candidates before filtering. invalid, duplicates, blocked and eligible account for the candidates. legacyConverted counts normalized legacy numbers and can overlap eligible or blocked.
- blocked includes numbers with a missing or disabled network route or a sender without approval for that route. An unmatched prefix has no per-network row.
- totalUnits is eligible × unitsPerRecipient. totalPrice is the preview value in EGP. walletUnits is the available credit balance when the response is generated.
- textAnalysis.language is arabic, english, other or empty. encoding is gsm7, unicode or empty. characters is the normalized UTF-16 length. encodingUnits is the GSM-7 septet count or the Unicode UTF-16 unit count. units is the number of parts per recipient.
- Each issues entry has character, codePoint, kind, count, replacement and position. kind can be extension, unicode or blocked. position is the first occurrence’s zero-based UTF-16 index in the normalized text. A hidden character can have an empty character field. issueCount counts all distinct code points with issues, while issues returns up to 12 entries. A replacement is a suggestion for the caller to apply.
Preview with blocked text · 422
The example body contains an invisible U+200B between A and B.
{
"error": "invalid_text",
"textAnalysis": {
"language": "english",
"encoding": "empty",
"characters": 3,
"units": 0,
"encodingUnits": 0,
"lineBreaks": 0,
"issueCount": 1,
"issues": [
{
"character": "",
"codePoint": "U+200B",
"kind": "blocked",
"count": 1,
"replacement": null,
"position": 1
}
],
"error": "invalid_text"
}
}
Preview with zero eligible recipients · 422
{
"batchId": null,
"entered": 1,
"invalid": 0,
"duplicates": 0,
"legacyConverted": 0,
"blocked": 1,
"eligible": 0,
"unitsPerRecipient": 1,
"totalUnits": 0,
"totalPrice": 0,
"walletUnits": 10000,
"networks": [
{"name": "WE", "prefix": "2015", "eligible": 0, "blocked": 1}
],
"expiresUtc": null,
"textAnalysis": {
"language": "english",
"encoding": "gsm7",
"characters": 13,
"units": 1,
"encodingUnits": 13,
"lineBreaks": 0,
"issueCount": 0,
"issues": [],
"error": null
},
"error": "no_recipients"
}
Send · 202
{
"batchId": "11223344-5566-4788-99aa-bbccddeeff00",
"state": "queued",
"eligible": 2,
"accepted": 0,
"rejected": 0,
"unknown": 0,
"dispatchDelayed": false,
"walletUnits": 9998,
"pending": 2
}
Status or completed Send retry · 200
{
"batchId": "11223344-5566-4788-99aa-bbccddeeff00",
"state": "complete",
"eligible": 2,
"accepted": 1,
"rejected": 0,
"unknown": 1,
"dispatchDelayed": false,
"walletUnits": 9998,
"pending": 0
}
| State or field | Meaning |
|---|---|
| draft | Preview exists and awaits confirmation |
| queued | Confirmed and waiting for the worker |
| processing | Worker has started processing the batch |
| complete | Every eligible recipient has a recorded gateway outcome |
| accepted | Gateway accepted the submission. Handset delivery is a separate event |
| rejected | A definitive rejection or confirmed inability to send. The associated credit is released back to the original top-up lots |
| unknown | Submission outcome could not be confirmed. The worker records it for review and skips automatic re-send |
| pending | Eligible recipients whose outcomes are still pending, including in-flight work |
| dispatchDelayed | True when the batch remains queued for more than one minute |
Send reserves credit at confirmation. Repeating Send with the same BatchId returns its current state and uses its existing reservation. If an equivalent recent batch already exists, Send can return that batch’s canonical ID. Save the batchId in the response and use it for subsequent Status requests. On a timeout or 503 after calling Send, retry using the same preview ID.
Discard · 200
{"success": true}
Discard expires an unconfirmed preview. Repeating it for an already expired or absent draft also succeeds. A confirmed batch returns already_submitted.
Error responses
Application errors return an error code as JSON. For example, Send with an insufficient wallet returns HTTP 402.
{"error": "insufficient_balance"}
| HTTP | error | What to do |
|---|---|---|
| 400 | invalid_request | Check the fields, sender ID, batch UUID and duplicate parameters |
| 401 | unauthorized | Supply a valid Bearer key. The response includes WWW-Authenticate: Bearer |
| 402 | insufficient_balance | Add credit and retry the same preview while it remains valid |
| 403 | https_required | Use the HTTPS installation URL |
| 403 | sender_not_approved | Choose an enabled sender ID from Senders |
| 403 | account_restricted | Check the account state in the customer workspace. If a key changed while a write was in progress, use the current key |
| 404 | not_found | Check the batch ID and the account that owns it. An expired draft may already have been cleaned up |
| 409 | preview_changed | Create a fresh preview after text, routing or pricing rules change |
| 409 | already_submitted | Use Status for the confirmed batch |
| 410 | expired | Create a new preview |
| 413 | too_many_numbers | Split the list or reduce the upload size |
| 415 | invalid_content_type | Choose the supported URL-encoded form or multipart request format |
| 422 | empty_text | Provide nonempty SMS text |
| 422 | text_too_long | Reduce the normalized text to 1,000 UTF-16 units |
| 422 | invalid_text | Review the blocked code points in textAnalysis |
| 422 | invalid_encoding | Save the uploaded text as UTF-8 |
| 422 | no_recipients | Review invalid, duplicate and blocked counts, then correct the recipient input or sender permissions |
| 429 | too_many_drafts | Discard unused previews or wait for expiration and cleanup |
| 503 | pricing_unavailable | Ask the administrator to configure the price |
| 503 | unavailable | Retry a transient failure. For a Send retry keep the same BatchId |