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.

Just getting started?
Check out the Examples section for complete code samples in Node.js, Python, PHP, and cURL.
1
Create an account
Register and get an API key
2
Buy a phone number
Set your webhook URL
3
Receive calls
We POST events to your webhook
4
Respond with actions
Speak, transfer, or hang up

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.

1

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.

2

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.

3

Buy a phone number

Go to Buy Number, pick a country, and purchase one. Setup is instant.

4

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:

Shell
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.

5

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.

That's it!
You now have a working AI phone agent. Next, try outbound calls, call transfer to humans, or pushing context to the AI mid-call.

Base URL

All API requests should be made to:

Base URL
https://agent.bubblyphone.com/api/v1

Authentication

All API requests require a Bearer token. Two methods are supported:

Session Token
Returned from login. Short-lived, used for the dashboard.
API Key
Prefixed with bp_live_sk_ — long-lived, for programmatic access.
Authenticated Request
curl https://agent.bubblyphone.com/api/v1/auth/me \
  -H "Authorization: Bearer bp_live_sk_your_api_key_here"
POST/api/v1/auth/register

Create a new developer account.

ParameterTypeRequiredDescription
namestringRequiredYour name
emailstringRequiredEmail address
passwordstringRequiredMin 8 characters
password_confirmationstringRequiredMust match password
company_namestringOptionalYour company
Example
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"}'
Response201 Created
{
  "data": { "id": 1, "name": "Jane", "email": "jane@example.com" },
  "token": "1|abc123..."
}
POST/api/v1/auth/login

Log in and receive a session token.

ParameterTypeRequiredDescription
emailstringRequiredEmail address
passwordstringRequiredAccount password
GET/api/v1/auth/me

Get the authenticated developer's profile.

POST/api/v1/auth/logout

Revoke the current session token.


AI Models

List available AI models and their per-minute pricing. Used when configuring a phone number in streaming mode.

GET/api/v1/models

List all available AI models with pricing.

Example
curl https://agent.bubblyphone.com/api/v1/models \
  -H "Authorization: Bearer bp_live_sk_YOUR_KEY"
Response200 OK
{
  "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.

Keys are masked
We store your key securely and only ever return the last four characters. The full key is never retrievable after creation.
POST/api/v1/model-keys

Store a BYOK API key for Google or OpenAI.

ParameterTypeRequiredDescription
providerstringRequired"google" or "openai"
api_keystringRequiredYour provider API key (min 10 chars)
labelstringOptionalFriendly label for this key
Example
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"}'
Response201 Created
{
  "data": {
    "id": 3, "provider": "openai", "label": "Production OpenAI",
    "last_four": "...r4nk", "created_at": "2026-03-28T09:00:00Z"
  }
}
GET/api/v1/model-keys

List all stored model keys. Full key is never returned.

DELETE/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:

HeaderDescription
X-RateLimit-LimitMax requests per minute
X-RateLimit-RemainingRemaining in current window

Phone Numbers

Search, purchase, manage, and release phone numbers for your AI agents.

GET/api/v1/phone-numbers/available

Search available phone numbers to purchase.

ParameterTypeRequiredDescription
countrystringOptional2-char country code (default: US)
area_codestringOptionalArea code filter (US/Canada)
localitystringOptionalCity or locality (e.g. "London", "San Francisco")
number_typestringOptionallocal, mobile, toll_free, national
containsstringOptionalDigits the number must contain
limitintegerOptionalMax results (1-50, default: 50)
Example
curl "https://agent.bubblyphone.com/api/v1/phone-numbers/available?country=US&area_code=415" \
  -H "Authorization: Bearer bp_live_sk_YOUR_KEY"
Response200 OK
{
  "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" }
}
POST/api/v1/phone-numbers

Purchase a phone number. Setup fee + first month deducted from balance.

