Authentication
Each interconnection has its own username and password, issued by Techalpha. Request credentials from Techalpha for every new interconnection. Credentials must be sent with every API request; requests without valid credentials are rejected as unauthorized.
Endpoint
Requests can be sent using GET or POST to the URL issued for your interconnection, in this format:
https://<host>:<port>/api?For POST requests, the URL must not include "api?". HTTPS is recommended; plain HTTP is supported but sends credentials unencrypted.
SMS submission
Request format
https://<host>:<port>/api?username=<username>&password=<password>&ani=<ani>&dnis=<dnis>&message=<message>&command=submit&serviceType=<serviceType>&longMessageMode=<longMessageMode>Parameters
| Parameter | Required | Description |
|---|---|---|
| username | Yes | Login issued for your interconnection. |
| password | Yes | Password issued for your interconnection. |
| ani | Yes | Sender ID (caller ID). Alphanumeric, up to 32 characters. Destination routes may apply stricter limits; many carriers cap alphanumeric sender IDs at 11 characters. |
| dnis | Yes | Destination number in international E.164 format, up to 15 digits. |
| message | Yes | Message text. URL-encode the value when sending via GET. |
| command | Yes | Request type. Must be set to submit. |
| serviceType | Yes | Service type issued for your interconnection. The parameter must be present but may be left blank. |
| longMessageMode | No | How long messages are handled. Default: 1 (cut). See below. |
Long message handling
| Value | Mode | Behavior |
|---|---|---|
| 1 | cut (default) | Trims the message to the first 140 bytes and sends only that. The rest of the text is dropped. |
| 2 | split | Splits the message into segments using a UDH header. |
| 3 | split_sar | Splits the message into segments using SAR parameters. |
| 4 | payload | Sends the full text in the message_payload field via SMPP. |
Important: the default (1, cut) silently truncates long messages. Set longMessageMode to 2 or 3 explicitly if your messages may exceed one segment.
When splitting (modes 2 and 3), segment size depends on the encoding:
| dataCoding | Single message limit | Per segment when split |
|---|---|---|
| 0 (GSM 7-bit) | 160 characters | 152 characters |
| 1 to 7 | 140 characters | 134 characters |
| 8 (UCS-2 / Unicode) | 70 characters (140 bytes) | 67 characters (134 bytes) |
Segment count = message length divided by the per-segment size, rounded up.
Responses
| HTTP status | Meaning | Response body |
|---|---|---|
| 200 OK | Message accepted | JSON with the message ID, e.g. {"message_id":"alss-a1b2c3d4-e5f67890"} |
| 400 Bad Request | Rejected: no compatible route, or unknown username | Plain-text reason, e.g. NO ROUTES or Unknown username |
| 401 Unauthorized | Incorrect password | Plain-text reason, e.g. Incorrect password |
Successful responses are returned with Content-Type: text/html; charset=UTF-8 even though the body is JSON. Parse the body as JSON regardless of the header.
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
{"message_id":"alss-a1b2c3d4-e5f67890"}Code examples
These examples use GET with URL-encoded values. The POST body format is not yet part of this specification; confirm it with your account team before you switch to POST.
curl --get "https://<host>:<port>/api" \
--data-urlencode "username=<username>" \
--data-urlencode "password=<password>" \
--data-urlencode "ani=<ani>" \
--data-urlencode "dnis=<dnis>" \
--data-urlencode "message=<message>" \
--data-urlencode "command=submit" \
--data-urlencode "serviceType=" \
--data-urlencode "longMessageMode=2"import json
import requests
params = {
"username": "<username>",
"password": "<password>",
"ani": "<ani>",
"dnis": "<dnis>",
"message": "<message>",
"command": "submit",
"serviceType": "", # must be present, may be blank
"longMessageMode": 2, # split instead of the default cut
}
# requests URL-encodes every parameter value
resp = requests.get("https://<host>:<port>/api", params=params, timeout=30)
if resp.status_code == 200:
# Body is JSON even though Content-Type is text/html
message_id = json.loads(resp.text)["message_id"]
print("Accepted:", message_id)
else:
# 400: NO ROUTES or Unknown username. 401: Incorrect password.
print("Rejected:", resp.status_code, resp.text)const params = new URLSearchParams({
username: "<username>",
password: "<password>",
ani: "<ani>",
dnis: "<dnis>",
message: "<message>",
command: "submit",
serviceType: "", // must be present, may be blank
longMessageMode: "2", // split instead of the default cut
});
// URLSearchParams URL-encodes every parameter value
const res = await fetch(`https://<host>:<port>/api?${params}`);
const body = await res.text();
if (res.ok) {
// Body is JSON even though Content-Type is text/html
const { message_id } = JSON.parse(body);
console.log("Accepted:", message_id);
} else {
// 400: NO ROUTES or Unknown username. 401: Incorrect password.
console.error("Rejected:", res.status, body);
}Delivery status request
Request format
https://<host>:<port>/api?username=<username>&password=<password>&messageId=<messageId>&command=queryParameters
All parameters are mandatory.
| Parameter | Description |
|---|---|
| username | Login issued for your interconnection. |
| password | Password issued for your interconnection. |
| messageId | Message ID returned in the submit response. |
| command | Request type. Must be set to query. |
Status can be requested within 48 hours of submission.
Status values
| Status | Meaning |
|---|---|
| ENROUTE | Message is in the routing stage. |
| SENT | Message was delivered to the SMSC. |
| ACCEPTD | Message was accepted by the SMSC. |
| DELIVRD | Message was delivered to the subscriber. |
| UNDELIV | Message could not be delivered. |
| EXPIRED | Message storage period expired. |
| DELETED | Message was deleted. |
| REJECTD | Message was rejected by the SMSC. |
| UNKNOWN | Message status is unknown. |
Responses
| HTTP status | Meaning | Response body |
|---|---|---|
| 200 OK | Status returned | JSON, e.g. {"status":"DELIVRD"} |
| 200 OK | Message ID not found | JSON with an error in the status field, e.g. {"status":"message ID alss-some-message-id not found"} |
| 400 Bad Request | Invalid request, e.g. unknown username | Plain-text reason, e.g. Unknown username |
| 401 Unauthorized | Incorrect password | Plain-text reason, e.g. Incorrect password |
A "not found" result returns 200 OK, not 404. Check the value of status against the list above before treating a response as a valid delivery status.
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
{"status":"DELIVRD"}Code examples
curl --get "https://<host>:<port>/api" \
--data-urlencode "username=<username>" \
--data-urlencode "password=<password>" \
--data-urlencode "messageId=<messageId>" \
--data-urlencode "command=query"import json
import requests
STATUSES = {"ENROUTE", "SENT", "ACCEPTD", "DELIVRD", "UNDELIV",
"EXPIRED", "DELETED", "REJECTD", "UNKNOWN"}
params = {
"username": "<username>",
"password": "<password>",
"messageId": "<messageId>",
"command": "query",
}
resp = requests.get("https://<host>:<port>/api", params=params, timeout=30)
if resp.status_code == 200:
status = json.loads(resp.text)["status"]
if status in STATUSES:
print("Status:", status)
else:
# A missing message ID also returns 200, with an error string
print("Lookup failed:", status)
else:
print("Rejected:", resp.status_code, resp.text)const STATUSES = new Set([
"ENROUTE", "SENT", "ACCEPTD", "DELIVRD", "UNDELIV",
"EXPIRED", "DELETED", "REJECTD", "UNKNOWN",
]);
const params = new URLSearchParams({
username: "<username>",
password: "<password>",
messageId: "<messageId>",
command: "query",
});
const res = await fetch(`https://<host>:<port>/api?${params}`);
const body = await res.text();
if (res.ok) {
const { status } = JSON.parse(body);
if (STATUSES.has(status)) {
console.log("Status:", status);
} else {
// A missing message ID also returns 200, with an error string
console.error("Lookup failed:", status);
}
} else {
console.error("Rejected:", res.status, body);
}Delivery reports and throughput
Delivery reports (DLR)
Delivery report settings are configured per customer, per connection. Contact your Techalpha account team to set up DLR for your interconnection. Delivery status can also be polled at any time within 48 hours using command=query.
Throughput
Each connection has a default limit of 20 messages per second (TPS). Higher throughput is available on request and is subject to commercial discussion with your Techalpha account team.
Integration notes and best practices
- Use HTTPS; prefer POST once the body format is confirmed with your account team. With GET, credentials and message text appear in the URL and can be stored in proxy and server logs.
- Set
longMessageModeexplicitly. The default (1, cut) truncates long messages without warning. Use2or3for messages that may exceed one segment. - Parse response bodies as JSON. Successful responses carry
Content-Type: text/htmlbut the body is JSON. - Read the body on 400 errors. Both routing failures (
NO ROUTES) and authentication failures (Unknown username) return 400. Use the body text to tell them apart. - Validate status values. A message ID that is not found returns 200 OK with an error string in
status. Treat any value outside the status list as an error. - Query status within 48 hours. Status is not available after 48 hours from submission.
- Check sender ID length per destination. The platform accepts up to 32 characters, but many destinations limit alphanumeric sender IDs to 11 characters.
- URL-encode parameter values on GET requests, especially
messageandani.