Skip to content

Send a voice message

POSThttps://api.9bits.net/voice/v1/voice/sendvoice

FieldTypeRequiredDescription
toarrayDestinations in international format. Max 1,000 unique numbers.
media_urlstringPublicly reachable URL of the audio to play. Fetched at dispatch time.
fromstringCaller ID. Defaults to your registered one; chosen at random if you have several.
Terminal window
curl -X POST https://api.9bits.net/voice/v1/voice/sendvoice \
-H "Authorization: Bearer $NINEBITS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": ["2348020000000", "2348030000000"],
"media_url": "https://cdn.example.com/audio/reminder.mp3",
"from": "2349000000000"
}'
{
"status_code": 200,
"message": "Request is being processed",
"ref_id": "9azmf1ntcl358gczduzz4rs0u"
}

ref_id is the only thing worth keeping. It’s what correlates this request with the call events we push to your webhook. Store it against whatever business record triggered the call.

CodeMeaning
200Request accepted
201Resource created
401Authentication failed
405Method not allowed
429Rate limited
500Server error
{
"status_code": 401,
"message": "Authentication failed: Signature verification failed"
}

Every call attempt produces an event, pushed to the URL you set under Settings → Webhook. This is the only way to get call outcomes — there’s no polling endpoint.

{
"start_at": "2026-07-14T18:48:14+01:00",
"answer_at": "2026-07-14T18:48:21+01:00",
"end_at": "2026-07-14T18:48:53+01:00",
"total_duration": 39,
"call_duration": 32,
"call_direction": "outbound",
"from_number": "2349000000000",
"to_number": "2348020000000",
"total_amount": "48.00"
}
FieldMeaning
start_atWhen we placed the call.
answer_atWhen it was answered. Absent if never answered — that’s how you detect a missed call.
end_atWhen it ended.
total_durationWall-clock seconds, including ring time.
call_durationBilled seconds — connected time only.
call_directionoutbound or inbound.
from_numberCaller ID used.
to_numberDestination.
total_amountAmount charged.
app.post('/webhooks/9bits/voice', async (req, res) => {
const evt = req.body;
res.sendStatus(200); // acknowledge first
const answered = Boolean(evt.answer_at);
await db.query(
`INSERT INTO voice_calls
(to_number, answered, billed_seconds, amount, ended_at)
VALUES ($1, $2, $3, $4, $5)`,
[evt.to_number, answered, evt.call_duration, evt.total_amount, evt.end_at],
);
if (!answered) await scheduleRetry(evt.to_number);
});

See the Webhooks guide for retries, acknowledgement, and de-duplication.

  • Host it at a stable, public HTTPS URL. We fetch at dispatch, not at request time.
  • We transcode to the best format for GSM voice — you don’t need to pre-convert.
  • Keep it short. People hang up; every extra second is billed on the calls that don’t.

POST /voice/v1/voice/sendvoice does not support Idempotency-Key. A retry after a timeout can place duplicate calls.

Guard it yourself: write an “attempted” row keyed on your own business ID before calling, and check it before retrying. See Idempotency & retries.