ParameterTypeRequiredDescription
phone_numberstringRequiredE.164 format (e.g. +14155551234)
webhook_urlstringOptionalHTTPS URL for call events
labelstringOptionalFriendly name
country_codestringOptional2-char country code (default: US)
number_typestringOptionallocal, mobile, toll_free, national (default: local)
International numbers require verification
Non-US numbers require country verification first. Returns 403 if verification is not approved. See Country Verification.
Webhook Secret
The response includes a webhook_secret — shown only once. Store it securely.
Example
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"}'
Response201 Created
{
  "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"
  }
}
GET/api/v1/phone-numbers

List all your phone numbers.

GET/api/v1/phone-numbers/{id}

Get details for a specific phone number.

PATCH/api/v1/phone-numbers/{id}

Update webhook URL, label, or configure streaming mode.

ParameterTypeRequiredDescription
webhook_urlstringOptionalNew HTTPS webhook URL (webhook mode)
labelstringOptionalNew label
modestringOptional"webhook" (default) or "streaming"
modelstringOptionalAI model slug, e.g. "gemini-3.1-flash-live"
model_key_idintegerOptionalBYOK model key ID — omit to use platform-managed key
system_promptstringOptionalSystem prompt for the AI agent (required when mode=streaming)
voicestringOptionalVoice ID (e.g. "Kore")
toolsarrayOptionalArray of function definitions for tool use
tool_webhook_urlstringOptionalURL to receive tool/function call events (required if tools are set)
max_call_durationintegerOptionalMax call duration in seconds (60–3600, default 3600)
silence_timeoutintegerOptionalAuto-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.

Enable Streaming
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
  }'
Tool calls in streaming mode
When the AI decides to call a function, BubblyPhone POSTs a call.tool_call event to your tool_webhook_url. Respond with the result JSON and the AI continues the conversation.
DELETE/api/v1/phone-numbers/{id}

Release a phone number permanently.

Permanent action
Releasing a number is permanent. It returns to the pool and may be claimed by someone else.

Number Pricing

GET/api/v1/number-pricing

Get cached pricing for all countries. Filter by country or number type.

ParameterTypeRequiredDescription
countrystringOptional2-char country code (e.g. US, GB)
number_typestringOptionallocal, mobile, toll_free, national
Example
curl "https://agent.bubblyphone.com/api/v1/number-pricing?country=GB" \
  -H "Authorization: Bearer bp_live_sk_YOUR_KEY"
Response200 OK
{
  "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.

StatusMeaning
not_requiredNo documents needed (e.g. US numbers)
pendingDocuments submitted, awaiting review
approvedVerified — you can purchase numbers
rejectedVerification failed — resubmit documents
GET/api/v1/countries

List countries and your verification status for each.

Example
curl https://agent.bubblyphone.com/api/v1/countries \
  -H "Authorization: Bearer bp_live_sk_YOUR_KEY"
Response200 OK
{
  "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" }
  ]
}
GET/api/v1/countries/{code}/requirements

Get the required documents for purchasing a number type in a country.

ParameterTypeRequiredDescription
number_typestringOptionallocal, mobile, toll_free (default: local)
Example
curl "https://agent.bubblyphone.com/api/v1/countries/GB/requirements?number_type=local" \
  -H "Authorization: Bearer bp_live_sk_YOUR_KEY"
Response200 OK
{
  "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"
    }
  ]
}
POST/api/v1/countries/{code}/verify

Submit documents for country verification. Uses multipart/form-data.

ParameterTypeRequiredDescription
number_typestringRequiredlocal, mobile, or toll_free
documents[]arrayRequiredArray of document objects: each with a type (requirement ID) and file upload
Multipart upload
Send as Content-Type: multipart/form-data. Each document needs a type (requirement ID from the requirements endpoint) and a file.
Example
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"
Response201 Created
{
  "data": {
    "id": "ver_abc123",
    "country_code": "GB",
    "number_type": "local",
    "status": "pending",
    "created_at": "2026-03-28T10:00:00Z"
  }
}
GET/api/v1/countries/{code}/verification

