Docs

Connect anything that can open a WebSocket

Vocra runs the whole voice loop — speech recognition, the language model, speech synthesis, turn-taking and interruption — on our server, using the provider keys you saved in the dashboard. Your client only has to stream microphone audio up and play audio back down.

1. Create an agent and a token

Configure an agent under Agents, then create an access token under API keys. Tokens look like vk_live_… and are shown once — store it somewhere safe.

2. Open a session

Connect to wss://voice.vocra.cloud/v1/ws and send a start frame within five seconds.

{
  "t": "start",
  "auth": "vk_live_…",
  "agent_id": "<agent uuid>",
  "audio_in":  { "encoding": "pcm16", "sample_rate": 16000, "channels": 1 },
  "audio_out": { "encoding": "mp3" }
}

The server replies with ready, then drives the conversation. Send raw PCM16 little-endian mono frames as binary messages — 20–100 ms per frame works well.

3. Handle the downstream frames

Text frames are JSON with a t discriminator: state, stt.interim, stt.final, llm.delta, assistant.message, barge_in, metrics, error and closed.

Binary frames carry audio: an 8-byte header (uint32 little-endian epoch, then uint32 little-endian seq) followed by one fully decodable audio clip for one sentence. Play clips in seq order, and the moment you receive a barge_in frame, throw away everything with an older epoch and stop playback — that is what makes interruptions feel instant.

const frame = new DataView(buf);
const epoch = frame.getUint32(0, true);
const seq   = frame.getUint32(4, true);
const audio = buf.slice(8);       // decodable clip (mp3 by default)

if (epoch < currentEpoch) return; // stale turn — drop it

4. Place a phone call

With a Pro plan, a saved Twilio credential and a number added under Phone numbers, an agent can dial out:

curl -X POST https://vocra.cloud/api/calls/outbound \
  -H "Authorization: Bearer vk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"agentId":"<uuid>","to":"+201234567890","from":"+15551234567"}'

The response contains a callId. Poll the call in the dashboard, or read it back from the API once the call completes to get the transcript and latency metrics.

Reading calls back

Every call — web, SDK or phone — leaves a row you can read with the same API key.

# List calls, newest first. Keyset pagination via nextCursor.
curl "https://www.vocra.cloud/api/calls?limit=25" \
  -H "Authorization: Bearer vk_live_..."

# One call, with its full transcript, analysis and recording status
curl "https://www.vocra.cloud/api/calls/CALL_ID" \
  -H "Authorization: Bearer vk_live_..."

Phone numbers: three ways to connect one

Vocra reaches the phone network three ways, and a number carries its own answer — you never pick a carrier per call. The first two are bring-your-own: per-minute cost stays with your carrier and Vocra takes none of it, which is what makes the flat monthly fee possible in the first place.

  • Twilio. Connect your Twilio account under Providers, register the number, and point its A call comes in webhook at Vocra. Per-minute cost stays on your Twilio bill.
  • Bring your own SIP trunk. Connect any carrier that speaks SIP — your own Telnyx account, Twilio Elastic SIP, a PBX — under Phone numbers. Vocra terminates the calls on its own SIP gateway, so there is no CPaaS in the middle taking a margin on every minute. Point your carrier’s origination for the number at sip:+E164@sip.voice.vocra.cloud:5060 over UDP or TCP.
  • Get a Vocra number. The one option that is not bring-your-own — see the next section. Vocra carries the traffic on its own carrier account, which is why it is billed per minute rather than included in the flat fee.

Outbound is the same call either way. from must be a number registered on your account, and its provider decides how the call is placed and billed.

curl -X POST https://www.vocra.cloud/api/calls/outbound \
  -H "Authorization: Bearer vk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "AGENT_ID",
    "to": "+15551234567",
    "variables": { "first_name": "Dana", "amount_due": "$120" }
  }'

variables fill in {{first_name}}-style tokens in the agent’s persona, greeting and farewell for that one call. A token with no matching variable is removed rather than read aloud.

Vocra-provisioned numbers, SMS and email

Every plan’s unlimited minutes are unlimited on your own provider keys — that qualifier is on every price and usage meter in the dashboard, and it is the whole reason it exists. A Vocra-provisioned number is different: Vocra pays its own carrier for it, per minute, per SMS segment and per number, so this one surface is metered against a monthly bundle instead of flat. Available on the Pro plan and up, and dark entirely on a deployment that has not configured a carrier.

# Order a number (US only). Past your plan's included count this
# requires acknowledgeExtraCost — a recurring charge nobody clicked
# for is exactly the kind of billing surprise Vocra avoids elsewhere.
curl -X POST https://www.vocra.cloud/api/vocra-numbers \
  -H "Authorization: Bearer vk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"areaCode": "415"}'

