Traitz Sms API
Send SMS to Cameroon numbers from your own software, follow every message to delivery and keep your contacts in sync.
All requests and responses are JSON. Every response carries a request_id to quote when you contact support.
Authentication
Create a key in the dashboard under Developers. It starts with tsms_ and is shown once, so store it in a secret manager.
Send it as a Bearer token. A revoked key stops working immediately.
Authorization: Bearer tsms_ab12cd34_...
Errors
Errors always have the same body. Refused messages are never charged.
{"error": "insufficient_credits", "message": "Not enough credits for this message.", "request_id": "01J..."}
| HTTP | error | Meaning |
|---|---|---|
| 400 | validation_failed | Missing or invalid field, with a field list |
| 401 | unauthorized | Missing, invalid or revoked key |
| 402 | insufficient_credits | Not enough credits for this request |
| 403 | sender_not_allowed | Sender ID not approved for this account |
| 404 | not_found | No resource with this ID in your account |
| 409 | duplicate_request | Same idempotency key with a different body |
| 422 | invalid_recipient | Not a valid Cameroon mobile number. The same status carries recipient_opted_out and content_blocked (words or links not allowed) |
| 429 | rate_limited | Key limit reached, with a Retry-After header |
| 503 | service_unavailable | Provider down, message not accepted |
Webhooks
We POST message.submitted, message.delivered, message.failed
and campaign.completed to your endpoints. Answer with any 2xx within 10 seconds.
Failed deliveries are retried with growing delays for 24 hours, and you can resend any delivery from the dashboard.
Each request has X-TraitzSms-Signature, the HMAC SHA-256 hex of the raw body with your endpoint secret,
and X-TraitzSms-Timestamp, also inside the signed body. Refuse requests older than 5 minutes to block replays.
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $secret);
$valid = hash_equals($expected, $_SERVER['HTTP_X_TRAITZSMS_SIGNATURE'] ?? '');
$fresh = abs(time() - (int) $_SERVER['HTTP_X_TRAITZSMS_TIMESTAMP']) < 300;
import crypto from 'node:crypto';
// rawBody: the exact bytes received, before JSON parsing
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-traitzsms-signature'] ?? ''));
const fresh = Math.abs(Date.now() / 1000 - Number(req.headers['x-traitzsms-timestamp'])) < 300;
import hmac, hashlib, time
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected, request.headers.get('X-TraitzSms-Signature', ''))
fresh = abs(time.time() - int(request.headers['X-TraitzSms-Timestamp'])) < 300
Limits
- 120 requests per minute per key, 600 per account. Over the limit you get 429 with a Retry-After header.
- Bulk sends take up to 1000 recipients per call.
- Your account daily sending limit applies to the API as well as the dashboard.
- Use an
Idempotency-Keyheader on sends so a retried request never sends twice.
Messages
Send one SMS
POST /api/v1/messages
Queues one SMS to a Cameroon mobile number. Without "from", your own approved sender ID is used, or the shared sender while yours is in review.
Body
- to
- Required. Cameroon mobile number in any common format, for example 699000001 or +237699000001.
- message
- Required. Up to 1530 characters. GSM text fits 160 characters per SMS part, Unicode 70.
- from
- Optional. An approved sender ID of your account.
- client_reference
- Optional. Your own reference, up to 100 characters, returned in every status and webhook.
- callback_url
- Optional. HTTPS URL that receives this message's status webhooks.
Headers
- Idempotency-Key
- Optional. Repeating a key with the same body returns the first message instead of sending twice.
curl -X POST https://app.traitzsms.com/api/v1/messages \
-H "Authorization: Bearer $TRAITZ_SMS_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"699000001","message":"Your order 1042 is ready for pickup.","client_reference":"order-1042"}'
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->post('https://app.traitzsms.com/api/v1/messages', ['to' => '699000001', 'message' => 'Your order 1042 is ready for pickup.', 'client_reference' => 'order-1042']);
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"to": "699000001",
"message": "Your order 1042 is ready for pickup.",
"client_reference": "order-1042"
}),
});
const data = await response.json();
import os, requests
response = requests.post(
"https://app.traitzsms.com/api/v1/messages",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
json={"to": "699000001", "message": "Your order 1042 is ready for pickup.", "client_reference": "order-1042"},
)
data = response.json()
Response 201
{
"id": "5b0f6d9e-8a3c-4c2a-9f6e-1d2c3b4a5e6f",
"to": "+237699000001",
"from": "MYBRAND",
"status": "queued",
"segments": 1,
"encoding": "gsm",
"cost_credits": 1,
"error_code": null,
"client_reference": "order-1042",
"created_at": "2026-10-29T14:00:00Z",
"submitted_at": null,
"delivered_at": null,
"failed_at": null
}
Send to many numbers
POST /api/v1/messages/bulk
Up to 1000 recipients per call, with one text for all or one text each. Every recipient is accepted or refused on its own; running out of credits stops the batch.
Body
- messages
- Required. List of recipients: to, optional message, optional client_reference.
- message
- Text used for recipients without their own message.
- from
- Optional. An approved sender ID of your account.
curl -X POST https://app.traitzsms.com/api/v1/messages/bulk \
-H "Authorization: Bearer $TRAITZ_SMS_KEY" \
-H "Content-Type: application/json" \
-d '{"message":"Weekend promotion: 20% off everything.","messages":[{"to":"699000001"},{"to":"677000002","client_reference":"c-2"}]}'
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->post('https://app.traitzsms.com/api/v1/messages/bulk', ['message' => 'Weekend promotion: 20% off everything.', 'messages' => [['to' => '699000001'], ['to' => '677000002', 'client_reference' => 'c-2']]]);
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/messages/bulk', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"message": "Weekend promotion: 20% off everything.",
"messages": [
{
"to": "699000001"
},
{
"to": "677000002",
"client_reference": "c-2"
}
]
}),
});
const data = await response.json();
import os, requests
response = requests.post(
"https://app.traitzsms.com/api/v1/messages/bulk",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
json={"message": "Weekend promotion: 20% off everything.", "messages": [{"to": "699000001"}, {"to": "677000002", "client_reference": "c-2"}]},
)
data = response.json()
Response 201
{
"accepted": 1,
"rejected": 1,
"results": [
{
"index": 0,
"id": "5b0f6d9e-8a3c-4c2a-9f6e-1d2c3b4a5e6f",
"to": "+237699000001",
"from": "MYBRAND",
"status": "queued",
"segments": 1,
"encoding": "gsm",
"cost_credits": 1,
"error_code": null,
"client_reference": "order-1042",
"created_at": "2026-10-29T14:00:00Z",
"submitted_at": null,
"delivered_at": null,
"failed_at": null
},
{
"index": 1,
"error": "invalid_recipient",
"message": "Not a valid Cameroon mobile number."
}
]
}
List messages
GET /api/v1/messages
Newest first, with cursor pagination. Pass next_cursor as cursor to get the next page.
Query
- status
- queued, submitted, sent, delivered or failed
- from
- ISO 8601 date
- to
- ISO 8601 date
- sender
- Sender ID name
- client_reference
- Exact reference
- limit
- 1 to 100, default 50
- cursor
- From the previous page
curl -X GET https://app.traitzsms.com/api/v1/messages \
-H "Authorization: Bearer $TRAITZ_SMS_KEY"
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->get('https://app.traitzsms.com/api/v1/messages');
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/messages', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
},
});
const data = await response.json();
import os, requests
response = requests.get(
"https://app.traitzsms.com/api/v1/messages",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
)
data = response.json()
Response 200
{
"data": [
{
"id": "5b0f6d9e-8a3c-4c2a-9f6e-1d2c3b4a5e6f",
"to": "+237699000001",
"from": "MYBRAND",
"status": "queued",
"segments": 1,
"encoding": "gsm",
"cost_credits": 1,
"error_code": null,
"client_reference": "order-1042",
"created_at": "2026-10-29T14:00:00Z",
"submitted_at": null,
"delivered_at": null,
"failed_at": null
}
],
"next_cursor": "eyJpZCI6MTAyfQ"
}
Get one message
GET /api/v1/messages/{id}
Status, parts, cost, error code and timestamps of one message.
curl -X GET https://app.traitzsms.com/api/v1/messages/5b0f6d9e-8a3c-4c2a-9f6e-1d2c3b4a5e6f \
-H "Authorization: Bearer $TRAITZ_SMS_KEY"
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->get('https://app.traitzsms.com/api/v1/messages/5b0f6d9e-8a3c-4c2a-9f6e-1d2c3b4a5e6f');
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/messages/5b0f6d9e-8a3c-4c2a-9f6e-1d2c3b4a5e6f', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
},
});
const data = await response.json();
import os, requests
response = requests.get(
"https://app.traitzsms.com/api/v1/messages/5b0f6d9e-8a3c-4c2a-9f6e-1d2c3b4a5e6f",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
)
data = response.json()
Response 200
{
"status": "delivered",
"delivered_at": "2026-10-29T14:00:04Z",
"id": "5b0f6d9e-8a3c-4c2a-9f6e-1d2c3b4a5e6f",
"to": "+237699000001",
"from": "MYBRAND",
"segments": 1,
"encoding": "gsm",
"cost_credits": 1,
"error_code": null,
"client_reference": "order-1042",
"created_at": "2026-10-29T14:00:00Z",
"submitted_at": null,
"failed_at": null
}
Account
List sender IDs
GET /api/v1/sender-ids
Your sender IDs and their approval status, plus the shared sender.
curl -X GET https://app.traitzsms.com/api/v1/sender-ids \
-H "Authorization: Bearer $TRAITZ_SMS_KEY"
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->get('https://app.traitzsms.com/api/v1/sender-ids');
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/sender-ids', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
},
});
const data = await response.json();
import os, requests
response = requests.get(
"https://app.traitzsms.com/api/v1/sender-ids",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
)
data = response.json()
Response 200
{
"data": [
{
"name": "MYBRAND",
"usage": "transactional",
"status": "approved",
"shared": false
}
]
}
Get the balance
GET /api/v1/balance
Credits left. One credit sends one SMS part.
curl -X GET https://app.traitzsms.com/api/v1/balance \
-H "Authorization: Bearer $TRAITZ_SMS_KEY"
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->get('https://app.traitzsms.com/api/v1/balance');
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/balance', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
},
});
const data = await response.json();
import os, requests
response = requests.get(
"https://app.traitzsms.com/api/v1/balance",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
)
data = response.json()
Response 200
{
"credits": 12450,
"approximate_sms": 12450,
"daily_limit": 5000
}
Contacts
List contacts
GET /api/v1/contacts
Search by name or number, filter by group name, with cursor pagination.
Query
- q
- Name or number
- group
- Group name
- limit
- 1 to 100, default 50
- cursor
- From the previous page
curl -X GET https://app.traitzsms.com/api/v1/contacts \
-H "Authorization: Bearer $TRAITZ_SMS_KEY"
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->get('https://app.traitzsms.com/api/v1/contacts');
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/contacts', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
},
});
const data = await response.json();
import os, requests
response = requests.get(
"https://app.traitzsms.com/api/v1/contacts",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
)
data = response.json()
Response 200
{
"data": [
{
"id": "01a1...",
"phone": "+237699000001",
"first_name": "Awa",
"last_name": "Ndongo",
"opted_out": false,
"groups": [
"VIP"
]
}
],
"next_cursor": null
}
Create or update a contact
POST /api/v1/contacts
Creates the contact, or updates the names when the number is already saved. Groups are matched by name and created when missing.
Body
- phone
- Required. Cameroon mobile number.
- first_name
- Optional.
- last_name
- Optional.
- groups
- Optional. List of group names.
curl -X POST https://app.traitzsms.com/api/v1/contacts \
-H "Authorization: Bearer $TRAITZ_SMS_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"699000001","first_name":"Awa","groups":["VIP"]}'
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->post('https://app.traitzsms.com/api/v1/contacts', ['phone' => '699000001', 'first_name' => 'Awa', 'groups' => ['VIP']]);
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/contacts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"phone": "699000001",
"first_name": "Awa",
"groups": [
"VIP"
]
}),
});
const data = await response.json();
import os, requests
response = requests.post(
"https://app.traitzsms.com/api/v1/contacts",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
json={"phone": "699000001", "first_name": "Awa", "groups": ["VIP"]},
)
data = response.json()
Response 201
{
"id": "01a1...",
"phone": "+237699000001",
"first_name": "Awa",
"last_name": null,
"opted_out": false,
"groups": [
"VIP"
]
}
Webhooks
List webhook endpoints
GET /api/v1/webhooks
Endpoints that receive your events. Secrets are never listed.
curl -X GET https://app.traitzsms.com/api/v1/webhooks \
-H "Authorization: Bearer $TRAITZ_SMS_KEY"
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->get('https://app.traitzsms.com/api/v1/webhooks');
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/webhooks', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
},
});
const data = await response.json();
import os, requests
response = requests.get(
"https://app.traitzsms.com/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
)
data = response.json()
Response 200
{
"data": [
{
"id": "01a2...",
"url": "https://example.cm/sms-events",
"events": [
"message.delivered",
"message.failed"
],
"active": true,
"created_at": "2026-10-29T14:00:00Z"
}
]
}
Add a webhook endpoint
POST /api/v1/webhooks
The signing secret is returned once, in this response. Store it to check the X-TraitzSms-Signature header.
Body
- url
- Required. HTTPS URL.
- events
- Required. Any of message.submitted, message.delivered, message.failed, campaign.completed.
curl -X POST https://app.traitzsms.com/api/v1/webhooks \
-H "Authorization: Bearer $TRAITZ_SMS_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.cm/sms-events","events":["message.delivered","message.failed"]}'
$response = Http::withToken(env('TRAITZ_SMS_KEY'))
->post('https://app.traitzsms.com/api/v1/webhooks', ['url' => 'https://example.cm/sms-events', 'events' => ['message.delivered', 'message.failed']]);
$data = $response->json();
const response = await fetch('https://app.traitzsms.com/api/v1/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TRAITZ_SMS_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://example.cm/sms-events",
"events": [
"message.delivered",
"message.failed"
]
}),
});
const data = await response.json();
import os, requests
response = requests.post(
"https://app.traitzsms.com/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['TRAITZ_SMS_KEY']}"},
json={"url": "https://example.cm/sms-events", "events": ["message.delivered", "message.failed"]},
)
data = response.json()
Response 201
{
"id": "01a2...",
"url": "https://example.cm/sms-events",
"events": [
"message.delivered",
"message.failed"
],
"active": true,
"created_at": "2026-10-29T14:00:00Z",
"secret": "whsec_..."
}
Traitz Sms, by Traitz Tech. Made in Cameroon.