Check your verification status for a country and number type.

ParameterTypeRequiredDescription
number_typestringOptionallocal, mobile, toll_free (default: local)
Response200 OK
{
  "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.

POST/api/v1/agents

Create a reusable AI agent.

ParameterTypeRequiredDescription
namestringRequiredHuman-readable name for this agent
descriptionstringOptionalOptional description of the agent's purpose
model_slugstringOptionalAI model slug (e.g. gemini-3.1-flash-live-preview)
system_promptstringOptionalInstructions for the AI agent (up to 10,000 chars)
voicestringOptionalTTS voice name (e.g. Kore, Aoede, Charon)
languagestringOptionalBCP-47 language code (e.g. en-US, es-US)
transfer_numberstringOptionalE.164 number for call transfers
recording_enabledbooleanOptionalRecord call audio (default: false)
Example
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
  }'
Response201 OK
{
  "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"
  }
}
GET/api/v1/agents

List all agents for your account.

Example
curl https://agents.bubblyphone.com/api/v1/agents \
  -H "Authorization: Bearer bp_live_sk_YOUR_KEY"
GET/api/v1/agents/{id}

Get a single agent by ID.

Example
curl https://agents.bubblyphone.com/api/v1/agents/1 \
  -H "Authorization: Bearer bp_live_sk_YOUR_KEY"
PATCH/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.

Example
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..."}'
DELETE/api/v1/agents/{id}

Delete an agent.

Fails with 422 if any phone numbers still reference this agent. Unlink them first.

Example
curl -X DELETE https://agents.bubblyphone.com/api/v1/agents/1 \
  -H "Authorization: Bearer bp_live_sk_YOUR_KEY"
POST/api/v1/calls

Use 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.

Example
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 Resolution Order
1) Per-call agent_idoverride → 2) Phone number's linked agent → 3) Phone number's inline fields (legacy fallback).
Migrating from previous version
When you migrated from the previous version, existing phone numbers automatically got agents created for them. Find them in your dashboard under Agents.

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.

Example
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

ParameterTypeRequiredDescription
textstring or nullOptionalFree text. Null if not found in transcript.
booleantrue or falseOptionalAlways returns true or false.
numbernumber or nullOptionalInteger or float. Null if not found.
selectorstringOptionalMust 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} returns analysis_result
  • Delivered via webhook: call.analysis.ready event fired to your phone number's webhook_url
Webhook payload
{
  "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"
}
Analysis uses the Whisper transcript if transcription is enabled, otherwise falls back to the real-time Gemini transcript. Both work well for extraction.

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.

POST/api/v1/calls

Initiate an outbound call.

ParameterTypeRequiredDescription
fromstringRequiredYour phone number (E.164)
tostringRequiredDestination number (E.164)
Example
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"}'
Response201 Created
{
  "data": {
    "id": 42, "from": "+14155551234", "to": "+15551234567",
    "direction": "outbound", "status": "initiated"
  }
}
GET/api/v1/calls

List calls with optional filters.

ParameterTypeRequiredDescription
directionstringOptionalinbound or outbound
statusstringOptionalinitiated, ringing, answered, completed, failed
phone_number_idintegerOptionalFilter by phone number
date_fromdatetimeOptionalStart date (ISO 8601)
date_todatetimeOptionalEnd date (ISO 8601)
GET/api/v1/calls/{id}

Get full details for a call.

Response200 OK
{
  "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"
  }
}
POST/api/v1/calls/{id}/speak

Send text-to-speech to an active call and gather the caller's response.

ParameterTypeRequiredDescription
textstringRequiredText to speak (max 5000 chars)
POST/api/v1/calls/{id}/hangup

End an active call.

POST/api/v1/calls/{id}/transfer

Transfer an active call to another number.

ParameterTypeRequiredDescription
tostringRequiredTransfer destination (E.164)
GET/api/v1/calls/{id}/transcript

Get post-call transcript (available after call ends).