# List what you have, and any orders still settling
curl https://www.vocra.cloud/api/vocra-numbers \
  -H "Authorization: Bearer vk_live_..."

# Give one back — irreversible; Telnyx does not hold a released number
curl -X DELETE https://www.vocra.cloud/api/vocra-numbers/NUMBER_ID \
  -H "Authorization: Bearer vk_live_..."

Point a Vocra number at an agent under Phone numbers and inbound calls route to it like any other number. Outbound calls placed from one claim a minute of the bundle before dialling — a call that would exceed the bundle is refused rather than started, so an exhausted account never runs up a bill it did not enable overage for.

SMS on a Vocra number requires 10DLC registration first — US carriers filter or block unregistered application-to-person traffic, and it damages deliverability for every number on the platform, not just yours. Register once under Messaging; carrier vetting then takes days and the status updates on its own. Every send checks it, in both directions:

curl -X POST https://www.vocra.cloud/api/messages \
  -H "Authorization: Bearer vk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"from": "+15551234567", "to": "+15559876543", "body": "Your appointment is confirmed for 3pm."}'

STOP and HELP are handled automatically and cannot be turned off: STOP records an opt-out and sends exactly one confirmation, HELP sends the required informational reply, and neither goes through your allowance — they are compliance traffic, not customer traffic. An opted-out recipient stays opted out of messages from your account specifically; it is not a platform-wide list.

Email works the same shape: a sender identity (a real business name and postal address — CAN-SPAM requires both, and there is no honest way to invent either on your behalf) is required before any agent-action or campaign email sends. Every send carries a working unsubscribe link and a List-Unsubscribe header; the link never expires. The same page has a test send (one email to your own address, so you can check the sender name and footer before a customer sees them) and a delivery log showing every email’s status — sent, delivered, bounced, blocked or failed — with the provider’s reason attached.

Usage bundles and overage

Vocra-carried voice minutes, SMS segments and emails each draw from a monthly bundle sized by plan. The bundle resets on the billing period and a mid-period upgrade raises it immediately; a downgrade takes effect at the next period rather than clawing back what was already paid for. By default a bundle simply stops working once used up — the call is refused before it is placed, the text is queued as blocked, the email is queued as blocked — rather than silently running a bill. Turn on overage in Billing to keep going past the bundle at the published per-unit rate instead.

Post-call analysis

When a call ends, the agent’s own model reads the transcript and returns a summary, the caller’s sentiment, whether the call achieved its purpose, and any custom fields you defined on the agent. It runs on your provider key — one short completion per call — and the result lands on the call row and in the call_analyzed webhook.

"analysis": {
  "summary": "Caller asked about their August invoice and booked a callback for Thursday.",
  "sentiment": "positive",
  "call_successful": true,
  "custom_analysis_data": {
    "callback_date": "2026-08-14",
    "invoice_number": "INV-8821",
    "outcome": "callback"
  }
}

Define the custom fields under After the call on the agent. A field the transcript does not answer comes back as null rather than a guess.

Post-call actions

An agent can carry one SMS action and one email action that fire automatically once analysis finishes, each gated by a condition — always, or only when an analysis field equals a particular value. “Text them the booking link if outcome is callback” is a condition, not custom code.

"postCallActions": {
  "sms": {
    "enabled": true,
    "condition": { "field": "outcome", "equals": "callback" },
    "template": "Hi {{first_name}}, following up on your call — book here: {{booking_link}}"
  },
  "email": {
    "enabled": true,
    "condition": "always",
    "toField": "caller_email",
    "subject": "Your call summary",
    "template": "Thanks for calling. Summary: {{summary}}"
  }
}

{{variables}} in the template render from the call’s own variables and the analysis fields together. Email needs toField because a phone call never learns a caller’s address on its own — name the analysis field that captured it (a custom field you defined, most often), and the action is skipped with a logged reason, not a guess, when that field comes back empty. Both actions consume the same monthly bundle and 10DLC/sender-identity requirements as sending manually.

Webhooks

Vocra posts three events per call, all carrying the same call_id: call_started, call_ended (with the transcript) and call_analyzed (with the analysis). Add an endpoint under Webhooks.

Each POST carries x-vocra-event, x-vocra-delivery-id (use it as an idempotency key) and x-vocra-signature. A non-2xx response is retried three times over about six minutes, then marked failed in the dashboard log.

