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 BG-Tel, a 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.

For BG-Tel, request a SIP trunk with IP authorization where possible. Enter the outbound gateway and source IPs BG-Tel assigns to you in the SIP-trunk form, then ask BG-Tel to route your DID to the origination address above. Username/password authorization is also supported; Vocra does not require or store a BG-Tel portal login.

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.

channel — voice (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.