Blooio Voice
Place and answer phone calls on Blooio numbers, and choose who takes the audio: your WebSocket server, a Blooio agent, or a teammate's softphone.
Blooio Voice puts phone calls on your organization's Blooio numbers. Every call has a handler that takes its audio once it's answered:
| Handler | Who talks |
|---|---|
stream |
Your WebSocket server, speaking the Blooio media stream protocol. |
agent |
The Blooio agent that owns the number. |
softphone |
A teammate in the dashboard, in their browser. |
A number gets Blooio Voice with the voice product: buy it with "products": ["voice"] (or ["sms", "voice"]), or add voice to a number you hold with PATCH /v4/numbers/{number}. See Blooio SMS for searching and buying.
Note Blooio Voice is enabled per organization. Until it's enabled on yours, placing and controlling calls returns
403 feature_not_enabled.
Inbound calls
Who answers a number's inbound calls is part of its voice channel's settings:
curl -X PATCH 'https://api.blooio.com/v4/channels/ch_.../settings' \
-H 'Authorization: Bearer bl_live_...' \
-H 'Content-Type: application/json' \
-d '{ "inbound": { "handler": "api", "ring_timeout_seconds": 30 } }'Try itinbound.handler |
What happens |
|---|---|
softphone |
Rings every teammate's dashboard softphone; the first to pick up takes it. |
agent |
The number's Blooio agent answers. Setting it needs a live agent that owns the number (409 agent_unavailable otherwise). Calls with a withheld caller ID are refused, and so are calls while the agent isn't live. |
api |
The call rings and Blooio sends call.ringing; answer it with POST /v4/calls/{call_id}/answer and a handler. |
ring_timeout_seconds (5 to 120, default 30) is how long a call rings before it ends missed.
Outbound calls
POST /v4/calls dials from one of your voice numbers and hands the call to a handler once it's answered:
curl -X POST 'https://api.blooio.com/v4/calls' \
-H 'Authorization: Bearer bl_live_...' \
-H 'Content-Type: application/json' \
-d '{
"from": "+14155550123",
"to": "+12125550199",
"handler": { "type": "stream", "url": "wss://voice.example.com/calls", "auth_token": "s3cret" }
}'Try itThe response is 201 with the call (call_...), queued while the dial is placed.
Controlling a call
| Endpoint | What it does |
|---|---|
POST /v4/calls/{call_id}/hangup |
Ends the call. |
POST /v4/calls/{call_id}/transfer |
Moves the call to another handler, or with to to a phone number, where it leaves Blooio once that number answers. |
POST /v4/calls/{call_id}/dtmf |
Plays keypad digits into the call (0-9, *, #, A-D; w pauses half a second, W a second). |
A call's status runs queued, ringing, in_progress, then completed. One that ends unanswered is missed, and one whose dial was refused is failed. When Blooio ends a call, error.code says why: handler_disconnected, call_time_limit (a call lasts at most 4 hours), handoff_unanswered, media_stream_failed, and others listed on the call.
Media stream protocol
With a stream handler, Blooio opens a WebSocket to your url once the call is answered, sending Authorization: Bearer <auth_token> when you set one. Every frame is JSON text with an event. Audio is base64 PCMU (G.711 μ-law) at 8 kHz, mono, in both directions.
Blooio sends:
event |
Frame |
|---|---|
connected |
{ "event": "connected", "version": "1.0.0" } |
start |
{ "event": "start", "call_id", "start": { "call_id", "from", "to", "media_format": { "encoding": "PCMU", "sample_rate": 8000, "channels": 1 } } } |
media |
{ "event": "media", "call_id", "media": { "track", "chunk", "timestamp", "payload" } }: the other party's audio. |
dtmf |
{ "event": "dtmf", "call_id", "dtmf": { "digit" } }: a key the other party pressed. |
mark |
{ "event": "mark", "call_id", "mark": { "name" } }: a mark you sent, once the audio before it has played. |
stop |
{ "event": "stop", "call_id" }: the call is over. |
You send:
event |
Frame |
|---|---|
media |
{ "event": "media", "media": { "payload" } }: audio to play to the other party. |
clear |
{ "event": "clear" }: drop audio you've sent that hasn't played yet, for example when the caller talks over it. |
mark |
{ "event": "mark", "mark": { "name" } }: ask to be told when the audio sent so far has played. |
If your socket closes, or sends a frame the protocol doesn't define, Blooio ends the call with handler_disconnected. When Blooio's side of the stream drops mid-call, for example during a deploy, Blooio opens a new socket to your url for the same call, starting again with connected and start, so keep a call's state by call_id rather than by socket.
Softphone
The dashboard's Calls page is a softphone. With the softphone inbound handler, an incoming call rings every teammate signed in to the dashboard, and they can dial out from it too. Only the dashboard can hand a call to softphone; the API can't, because the handler is the teammate making the request.
Blooio agents
With the agent handler, the Blooio agent that owns the number talks to the caller, using the same tools and memory it has in messages. An agent can hand a call to a teammate's softphone; the caller waits up to ring_timeout_seconds for someone to pick up, and the call ends handoff_unanswered if nobody does. A call whose handler can't be connected, when it's answered or transferred, waits for a teammate the same way, and so does one transferred to a phone number that doesn't answer. An agent's call that goes two minutes with nobody speaking is hung up, with error.code call_silent. What was said is on GET /v4/calls/{call_id}/transcript, in order, each segment with its speaker (contact or agent).
Webhooks
| Event | When |
|---|---|
call.ringing |
An inbound call is ringing. With the api handler, answer it before ring_timeout_seconds passes. |
call.answered |
A call was answered. Its handler connects right after. |
call.ended |
A call ended, with its final status, hangup_cause, and ended_at. |
Every payload is the call, as GET /v4/calls/{call_id} returns it.
Scopes
API keys have full access. OAuth apps need calls:read to read calls and transcripts, and calls:write to place, answer, and control them.