API Reference
BubblyPhone Agents gives your AI agents phone numbers for inbound and outbound calls. You bring your own AI logic — we handle telephony, billing, and call management.
Quick Start — Your first AI call in 5 minutes
Follow these steps to make your first AI phone call. You'll buy a number, configure an AI agent, and call it from your phone to hear the agent answer.
Top up your balance
Go to Billingand add at least $10 in credits. You'll need enough to cover the phone number ($3/mo) plus a few minutes of calling.
Create an API key
Visit API Keys and create a new key. Keys start with bp_live_sk_. Save it somewhere safe — you'll use it for all API requests.
Buy a phone number
Go to Buy Number, pick a country, and purchase one. Setup is instant.
Configure the AI agent
Open your number's detail page and configure the AI. Set mode = streaming, pick a model (Gemini 3.1 Flash Live is fastest and cheapest), and write a system prompt:
curl -X PATCH https://agents.bubblyphone.com/api/v1/phone-numbers/1 \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "streaming",
"model": "gemini-3.1-flash-live-preview",
"system_prompt": "You are a friendly AI assistant. Greet callers and ask how you can help. Keep responses brief and conversational.",
"voice": "Kore",
"language": "en-US"
}'Or use the dashboard: go to the number's detail page and fill out the configuration form.
Call your number
Dial the number from your phone. The AI will answer and greet you. Have a conversation — the AI will respond in real-time. When you hang up, check the Calls page for the transcript and cost breakdown.
Base URL
All API requests should be made to:
https://agent.bubblyphone.com/api/v1Authentication
All API requests require a Bearer token. Two methods are supported:
bp_live_sk_ — long-lived, for programmatic access.curl https://agent.bubblyphone.com/api/v1/auth/me \
-H "Authorization: Bearer bp_live_sk_your_api_key_here"/api/v1/auth/registerCreate a new developer account.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Your name |
| string | Required | Email address | |
| password | string | Required | Min 8 characters |
| password_confirmation | string | Required | Must match password |
| company_name | string | Optional | Your company |
curl -X POST https://agent.bubblyphone.com/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"Jane","email":"jane@example.com","password":"secret123","password_confirmation":"secret123"}'{
"data": { "id": 1, "name": "Jane", "email": "jane@example.com" },
"token": "1|abc123..."
}/api/v1/auth/loginLog in and receive a session token.
| Parameter | Type | Required | Description |
|---|---|---|---|
| string | Required | Email address | |
| password | string | Required | Account password |
/api/v1/auth/meGet the authenticated developer's profile.
/api/v1/auth/logoutRevoke the current session token.
AI Models
List available AI models and their per-minute pricing. Used when configuring a phone number in streaming mode.
/api/v1/modelsList all available AI models with pricing.
curl https://agent.bubblyphone.com/api/v1/models \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY"{
"data": [
{ "slug": "gemini-3.1-flash-live", "provider": "google", "display_name": "Gemini 3.1 Flash Live", "price_per_minute": "0.040000", "status": "available" },
{ "slug": "gpt-realtime-1.5", "provider": "openai", "display_name": "GPT Realtime 1.5", "price_per_minute": "0.120000", "status": "available" },
{ "slug": "gpt-realtime-mini", "provider": "openai", "display_name": "GPT Realtime Mini", "price_per_minute": "0.040000", "status": "available" }
]
}Model Keys (BYOK)
Bring your own API key for Google or OpenAI. BYOK calls use your key directly — model cost is $0 on your BubblyPhone bill.
/api/v1/model-keysStore a BYOK API key for Google or OpenAI.
| Parameter | Type | Required | Description |
|---|---|---|---|
| provider | string | Required | "google" or "openai" |
| api_key | string | Required | Your provider API key (min 10 chars) |
| label | string | Optional | Friendly label for this key |
curl -X POST https://agent.bubblyphone.com/api/v1/model-keys \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"openai","api_key":"sk-proj-abc123...","label":"Production OpenAI"}'{
"data": {
"id": 3, "provider": "openai", "label": "Production OpenAI",
"last_four": "...r4nk", "created_at": "2026-03-28T09:00:00Z"
}
}/api/v1/model-keysList all stored model keys. Full key is never returned.
/api/v1/model-keys/{id}Remove a stored model key permanently.
Rate Limits
100 requests per minute per API key. Headers included in every response:
| Header | Description |
|---|---|
| X-RateLimit-Limit | Max requests per minute |
| X-RateLimit-Remaining | Remaining in current window |
Phone Numbers
Search, purchase, manage, and release phone numbers for your AI agents.
/api/v1/phone-numbers/availableSearch available phone numbers to purchase.
| Parameter | Type | Required | Description |
|---|---|---|---|
| country | string | Optional | 2-char country code (default: US) |
| area_code | string | Optional | Area code filter (US/Canada) |
| locality | string | Optional | City or locality (e.g. "London", "San Francisco") |
| number_type | string | Optional | local, mobile, toll_free, national |
| contains | string | Optional | Digits the number must contain |
| limit | integer | Optional | Max results (1-50, default: 50) |
curl "https://agent.bubblyphone.com/api/v1/phone-numbers/available?country=US&area_code=415" \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY"{
"data": [
{
"phone_number": "+14155550100",
"phone_number_type": "local",
"features": [
{ "name": "voice" }, { "name": "sms" }, { "name": "mms" }, { "name": "fax" }
],
"cost_information": { "upfront_cost": "1.00000", "monthly_cost": "1.00000", "currency": "USD" },
"region_information": [
{ "region_type": "rate_center", "region_name": "SAN FRANCISCO" },
{ "region_type": "state", "region_name": "CA" },
{ "region_type": "country_code", "region_name": "US" }
]
}
],
"pricing": { "setup_cost": "3.00", "monthly_cost": "3.00", "currency": "USD" }
}/api/v1/phone-numbersPurchase a phone number. Setup fee + first month deducted from balance.
| Parameter | Type | Required | Description |
|---|---|---|---|
| phone_number | string | Required | E.164 format (e.g. +14155551234) |
| webhook_url | string | Optional | HTTPS URL for call events |
| label | string | Optional | Friendly name |
| country_code | string | Optional | 2-char country code (default: US) |
| number_type | string | Optional | local, mobile, toll_free, national (default: local) |
webhook_secret — shown only once. Store it securely.curl -X POST https://agent.bubblyphone.com/api/v1/phone-numbers \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"phone_number":"+14155551234","webhook_url":"https://your-app.com/webhook","label":"Support"}'{
"data": {
"id": 1, "phone_number": "+14155551234", "label": "Support",
"webhook_url": "https://your-app.com/webhook",
"webhook_secret": "whsec_abc123...", "status": "active", "monthly_cost": "3.00"
}
}/api/v1/phone-numbersList all your phone numbers.
/api/v1/phone-numbers/{id}Get details for a specific phone number.
/api/v1/phone-numbers/{id}Update webhook URL, label, or configure streaming mode.
| Parameter | Type | Required | Description |
|---|---|---|---|
| webhook_url | string | Optional | New HTTPS webhook URL (webhook mode) |
| label | string | Optional | New label |
| mode | string | Optional | "webhook" (default) or "streaming" |
| model | string | Optional | AI model slug, e.g. "gemini-3.1-flash-live" |
| model_key_id | integer | Optional | BYOK model key ID — omit to use platform-managed key |
| system_prompt | string | Optional | System prompt for the AI agent (required when mode=streaming) |
| voice | string | Optional | Voice ID (e.g. "Kore") |
| tools | array | Optional | Array of function definitions for tool use |
| tool_webhook_url | string | Optional | URL to receive tool/function call events (required if tools are set) |
| max_call_duration | integer | Optional | Max call duration in seconds (60–3600, default 3600) |
| silence_timeout | integer | Optional | Auto-hangup after N seconds of silence (10–120, default 30) |
Streaming Mode Example
In streaming mode BubblyPhone connects the call directly to the AI model via a real-time audio stream. No webhook round-trips per utterance.
curl -X PATCH https://agent.bubblyphone.com/api/v1/phone-numbers/1 \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "streaming",
"model": "gemini-3.1-flash-live",
"system_prompt": "You are a helpful receptionist for Acme Corp...",
"voice": "Kore",
"tools": [
{
"name": "check_availability",
"description": "Check calendar availability",
"parameters": { "date": "string" }
}
],
"tool_webhook_url": "https://yourapp.com/tools",
"silence_timeout": 20
}'call.tool_call event to your tool_webhook_url. Respond with the result JSON and the AI continues the conversation./api/v1/phone-numbers/{id}Release a phone number permanently.
Number Pricing
/api/v1/number-pricingGet cached pricing for all countries. Filter by country or number type.
| Parameter | Type | Required | Description |
|---|---|---|---|
| country | string | Optional | 2-char country code (e.g. US, GB) |
| number_type | string | Optional | local, mobile, toll_free, national |
curl "https://agent.bubblyphone.com/api/v1/number-pricing?country=GB" \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY"{
"data": [
{ "country_code": "US", "number_type": "local", "setup_cost": "3.00", "monthly_cost": "3.00", "currency": "USD" },
{ "country_code": "GB", "number_type": "local", "setup_cost": "3.00", "monthly_cost": "3.00", "currency": "USD" },
{ "country_code": "GB", "number_type": "mobile", "setup_cost": "6.00", "monthly_cost": "6.00", "currency": "USD" }
]
}Country Verification
Before purchasing phone numbers in a country, you must verify your identity. US numbers are auto-verified. International numbers require document submission.
| Status | Meaning |
|---|---|
| not_required | No documents needed (e.g. US numbers) |
| pending | Documents submitted, awaiting review |
| approved | Verified — you can purchase numbers |
| rejected | Verification failed — resubmit documents |
/api/v1/countriesList countries and your verification status for each.
curl https://agent.bubblyphone.com/api/v1/countries \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY"{
"data": [
{ "country_code": "US", "country_name": "United States", "number_type": "local", "status": "not_required" },
{ "country_code": "GB", "country_name": "United Kingdom", "number_type": "local", "status": "approved" },
{ "country_code": "DE", "country_name": "Germany", "number_type": "local", "status": "pending" }
]
}/api/v1/countries/{code}/requirementsGet the required documents for purchasing a number type in a country.
| Parameter | Type | Required | Description |
|---|---|---|---|
| number_type | string | Optional | local, mobile, toll_free (default: local) |
curl "https://agent.bubblyphone.com/api/v1/countries/GB/requirements?number_type=local" \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY"{
"data": [
{
"id": "2708e569-...",
"name": "Contact Info",
"type": "textual",
"description": "Provide the name, business name, and contact phone numbers",
"example": "Name, business name, and contact phone numbers",
"time_limit": null
},
{
"id": "6d3e2643-...",
"name": "Proof of Address (Local)",
"type": "document",
"description": "Proof of residence such as utility bill",
"example": "Utility bill dated within 3 months",
"time_limit": "Less than 3 months old"
}
]
}/api/v1/countries/{code}/verifySubmit documents for country verification. Uses multipart/form-data.
| Parameter | Type | Required | Description |
|---|---|---|---|
| number_type | string | Required | local, mobile, or toll_free |
| documents[] | array | Required | Array of document objects: each with a type (requirement ID) and file upload |
Content-Type: multipart/form-data. Each document needs a type (requirement ID from the requirements endpoint) and a file.curl -X POST https://agent.bubblyphone.com/api/v1/countries/GB/verify \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-F "number_type=local" \
-F "documents[0][type]=6d3e2643-..." \
-F "documents[0][file]=@/path/to/utility-bill.pdf"{
"data": {
"id": "ver_abc123",
"country_code": "GB",
"number_type": "local",
"status": "pending",
"created_at": "2026-03-28T10:00:00Z"
}
}/api/v1/countries/{code}/verificationCheck your verification status for a country and number type.
| Parameter | Type | Required | Description |
|---|---|---|---|
| number_type | string | Optional | local, mobile, toll_free (default: local) |
{
"data": {
"country_code": "GB",
"number_type": "local",
"status": "pending",
"submitted_at": "2026-03-28T10:00:00Z",
"reviewed_at": null
}
}Agents
Reusable AI agent configurations. Define an agent once (model, prompt, voice, tools, transfer settings) and assign it to multiple phone numbers or override per-call. Similar to RetellAI's agent concept.
/api/v1/agentsCreate a reusable AI agent.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Human-readable name for this agent |
| description | string | Optional | Optional description of the agent's purpose |
| model_slug | string | Optional | AI model slug (e.g. gemini-3.1-flash-live-preview) |
| system_prompt | string | Optional | Instructions for the AI agent (up to 10,000 chars) |
| voice | string | Optional | TTS voice name (e.g. Kore, Aoede, Charon) |
| language | string | Optional | BCP-47 language code (e.g. en-US, es-US) |
| transfer_number | string | Optional | E.164 number for call transfers |
| recording_enabled | boolean | Optional | Record call audio (default: false) |
curl -X POST https://agents.bubblyphone.com/api/v1/agents \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Support Agent v1",
"description": "Handles customer support calls",
"model_slug": "gemini-3.1-flash-live-preview",
"system_prompt": "You are a helpful customer support agent...",
"voice": "Kore",
"language": "en-US",
"transfer_number": "+14155550100",
"recording_enabled": true
}'{
"data": {
"id": 1,
"developer_id": 1,
"name": "Support Agent v1",
"model_slug": "gemini-3.1-flash-live-preview",
"voice": "Kore",
"language": "en-US",
"phone_numbers_count": 0,
"created_at": "2026-04-05T12:00:00Z"
}
}/api/v1/agentsList all agents for your account.
curl https://agents.bubblyphone.com/api/v1/agents \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY"/api/v1/agents/{id}Get a single agent by ID.
curl https://agents.bubblyphone.com/api/v1/agents/1 \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY"/api/v1/agents/{id}Update an agent.
Partial updates. Any field not provided stays unchanged. Changes apply to all phone numbers linked to this agent immediately.
curl -X PATCH https://agents.bubblyphone.com/api/v1/agents/1 \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"system_prompt": "You are a friendly support agent..."}'/api/v1/agents/{id}Delete an agent.
Fails with 422 if any phone numbers still reference this agent. Unlink them first.
curl -X DELETE https://agents.bubblyphone.com/api/v1/agents/1 \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY"/api/v1/phone-numbers/{id}Link an agent to a phone number.
curl -X PATCH https://agents.bubblyphone.com/api/v1/phone-numbers/1 \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-d '{"agent_id": 1}'/api/v1/callsUse an agent in an outbound call.
The agent_id in POST /calls overrides the phone number's default agent. You can still pass other fields (system_prompt, voice, etc.) to override specific fields of the agent for this call only.
curl -X POST https://agents.bubblyphone.com/api/v1/calls \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-d '{
"from": "+14155940554",
"to": "+12025551234",
"mode": "streaming",
"agent_id": 1,
"dynamic_variables": {
"customer_name": "John"
}
}'agent_idoverride → 2) Phone number's linked agent → 3) Phone number's inline fields (legacy fallback).Post-Call Analysis
After every call, automatically extract structured data from the transcript using AI. Define an analysis schema on your agent and we'll run GPT-5.4-nano against the transcript, returning typed data (booleans, numbers, selectors, text) for each field.
Defining the Schema
Add an analysis_schema field to your agent when creating or updating it. Each field has a type and description.
curl -X POST https://agents.bubblyphone.com/api/v1/agents \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Sales Agent",
"model_slug": "gemini-3.1-flash-live-preview",
"system_prompt": "You are a friendly sales rep...",
"analysis_schema": {
"qualified_lead": {
"type": "boolean",
"description": "Did the caller express clear interest in purchasing?"
},
"budget": {
"type": "number",
"description": "What budget did the caller mention? Null if not mentioned."
},
"disposition": {
"type": "selector",
"description": "How the call ended",
"options": ["interested", "not_interested", "callback_requested", "wrong_number"]
},
"summary": {
"type": "text",
"description": "Brief 1-2 sentence summary of the call"
}
}
}'Field Types
| Parameter | Type | Required | Description |
|---|---|---|---|
| text | string or null | Optional | Free text. Null if not found in transcript. |
| boolean | true or false | Optional | Always returns true or false. |
| number | number or null | Optional | Integer or float. Null if not found. |
| selector | string | Optional | Must be one of the options array values. |
Results
After the call ends, analysis runs automatically if the agent has a schema. Results are:
- Stored on the call record:
GET /calls/{id}returnsanalysis_result - Delivered via webhook:
call.analysis.readyevent fired to your phone number'swebhook_url
{
"event": "call.analysis.ready",
"call_id": "68",
"analysis": {
"qualified_lead": true,
"budget": 5000,
"disposition": "interested",
"summary": "Customer wants to discuss enterprise pricing next week."
},
"timestamp": "2026-04-05T18:00:00Z"
}Billing
Each analysis is charged at $0.01 per call, regardless of transcript length. No charge if the agent has no schema defined.
Calls
Initiate outbound calls and manage active calls programmatically.
/api/v1/callsInitiate an outbound call.
| Parameter | Type | Required | Description |
|---|---|---|---|
| from | string | Required | Your phone number (E.164) |
| to | string | Required | Destination number (E.164) |
curl -X POST https://agent.bubblyphone.com/api/v1/calls \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"from":"+14155551234","to":"+15551234567"}'{
"data": {
"id": 42, "from": "+14155551234", "to": "+15551234567",
"direction": "outbound", "status": "initiated"
}
}/api/v1/callsList calls with optional filters.
| Parameter | Type | Required | Description |
|---|---|---|---|
| direction | string | Optional | inbound or outbound |
| status | string | Optional | initiated, ringing, answered, completed, failed |
| phone_number_id | integer | Optional | Filter by phone number |
| date_from | datetime | Optional | Start date (ISO 8601) |
| date_to | datetime | Optional | End date (ISO 8601) |
/api/v1/calls/{id}Get full details for a call.
{
"data": {
"id": 42, "from": "+14155551234", "to": "+15551234567",
"direction": "outbound", "status": "completed",
"duration_seconds": 120, "cost": "0.10",
"answered_at": "2026-03-27T12:00:05Z", "ended_at": "2026-03-27T12:02:05Z"
}
}/api/v1/calls/{id}/speakSend text-to-speech to an active call and gather the caller's response.
| Parameter | Type | Required | Description |
|---|---|---|---|
| text | string | Required | Text to speak (max 5000 chars) |
/api/v1/calls/{id}/hangupEnd an active call.
/api/v1/calls/{id}/transferTransfer an active call to another number.
| Parameter | Type | Required | Description |
|---|---|---|---|
| to | string | Required | Transfer destination (E.164) |
/api/v1/calls/{id}/transcriptGet post-call transcript (available after call ends).
/api/v1/calls/{id}/recordingGet the recording URL.
/api/v1/calls/{id}/eventsGet the full event timeline for a call. Useful for debugging exactly what happened.
{
"data": [
{
"event_type": "call.initiated", "source": "system",
"event_timestamp": "2026-03-28T10:00:00.000Z", "metadata": {}
},
{
"event_type": "call.answered", "source": "telephony",
"event_timestamp": "2026-03-28T10:00:04.120Z", "metadata": {}
},
{
"event_type": "stream.connected", "source": "streaming",
"event_timestamp": "2026-03-28T10:00:04.350Z", "metadata": { "model": "gemini-3.1-flash-live" }
}
]
}/api/v1/calls/{id}/webhook-logsGet webhook delivery logs for a call — every attempt, response, and latency.
{
"data": [
{
"event_type": "call.incoming",
"url": "https://yourapp.com/webhook",
"request_payload": { "event": "call.incoming", "call_id": "42" },
"response_status": 200,
"response_body": "{"action":"answer","text":"Hello!"}",
"latency_ms": 183,
"attempt": 1,
"success": true,
"error_message": null
}
]
}Call Flow
| Status | Description | Billing |
|---|---|---|
| initiated | Call created | — |
| ringing | Remote party ringing | — |
| answered | Connected | Billing starts |
| completed | Ended normally | Final charge |
| failed | Could not connect | No charge |
Call Pricing
| Service | Rate |
|---|---|
| Outbound (US) | $0.05/min |
| Inbound (US) | $0.04/min |
| Transcription | $0.02/call |
Sandbox
Start a browser-based test call and talk to your AI agent over WebSocket — no real phone required. Telephony is $0; AI model usage is billed at the normal rate.
/api/v1/sandbox/callsStart a sandbox test call and get a WebSocket URL to connect your browser audio.
| Parameter | Type | Required | Description |
|---|---|---|---|
| phone_number_id | integer | Required | Phone number to test (must be in streaming mode) |
| overrides | object | Optional | Optional overrides: system_prompt, model, voice |
curl -X POST https://agent.bubblyphone.com/api/v1/sandbox/calls \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number_id": 1,
"overrides": { "system_prompt": "You are a test assistant." }
}'{
"data": {
"call_id": "sandbox_abc123",
"ws_url": "wss://agent.bubblyphone.com/sandbox/ws/sandbox_abc123",
"model": "gemini-3.1-flash-live",
"expires_in": 30
}
}WebSocket Protocol
Connect to ws_url within 30 seconds. Send and receive JSON frames:
Send (client → server)
{ "type": "audio", "data": "<base64-encoded PCM16 16kHz mono>" }Receive (server → client)
{ "type": "audio", "data": "<base64-encoded audio>" }{ "type": "transcript", "role": "user", "text": "What are your hours?" }
{ "type": "transcript", "role": "model", "text": "We are open Monday to Friday, 9am to 5pm." }{ "type": "tool_call", "tool": "check_availability", "arguments": { "date": "2026-04-01" } }{ "type": "ended", "reason": "completed" }audio frames. Play received audio frames through an AudioContext./api/v1/sandbox/demoStart a free demo call — no balance required, no phone number needed.
curl -X POST https://agent.bubblyphone.com/api/v1/sandbox/demo \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY"{
"data": {
"call_id": 5001,
"ws_url": "wss://api.bubblyphone.com/bridge/sandbox?token=...",
"model": "gemini-3.1-flash-live",
"demos_remaining": 2
}
}Webhooks
We send events to your webhook_url for each phone number. Respond within 10 seconds.
Events
Inbound call arrived. Respond with an action.
{
"event": "call.incoming", "call_id": "42", "phone_number_id": "1",
"from": "+15551234567", "to": "+14155551234",
"direction": "inbound", "timestamp": "2026-03-27T12:00:00Z"
}Call answered. Billing starts.
{"event": "call.answered", "call_id": "42", "timestamp": "2026-03-27T12:00:05Z"}Caller spoke. Contains transcribed text.
{
"event": "call.speech", "call_id": "42",
"text": "I'd like to make a reservation for two",
"timestamp": "2026-03-27T12:00:15Z"
}Call ended. Includes duration and cost.
{
"event": "call.hangup", "call_id": "42",
"duration_seconds": 120, "cost": 0.10,
"hangup_cause": "normal_clearing", "timestamp": "2026-03-27T12:02:05Z"
}Transcript available. Fetch via the transcript endpoint.
{"event": "call.transcript.ready", "call_id": "42", "timestamp": "2026-03-27T12:03:00Z"}Streaming Mode Events
These additional events are sent for phone numbers in streaming mode.
Call answered and AI stream connected. Billing starts.
{
"event": "call.started", "call_id": "42", "model": "gemini-3.1-flash-live",
"timestamp": "2026-03-28T10:00:04Z"
}AI wants to call a function. Sent only to tool_webhook_url. Respond with the result.
{
"event": "call.tool_call", "call_id": "42",
"tool": "check_availability", "arguments": { "date": "2026-04-01" },
"timestamp": "2026-03-28T10:00:20Z"
}{ "result": { "available": true, "slots": ["10:00", "14:00"] } }Streaming call finished. Includes itemized cost breakdown.
{
"event": "call.ended", "call_id": "42",
"duration_seconds": 95,
"telephony_cost": "0.08", "model_cost": "0.063", "total_cost": "0.143",
"hangup_cause": "normal_clearing", "timestamp": "2026-03-28T10:01:39Z"
}Actions
Respond to call.incoming and call.speech with JSON:
answer
Answer and speak
{"action": "answer", "text": "Hello! How can I help?"}speak
Send TTS
{"action": "speak", "text": "Your reservation is confirmed."}transfer
Transfer call
{"action": "transfer", "to": "+15559876543"}hangup
End call
{"action": "hangup"}Retries
Non-2xx or timeout? We retry up to 3 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 5 seconds |
| 2nd retry | 30 seconds |
| 3rd retry | 120 seconds |
Outbound AI Calls
Use POST /calls with mode: "streaming" to initiate an outbound call handled by an AI agent. The AI will greet the person when they pick up and carry the conversation autonomously.
Per-call overrides let you customise the agent for each call — any field you omit falls back to the phone number's configuration.
/api/v1/callsInitiate an outbound call handled by a streaming AI agent.
| Parameter | Type | Required | Description |
|---|---|---|---|
| from | string | Required | Your purchased phone number (E.164 format) |
| to | string | Required | Destination number (E.164 format) |
| mode | string | Optional | "webhook" (default) or "streaming" |
| model | string | Optional | AI model slug (e.g. "gemini-3.1-flash-live-preview"). Falls back to phone number config. |
| system_prompt | string | Optional | Instructions for the AI agent. Falls back to phone number config. |
| voice | string | Optional | TTS voice name (e.g. "Kore"). Falls back to phone number config. |
| tools | array | Optional | Tool definitions for function calling. Falls back to phone number config. |
| tool_webhook_url | string | Optional | HTTPS URL for tool call webhooks. Falls back to phone number config. |
| model_key_id | integer | Optional | BYOK key ID. Falls back to phone number config. |
| transfer_number | string | Optional | Number to transfer to if AI decides to hand off. |
{
"from": "+14155940554",
"to": "+12025551234",
"mode": "streaming",
"model": "gemini-3.1-flash-live-preview",
"system_prompt": "You are calling to confirm a dental appointment for John Smith on April 15 at 2pm. Be polite and brief. If they want to reschedule, use the reschedule tool.",
"voice": "Kore"
}{
"data": {
"id": 46,
"direction": "outbound",
"from_number": "+14155940554",
"to_number": "+12025551234",
"status": "initiated",
"mode": "streaming",
"model_slug": "gemini-3.1-flash-live-preview",
"started_at": "2026-04-02T14:00:00Z"
}
}Dynamic Variables
Personalize outbound calls by injecting per-call data into system prompts and greeting messages using {{variable_name}} templating.
When making outbound calls at scale (appointment reminders, sales outreach, surveys), you typically need to reference the customer's name, order details, or appointment info in the AI's prompt. Instead of rewriting the system prompt for every call, use dynamic variables.
Syntax
Wrap variable names in double curly braces: {{variable_name}}. Variable names must start with a letter or underscore and contain only letters, digits, and underscores.
Where variables work
system_prompt— the AI agent's instructionsgreeting_message— what triggers the AI's first response
Example request
curl -X POST https://agents.bubblyphone.com/api/v1/calls \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+14155940554",
"to": "+12025551234",
"mode": "streaming",
"system_prompt": "You are calling {{customer_name}} to confirm their {{service}} appointment on {{date}} at {{time}}. Be polite and brief.",
"greeting_message": "Start by greeting {{customer_name}} by name.",
"dynamic_variables": {
"customer_name": "John Smith",
"service": "dental cleaning",
"date": "April 15",
"time": "2pm"
}
}'Parameter reference
| Parameter | Type | Required | Description |
|---|---|---|---|
| dynamic_variables | object | Optional | Key-value pairs. Keys match {{variable_name}} tokens, values are strings (max 2000 chars) |
| greeting_message | string | Optional | Custom greeting trigger for the AI. Defaults to a generic greeting. Supports variables. |
Error handling
If the system prompt or greeting message references a variable that isn't provided in dynamic_variables, the API returns 422 at call initiation — before any charges are incurred.
{
"message": "Missing required dynamic variables.",
"errors": {
"dynamic_variables": ["Missing variables: customer_name, service"]
}
}Voicemail Detection
When an outbound call reaches a voicemail/answering machine, either hang up to save costs or let the AI leave a contextual message.
BubblyPhone automatically detects when an outbound streaming call is answered by voicemail or an answering machine. You control what happens next via the voicemail_actionfield — either hang up immediately (saves cost) or let the AI leave a contextual voicemail based on the call's purpose.
Configure the default behavior on the phone number, then override it per-call as needed.
Configure on phone number
Set voicemail_action via PATCH /api/v1/phone-numbers/{id} to apply a default for all outbound calls on that number.
/api/v1/phone-numbers/{id}Update voicemail action for a phone number
| Parameter | Type | Required | Description |
|---|---|---|---|
| voicemail_action | string | Optional | "hangup" (default) or "ai_message". Controls what happens when voicemail is detected on outbound calls. |
curl -X PATCH https://agents.bubblyphone.com/api/v1/phone-numbers/1 \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"voicemail_action": "ai_message"}'Per-call override
You can override the phone number default on a per-call basis by passing voicemail_action in the POST /calls body.
curl -X POST https://agents.bubblyphone.com/api/v1/calls \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+14155940554",
"to": "+12025551234",
"mode": "streaming",
"voicemail_action": "hangup",
"system_prompt": "..."
}'| Parameter | Type | Required | Description |
|---|---|---|---|
| voicemail_action | string | Optional | "hangup" (default) or "ai_message". Only applies to outbound streaming calls. |
voicemail_detected field is set to true. You can filter calls that reached voicemail via GET /calls?voicemail_detected=true (not yet implemented).Billing
- Hangup mode: minimum 1 minute telephony charge for the detection window, no AI model charge.
- AI message mode: normal streaming billing for the duration of the AI's message.
Call Transfer
Every streaming call has a built-in transfer_call tool that the AI can invoke when it decides a human agent should take over. Two setup options are available: simple (fixed number) or dynamic (webhook routing).
Simple Setup
Set transfer_number on a phone number to give the AI a fixed destination to transfer to.
curl -X PATCH https://agents.bubblyphone.com/api/v1/phone-numbers/1 \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"transfer_number": "+12025559999"}'Dynamic Routing
When tool_webhook_url is set, the platform sends the transfer request to your webhook and uses the number you return. This lets you route calls to different teams based on context.
{
"event": "call.tool_call",
"call_id": "45",
"tool": "transfer_call",
"arguments": {"reason": "customer wants billing help"}
}{
"result": {
"transfer_to": "+12025559999"
}
}Transfer Billing
Transferred calls are billed in two phases:
| Phase | Telephony | AI Model |
|---|---|---|
| Phase 1 — AI active | Normal rate | Per model price/min |
| Phase 2 — Transfer leg | Outbound rate to destination | $0.00 |
transfer_call tool, set auto_transfer_tool: false on the phone number configuration.Mid-call Context Injection
Push information to the AI agent during an active streaming call. The AI incorporates the context into its next response.
/api/v1/calls/{id}/contextSend a context message to the AI agent on an active streaming call.
| Parameter | Type | Required | Description |
|---|---|---|---|
| message | string | Required | Context message for the AI agent (max 2000 chars) |
{
"message": "The customer's account has been credited $50. Their new balance is $120."
}{
"status": "delivered"
}Real-time Transcript Webhooks
Receive transcript segments via webhook as the conversation happens. Enable per phone number or per outbound call.
Enabling
curl -X PATCH https://agents.bubblyphone.com/api/v1/phone-numbers/1 \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"realtime_transcript": true}'Webhook Payload
{
"event": "call.transcript.segment",
"call_id": "45",
"role": "user",
"text": "I'd like to check my account balance",
"timestamp": "2026-04-02T14:01:23Z"
}Call Recording
Record streaming call audio to the cloud. Enable per phone number or per outbound call.
Enable Recording
curl -X PATCH https://agents.bubblyphone.com/api/v1/phone-numbers/1 \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"recording_enabled": true}'Access a Recording
/api/v1/calls/{id}/recordingGet a time-limited signed URL for the call recording.
{
"data": {
"recording_url": "https://..."
}
}Whisper Transcription
Get high-accuracy transcripts powered by Groq Whisper. Requires recording to be enabled.
Enable Transcription
curl -X PATCH https://agents.bubblyphone.com/api/v1/phone-numbers/1 \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"transcription_enabled": true, "recording_enabled": true}'Retrieve Transcript
The Whisper transcript is available via GET /calls/{id}/transcript alongside the real-time conversation transcript.
| Parameter | Type | Required | Description |
|---|---|---|---|
| whisper_transcript | string | Optional | Full text of the Whisper transcription |
| whisper_segments | array | Optional | Array of segments with start, end, and text fields |
Pricing
Whisper transcription costs $0.03 per minute of audio transcribed.
recording_enabled to be true. The Whisper transcript is in addition to the real-time conversation transcript.Billing
Pay-as-you-go with prepaid credit. Top up via Stripe Checkout.
Calls billed per minute (rounded up) when the call ends
Phone numbers billed monthly on purchase anniversary
Balance hits $0 during a call → call auto-ended
Minimum $0.50 balance required to initiate a call
Itemized Billing
Streaming calls produce two separate line items on your invoice:
| Scenario | Telephony | AI Model |
|---|---|---|
| Webhook mode | Normal rate | — |
| Streaming (platform key) | Normal rate | Per model price/min |
| Streaming (BYOK) | Normal rate | $0.00 |
| Sandbox call | $0.00 | Per model price/min |
/api/v1/billing/balanceGet current credit balance.
{"data": {"balance": "24.50", "currency": "USD"}}/api/v1/billing/transactionsList all transactions (cursor-paginated).
{
"data": [
{"id": 1, "type": "credit", "amount": "10.00", "description": "Balance top-up"},
{"id": 2, "type": "debit", "amount": "0.10", "description": "Call to +15551234567 (2 min)"}
]
}/api/v1/billing/top-upCreate a Stripe Checkout session to add credit.
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | number | Required | One of: 5, 10, 20, 50, 100 (USD) |
{"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_..."}/api/v1/billing/auto-topupGet your current auto top-up settings.
{
"data": {
"enabled": false,
"threshold": null,
"amount": null,
"has_payment_method": true
}
}/api/v1/billing/auto-topupEnable or update auto top-up. Automatically adds credit when balance falls below a threshold.
| Parameter | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | Required | Enable or disable auto top-up |
| threshold | number | Optional | Top-up when balance drops below this (min 1, max 100) |
| amount | number | Optional | Amount to add — one of: 10, 25, 50, 100 (USD) |
curl -X PUT https://agent.bubblyphone.com/api/v1/billing/auto-topup \
-H "Authorization: Bearer bp_live_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"threshold":5,"amount":25}'{
"data": {
"enabled": true,
"threshold": 5,
"amount": 25,
"has_payment_method": true
}
}/api/v1/billing/usageUsage summary for a billing period, broken down by category.
| Parameter | Type | Required | Description |
|---|---|---|---|
| period | string | Optional | YYYY-MM (default: current month) |
{
"data": {
"period": "2026-03", "total_calls": 142, "total_minutes": 284,
"total_cost": "25.87", "phone_numbers": 2, "phone_number_cost": "6.00",
"categories": {
"telephony": { "cost": "14.20", "minutes": 284 },
"ai_model": { "cost": "5.67", "minutes": 142 }
}
}
}Examples
Complete, working code samples to get you started.
AI Receptionist
A webhook handler that answers calls and routes to your AI.
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', async (req, res) => {
const { event, text } = req.body;
switch (event) {
case 'call.incoming':
return res.json({
action: 'answer',
text: 'Hi! Thanks for calling Acme Corp. How can I help you?'
});
case 'call.speech':
const aiResponse = await getAIResponse(text);
return res.json({ action: 'speak', text: aiResponse });
case 'call.hangup':
console.log('Call ended:', req.body.duration_seconds, 'seconds');
return res.json({ status: 'ok' });
default:
return res.json({ status: 'ok' });
}
});
app.listen(3001);Outbound Caller
Initiate a call and send TTS programmatically.
import requests
API_KEY = "bp_live_sk_your_key_here"
BASE = "https://agent.bubblyphone.com/api/v1"
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
# Initiate a call
call = requests.post(f"{BASE}/calls", json={
"from": "+14155551234", "to": "+15551234567"
}, headers=headers).json()
print(f"Call initiated: {call['data']['id']}")
# Send TTS
requests.post(
f"{BASE}/calls/{call['data']['id']}/speak",
json={"text": "Hello! This is an automated call from Acme Corp."},
headers=headers
)Full cURL Workflow
End-to-end from registration to making your first call.
# 1. Register
curl -X POST https://agent.bubblyphone.com/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"Dev","email":"dev@example.com","password":"secret123","password_confirmation":"secret123"}'
# 2. Create API key
curl -X POST https://agent.bubblyphone.com/api/v1/api-keys \
-H "Authorization: Bearer SESSION_TOKEN" \
-d '{"label":"Production"}'
# 3. Search numbers
curl "https://agent.bubblyphone.com/api/v1/phone-numbers/available?country=US&area_code=415" \
-H "Authorization: Bearer bp_live_sk_KEY"
# 4. Buy a number
curl -X POST https://agent.bubblyphone.com/api/v1/phone-numbers \
-H "Authorization: Bearer bp_live_sk_KEY" \
-d '{"phone_number":"+14155551234","webhook_url":"https://your-app.com/webhook"}'
# 5. Check balance
curl https://agent.bubblyphone.com/api/v1/billing/balance \
-H "Authorization: Bearer bp_live_sk_KEY"
# 6. Make a call
curl -X POST https://agent.bubblyphone.com/api/v1/calls \
-H "Authorization: Bearer bp_live_sk_KEY" \
-d '{"from":"+14155551234","to":"+15559876543"}'API Keys
Long-lived access for your applications. The plaintext key is only shown once.
/api/v1/api-keysCreate a new API key.
| Parameter | Type | Required | Description |
|---|---|---|---|
| label | string | Optional | Key label (default: 'Default') |
{
"data": {"id": 1, "label": "Production", "key": "bp_live_sk_abc123...", "created_at": "2026-03-27T12:00:00Z"}
}/api/v1/api-keysList all API keys (plaintext never exposed).
/api/v1/api-keys/{id}Revoke an API key permanently.
Webhook Signature Verification
Every webhook includes X-Signature and X-Timestamp headers. Verify using HMAC-SHA256 with your webhook_secret.
const crypto = require('crypto');
function verifySignature(body, timestamp, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + JSON.stringify(body))
.digest('hex');
return 'sha256=' + expected === signature;
}
app.post('/webhook', (req, res) => {
const sig = req.headers['x-signature'];
const ts = req.headers['x-timestamp'];
if (!verifySignature(req.body, ts, sig, YOUR_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Handle event...
});