Send a voice message
Ring a list of phones and play a recording. One call, with the audio in the request — there is no separate upload step unless you want one.
/api/v1/voice/sendvoice_file, voice_id, retry, recipients, callback_url — follow the conventions most Ghanaian voice APIs use, so moving across is usually a new base URL and a new key and nothing more. Two things to check: voice_file is base64 rather than multipart, and we answer 202 rather than 200.Supplying the audio
Exactly one of these three. More than one is rejected rather than us guessing which you meant.
voice_file_type when it is not MP3.POST /api/v1/voice/audio. Use this when you send the same message repeatedly rather than pushing the bytes every time.true. Accepts a real boolean or the string "false", so either convention works. Each attempt is charged again.curl -X POST https://api.jkasms.com/api/v1/voice/send \
-H "Authorization: Bearer $JKASMS_SECRET_KEY" \
-H "X-JKASMS-Org-Id: $JKASMS_ORG_ID" \
-H "Content-Type: application/json" \
-d '{
"voice_file": "'"$(base64 -w0 announcement.mp3)"'",
"voice_file_name": "announcement.mp3",
"voice_id": "0540000000",
"retry": "false",
"recipients": ["0241234567", "0551234567"],
"callback_url": "https://example.com/voice-callback"
}'{
"success": true,
"message": "Voice SMS request sent successfully!",
"data": {
"campaign_id": "cmujohinz0003pz707b91rmmb",
"recipients": [
{ "call_id": "cmujohip70008pz70wdr93wri", "recipient": "+233241234567", "status": "QUEUED" },
{ "call_id": "cmujohip70009pz70a2k1xm4c", "recipient": "+233551234567", "status": "QUEUED" }
],
"rejected": [],
"duration_seconds": 18,
"credits_used": 36,
"credits_label": "36s"
}
}202, not 200. The calls are queued, not placed. Saying “OK” for work that has not happened yet is how a developer concludes a call connected when it is still in a worker queue.
What it costs
credits_used is the recording's length times the number of recipients, in seconds, taken from your voice credit:
18 seconds × 2 recipients = 36 secondsThe length is measured from your file on upload, never taken from the request — a caller who could declare their own duration would be writing their own invoice.
402 INSUFFICIENT_BALANCE, even if you hold thousands of SMS credits. The two balances cannot pay for each other. Price a send first with POST /api/v1/voice/quote, which takes the same body and dials nothing.Outcomes
Call time is reserved on send and settled per call: an answered call is charged, and a call that rings out, is busy or fails releases its seconds back to you. Outcomes arrive as webhooks — voice.call.answered, voice.call.no_answer, voice.call.busy, voice.call.failed — signed the same way as SMS delivery events. See Verifying signatures.
{
"event": "voice.call.answered",
"data": {
"call_id": "cmujohip70008pz70wdr93wri",
"recipient": "+233241234567",
"status": "ANSWERED",
"credits_used": 18,
"actual_duration_seconds": 18,
"retries": 0,
"campaign_id": "cmujohinz0003pz707b91rmmb",
"campaign_name": "Closure notice",
"failure_reason": null,
"submitted_at": "2026-09-27T09:31:02.000Z",
"answered_at": "2026-09-27T09:31:19.000Z",
"failed_at": null
},
"sent_at": "2026-09-27T09:31:19.412Z"
}If you passed callback_url, that URL receives the same payload. Set both and you are called once per destination, not twice for the same one.
The rest of the endpoints
/api/v1/voice/quotePrices a send. Same body, nothing dialled, nothing charged.
/api/v1/voice/audioUploads a recording and returns an audio_file_id plus its measured duration_seconds.
/api/v1/voice/calls/{id}/api/v1/voice/callsOne call, or a cursor-paginated list. Filter by campaign_id, status, recipient, from and to (both YYYY-MM-DD, inclusive).
/api/v1/voice/campaigns/{id}Progress and cost: answered, failed, pending, credits_reserved against credits_used.
/api/v1/voice/campaigns/{id}/cancelStops calls that have not dialled and returns their seconds. Calls already handed to the carrier are on the network and cannot be recalled; the response says how many.
/api/v1/voice/balanceCall time remaining, in seconds.
Test keys
A TEST key routes only to a carrier with a sandbox, so it never rings a real phone. If no sandbox voice route is configured the campaign is accepted and marked as sandbox, and nothing is dialled. See Test mode.