GET/api/v1/calls/{id}/recording

Get the recording URL.

GET/api/v1/calls/{id}/events

Get the full event timeline for a call. Useful for debugging exactly what happened.

Response200 OK
{
  "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" }
    }
  ]
}
GET/api/v1/calls/{id}/webhook-logs

Get webhook delivery logs for a call — every attempt, response, and latency.

Response200 OK
{
  "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

initiated
ringing
answered
completed
failed
StatusDescriptionBilling
initiatedCall created
ringingRemote party ringing
answeredConnectedBilling starts
completedEnded normallyFinal charge
failedCould not connectNo charge

Call Pricing

ServiceRate
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.

POST/api/v1/sandbox/calls

Start a sandbox test call and get a WebSocket URL to connect your browser audio.

ParameterTypeRequiredDescription
phone_number_idintegerRequiredPhone number to test (must be in streaming mode)
overridesobjectOptionalOptional overrides: system_prompt, model, voice
Example
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." }
  }'
Response201 Created
{
  "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)

Audio frame
{ "type": "audio", "data": "<base64-encoded PCM16 16kHz mono>" }

Receive (server → client)

Audio response
{ "type": "audio", "data": "<base64-encoded audio>" }
Transcript
{ "type": "transcript", "role": "user", "text": "What are your hours?" }
{ "type": "transcript", "role": "model", "text": "We are open Monday to Friday, 9am to 5pm." }
Tool call
{ "type": "tool_call", "tool": "check_availability", "arguments": { "date": "2026-04-01" } }
Call ended
{ "type": "ended", "reason": "completed" }
Browser usage
Capture microphone audio with the Web Audio API, downsample to PCM16 at 16 kHz, base64-encode, and send as audio frames. Play received audio frames through an AudioContext.
POST/api/v1/sandbox/demo

Start a free demo call — no balance required, no phone number needed.

Try before you top up
Each account gets 3 free demo calls (2 minutes each). No parameters required.
Example
curl -X POST https://agent.bubblyphone.com/api/v1/sandbox/demo \
  -H "Authorization: Bearer bp_live_sk_YOUR_KEY"
Response201 Created
{
  "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

call.incoming

Inbound call arrived. Respond with an action.

Payload
{
  "event": "call.incoming", "call_id": "42", "phone_number_id": "1",
  "from": "+15551234567", "to": "+14155551234",
  "direction": "inbound", "timestamp": "2026-03-27T12:00:00Z"
}
call.answered

Call answered. Billing starts.

Payload
{"event": "call.answered", "call_id": "42", "timestamp": "2026-03-27T12:00:05Z"}
call.speech

Caller spoke. Contains transcribed text.

Payload
{
  "event": "call.speech", "call_id": "42",
  "text": "I'd like to make a reservation for two",
  "timestamp": "2026-03-27T12:00:15Z"
}
call.hangup

Call ended. Includes duration and cost.

Payload
{
  "event": "call.hangup", "call_id": "42",
  "duration_seconds": 120, "cost": 0.10,
  "hangup_cause": "normal_clearing", "timestamp": "2026-03-27T12:02:05Z"
}
call.transcript.ready

Transcript available. Fetch via the transcript endpoint.

Payload
{"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.started

Call answered and AI stream connected. Billing starts.

Payload
{
  "event": "call.started", "call_id": "42", "model": "gemini-3.1-flash-live",
  "timestamp": "2026-03-28T10:00:04Z"
}
call.tool_call

AI wants to call a function. Sent only to tool_webhook_url. Respond with the result.

Payload
{
  "event": "call.tool_call", "call_id": "42",
  "tool": "check_availability", "arguments": { "date": "2026-04-01" },
  "timestamp": "2026-03-28T10:00:20Z"
}
Your response
{ "result": { "available": true, "slots": ["10:00", "14:00"] } }
call.ended

Streaming call finished. Includes itemized cost breakdown.

Payload
{
  "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

JSON
{"action": "answer", "text": "Hello! How can I help?"}

speak

Send TTS

JSON
{"action": "speak", "text": "Your reservation is confirmed."}

transfer

Transfer call

JSON
{"action": "transfer", "to": "+15559876543"}

hangup

End call

JSON
{"action": "hangup"}

Retries

Non-2xx or timeout? We retry up to 3 times with exponential backoff:

AttemptDelay
1st retry5 seconds
2nd retry30 seconds
3rd retry120 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.

POST/api/v1/calls

Initiate an outbound call handled by a streaming AI agent.

ParameterTypeRequiredDescription
fromstringRequiredYour purchased phone number (E.164 format)
tostringRequiredDestination number (E.164 format)
modestringOptional"webhook" (default) or "streaming"
modelstringOptionalAI model slug (e.g. "gemini-3.1-flash-live-preview"). Falls back to phone number config.
system_promptstringOptionalInstructions for the AI agent. Falls back to phone number config.
voicestringOptionalTTS voice name (e.g. "Kore"). Falls back to phone number config.
toolsarrayOptionalTool definitions for function calling. Falls back to phone number config.
tool_webhook_urlstringOptionalHTTPS URL for tool call webhooks. Falls back to phone number config.
model_key_idintegerOptionalBYOK key ID. Falls back to phone number config.
transfer_numberstringOptionalNumber to transfer to if AI decides to hand off.
Example Request Body
{
  "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"
}
Response201 Created
{
  "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"
  }
}
Balance requirement
Your account needs enough balance to cover 5 minutes of telephony + AI model costs as escrow. The escrow is refunded when the call completes and actual charges are applied.

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 instructions
  • greeting_message — what triggers the AI's first response

Example request

Shell
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

ParameterTypeRequiredDescription
dynamic_variablesobjectOptionalKey-value pairs. Keys match {{variable_name}} tokens, values are strings (max 2000 chars)
greeting_messagestringOptionalCustom 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.

Response422 Unprocessable Entity
{
  "message": "Missing required dynamic variables.",
  "errors": {
    "dynamic_variables": ["Missing variables: customer_name, service"]
  }
}
Dynamic variables work for outbound calls only. Support for inbound calls, tool descriptions, and default values will be added in future iterations.

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.

PATCH/api/v1/phone-numbers/{id}

Update voicemail action for a phone number

ParameterTypeRequiredDescription
voicemail_actionstringOptional"hangup" (default) or "ai_message". Controls what happens when voicemail is detected on outbound calls.
Shell
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.

Shell
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": "..."
  }'
ParameterTypeRequiredDescription
voicemail_actionstringOptional"hangup" (default) or "ai_message". Only applies to outbound streaming calls.
When voicemail is detected, the call's 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.

Set a fixed transfer number
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.

Webhook request (call.tool_call)
{
  "event": "call.tool_call",
  "call_id": "45",
  "tool": "transfer_call",
  "arguments": {"reason": "customer wants billing help"}
}
Your webhook response
{
  "result": {
    "transfer_to": "+12025559999"
  }
}

Transfer Billing

Transferred calls are billed in two phases:

PhaseTelephonyAI Model
Phase 1 — AI activeNormal ratePer model price/min
Phase 2 — Transfer legOutbound rate to destination$0.00
Disable the built-in transfer tool
To disable the automatic 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.

POST/api/v1/calls/{id}/context

Send a context message to the AI agent on an active streaming call.

ParameterTypeRequiredDescription
messagestringRequiredContext message for the AI agent (max 2000 chars)
Request body
{
  "message": "The customer's account has been credited $50. Their new balance is $120."
}
Response200 OK
{
  "status": "delivered"
}
Only works on active streaming calls (mode: streaming, status: answered). Rate limited to 10 context injections per call.

Real-time Transcript Webhooks

Receive transcript segments via webhook as the conversation happens. Enable per phone number or per outbound call.

Enabling

Shell
curl -X PATCH https://agents.bubblyphone.com/api/v1/phone-numbers/1 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"realtime_transcript": true}'

Webhook Payload

JSON
{
  "event": "call.transcript.segment",
  "call_id": "45",
  "role": "user",
  "text": "I'd like to check my account balance",
  "timestamp": "2026-04-02T14:01:23Z"
}
Transcript segments are delivered fire-and-forget (no retries). The complete transcript is still available via call.transcript.ready after the call ends.

Call Recording

Record streaming call audio to the cloud. Enable per phone number or per outbound call.

Enable Recording

Shell
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

GET/api/v1/calls/{id}/recording

Get a time-limited signed URL for the call recording.

Response200 OK
{
  "data": {
    "recording_url": "https://..."
  }
}
Recording is free
Storage costs for call recordings are absorbed by the platform — you are not charged for recording storage.

Whisper Transcription

Get high-accuracy transcripts powered by Groq Whisper. Requires recording to be enabled.

Enable Transcription

Shell
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.

ParameterTypeRequiredDescription
whisper_transcriptstringOptionalFull text of the Whisper transcription
whisper_segmentsarrayOptionalArray of segments with start, end, and text fields

Pricing

Whisper transcription costs $0.03 per minute of audio transcribed.

Recording required
Transcription requires 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:

ScenarioTelephonyAI Model
Webhook modeNormal rate
Streaming (platform key)Normal ratePer model price/min
Streaming (BYOK)Normal rate$0.00
Sandbox call$0.00Per model price/min
GET/api/v1/billing/balance

Get current credit balance.

Response200 OK
{"data": {"balance": "24.50", "currency": "USD"}}
GET/api/v1/billing/transactions

List all transactions (cursor-paginated).

Response200 OK
{
  "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)"}
  ]
}
POST/api/v1/billing/top-up

