Send a message
Queue one or more SMS for delivery.
/api/v1/sms/sendSENDER_NOT_APPROVED.template_id instead.message. Send exactly one of the two — both, or neither, is a VALIDATION_ERROR.024…, 233… and +233… are all accepted and normalised; duplicates across formats are removed before you are charged.TRANSACTIONAL (default), PROMOTIONAL or OTP. Promotional messages skip recipients who have opted out.{{placeholders}} in the message.Request
curl -X POST https://api.jkasms.com/api/v1/sms/send \
-H "Authorization: Bearer $JKASMS_SECRET_KEY" \
-H "X-JKASMS-Org-Id: $JKASMS_ORG_ID" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-8821-reminder" \
-d '{
"sender": "JKASMS",
"message": "Your appointment is confirmed for tomorrow at 10:00am.",
"recipients": ["0241234567", "0551234567"]
}'{
"success": true,
"data": {
"campaign_id": null,
"accepted": 2,
"rejected": 0,
"duplicates_removed": 0,
"total_segments": 2,
"total_cost": "0.12",
"currency": "GHS",
"messages": [
{
"message_id": "cmu9x2k4a0001abcd",
"recipient": "+233241234567",
"segments": 1,
"cost": "0.06",
"status": "QUEUED"
}
],
"rejected_recipients": []
}
}GET /api/v1/sms/:id or receive a delivery webhook.Rejected recipients
Invalid numbers are reported in rejected_recipients rather than failing the whole request — one typo in a list of 500 should not stop the other 499. You are not charged for them.
"rejected_recipients": [
{ "recipient": "0999999999", "reason": "Unknown Ghanaian mobile prefix \"09\"" },
{ "recipient": "+233241234567", "reason": "Recipient has opted out of promotional messages" }
]Personalization
Substitution happens before segmentation, so you are billed for the rendered text. A long value can push a message into an extra segment.
{
"sender": "SCHOOL",
"message": "Dear {{name}}, fees of GHS {{amount}} are due on {{date}}.",
"recipients": ["0241234567", "0551234567"],
"personalization": {
"+233241234567": { "name": "Ama", "amount": "450", "date": "30 Sep" },
"+233551234567": { "name": "Kwame", "amount": "380", "date": "30 Sep" }
}
}A placeholder with no matching value renders as an empty string rather than leaving {{name}} visible to the recipient.
Sending from a template
Replace message with template_id to use a saved body. Personalization works identically — the template is rendered per recipient, then segmented, so you are billed for the rendered text.
{
"sender": "SCHOOL",
"template_id": "cmuaxq0of000rtwq8n65tld2n",
"recipients": ["0241234567"],
"personalization": {
"+233241234567": { "name": "Ama", "amount": "450", "date": "30 Sep" }
}
}template_id belonging to another account returns 404 NOT_FOUND, the same as one that does not exist. Templates routinely contain balances and account numbers, so we never confirm that someone else's id is real.Idempotency
Send an Idempotency-Key on anything you might retry. If your HTTP client times out after we already queued the message, retrying with the same key returns the original result instead of sending twice.
| Situation | Result |
|---|---|
| Same key, same body, completed | Original response replayed. Nothing sent. |
| Same key, same body, still in flight | 409 REQUEST_IN_PROGRESS. Retry shortly. |
| Same key, different body | 422 DUPLICATE_REQUEST. This is a bug in your code and hiding it would be worse. |
Keys are remembered for 24 hours.
Errors
| Code | Status | Meaning |
|---|---|---|
SENDER_NOT_APPROVED | 403 | Sender ID exists but is not approved yet |
INVALID_SENDER | 400 | Sender ID is not registered on your account |
INSUFFICIENT_BALANCE | 402 | Not enough credit for the whole send |
INVALID_PHONE_NUMBER | 400 | No valid recipients in the request |
RECIPIENT_SUPPRESSED | 400 | Every recipient has opted out |