// Verify before you trust it — the URL is public.
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.trim().split('=')),
  );
  const timestamp = Number.parseInt(parts.t, 10);

  // Reject an old delivery, or a captured one can be replayed forever.
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(timestamp + '.' + rawBody)
    .digest('hex');

  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Campaigns

Upload a contact list and Vocra works through it for you, respecting a concurrency limit (voice) or per-user pacing (SMS/email), a calling window in the contacts’ timezone, a daily cap and a retry policy. Each contact carries its own variables, so one agent or one template covers the whole list.

channelvoice (default), sms or email — is fixed at creation and cannot change later; a running campaign has to be paused before anything else about it can be edited. SMS and email campaigns replace agentId with messageTemplate (and emailSubject for email), must send from a Vocra-provisioned number or sender identity, and draw down the same monthly bundle a manual send does — a campaign that hits the bundle pauses with pauseReason: "quota" rather than turning every remaining contact into a failure.

# 1. Create it (starts as a draft — nothing dials or sends yet)
curl -X POST https://www.vocra.cloud/api/campaigns \
  -H "Authorization: Bearer vk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "AGENT_ID",
    "name": "August renewals",
    "fromNumber": "+15551234567",
    "concurrencyLimit": 3,
    "schedule": {
      "timezone": "America/New_York",
      "windowStart": "09:00",
      "windowEnd": "19:00",
      "daysOfWeek": [1, 2, 3, 4, 5]
    },
    "retry": { "maxAttempts": 2, "retryDelayMinutes": 60, "retryOn": ["no_answer"] }
  }'

# ...or an SMS campaign instead of a voice one:
curl -X POST https://www.vocra.cloud/api/campaigns \
  -H "Authorization: Bearer vk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "name": "August renewal reminders",
    "fromNumber": "+15551234567",
    "messageTemplate": "Hi {{first_name}}, your renewal is due — reply STOP to opt out."
  }'

# 2. Add contacts (up to 1000 per request)
curl -X POST https://www.vocra.cloud/api/campaigns/CAMPAIGN_ID/contacts \
  -H "Authorization: Bearer vk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"contacts":[{"phone":"+15559876543","email":"dana@example.com","variables":{"first_name":"Dana"}}]}'

# 3. Start it
curl -X POST https://www.vocra.cloud/api/campaigns/CAMPAIGN_ID/start \
  -H "Authorization: Bearer vk_live_..."

Pausing stops new calls or sends; anything already in flight finishes. Cancelling also cancels every contact still waiting, but never hangs up a live conversation or un-sends a message. Set "dryRun": true to walk the whole thing through — claims, retries, completion — without placing a single call or sending a single message.

Calling and sending windows are how you respect local calling-hours law. Vocra does not infer a contact’s timezone from their number or address; the window you set is the rule it follows.

Recordings

Phone calls are recorded in stereo — caller on the left channel, agent on the right — and kept for as long as your plan keeps transcripts. Turn it off per agent under After the call.

curl "https://www.vocra.cloud/api/calls/CALL_ID/recording" \
  -H "Authorization: Bearer vk_live_..."
# -> { "url": "https://...", "expiresAt": "...", "channels": { "left": "caller", "right": "agent" } }

The link is signed and lasts an hour, which is why webhooks carry recording_status rather than a URL — fetch one when you actually need the audio.

Automate with n8n

The webhooks above work with n8n’s stock Webhook node, and the REST API with its HTTP Request node — no custom node needed. Three ready-made workflows to import:

Each one starts with a Code node that verifies the signature. Keep it. Each also filters to call_analyzed — all three events fire per call, and forgetting to filter is the usual reason a workflow appears to run three times.

Flutter and mobile

The open-source vocra_flutter package runs the same pipeline on-device with your keys held locally. It is a good fit when you want no server hop at all; use this cloud API when you would rather keep keys off the device and share agents across platforms.

Limits

Sessions end on the agent’s max duration, on a configured end phrase, or after a silence timeout. Concurrent sessions are capped by your plan; a session over the cap is rejected with {"t":"error","code":"concurrency_limit"}.

Each agent has a “reply after caller silence” setting — how long a caller can pause before the agent treats their turn as complete and starts replying, 1 second by default. It is distinct from interrupt sensitivity, which controls how readily the agent stops talking once the caller starts: one governs how patient the agent is waiting for the caller to finish, the other how quickly it yields the floor once they do.

A call on a Vocra-provisioned number carries one more close reason: quota_exhausted, when the monthly Vocra-minute bundle runs out mid-call or was already gone when the call arrived. It only ever applies to Vocra-carried calls — BYO Twilio and BYO SIP have no bundle to exhaust, since their minutes are never metered by Vocra at all.

Docs — Vocra