Create a Stripe Checkout session to add credit.

ParameterTypeRequiredDescription
amountnumberRequiredOne of: 5, 10, 20, 50, 100 (USD)
Response200 OK
{"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_..."}
GET/api/v1/billing/auto-topup

Get your current auto top-up settings.

Response200 OK
{
  "data": {
    "enabled": false,
    "threshold": null,
    "amount": null,
    "has_payment_method": true
  }
}
PUT/api/v1/billing/auto-topup

Enable or update auto top-up. Automatically adds credit when balance falls below a threshold.

ParameterTypeRequiredDescription
enabledbooleanRequiredEnable or disable auto top-up
thresholdnumberOptionalTop-up when balance drops below this (min 1, max 100)
amountnumberOptionalAmount to add — one of: 10, 25, 50, 100 (USD)
Requires a saved card
Auto top-up requires at least one completed manual top-up via Stripe Checkout to save a payment method.
Example
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}'
Response200 OK
{
  "data": {
    "enabled": true,
    "threshold": 5,
    "amount": 25,
    "has_payment_method": true
  }
}
GET/api/v1/billing/usage

Usage summary for a billing period, broken down by category.

ParameterTypeRequiredDescription
periodstringOptionalYYYY-MM (default: current month)
Response200 OK
{
  "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.

BYOA — Bring Your Own Agent
BubblyPhone provides telephony. You bring your AI — connect any LLM (OpenAI, Claude, Gemini, etc.) to power your phone agent.

AI Receptionist

A webhook handler that answers calls and routes to your AI.

JavaScript
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.

Python
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.

Complete Workflow
# 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.

Store securely
We store a hash — if you lose the key, create a new one.
POST/api/v1/api-keys

Create a new API key.

ParameterTypeRequiredDescription
labelstringOptionalKey label (default: 'Default')
Response201 Created
{
  "data": {"id": 1, "label": "Production", "key": "bp_live_sk_abc123...", "created_at": "2026-03-27T12:00:00Z"}
}
GET/api/v1/api-keys

List all API keys (plaintext never exposed).

DELETE/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.

Node.js Verification
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...
});