Bring Your Own Model (BYOM)

What you build: an agent whose “brain” is your server. Voicebip answers the call, transcribes the caller, and POSTs each turn to your signed webhook_url. You run whatever model you like (OpenAI, Claude, a fine-tune, a rules engine), return the reply text, and Voicebip speaks it. You own the prompt and the model; Voicebip owns the phone line.

Caller speaks ─▶ Voicebip STT ─▶ POST agent.webhook_url ──▶ your server
BYOMVoiceWebhookRequest │ your model + your prompt
Caller hears ◀─ Voicebip TTS ◀─ { "text": "…" } ◀─────────┘

Who it’s for: teams with an existing LLM stack, strict data-residency needs, or a proprietary model they can’t hand over. Also the escape hatch when hosted-AI features don’t fit.

BYOM is a signed relay — no Voicebip model runs. Because your endpoint is the model, the dashboard’s system_prompt and tool_definitions fields do not apply to BYOM agents — your prompt and tools live in your own code. If you want Voicebip to run the model and call your functions instead, that’s Tool-calling, a different (hosted) path.

Prerequisites

  • An HTTPS webhook_url that responds within 5 seconds (the hard timeout — slower responses are treated as failures and count against the circuit breaker).
  • Your workspace signing secret (dashboard → Workspace → Signing Secret) — BYOM requests are HMAC-signed and you must verify them.

Step 1 — Create a BYOM agent

Set ai_provider: "byom" and point webhook_url at your endpoint:

$curl -X POST https://api.voicebip.com/v1/agents \
> -H "Authorization: Bearer pk_xxx" -H "Content-Type: application/json" \
> -d '{
> "display_name": "Custom-LLM Agent",
> "ai_provider": "byom",
> "webhook_url": "https://you.example.com/voice-turn",
> "tts_voice": "<voice_id>",
> "greeting_text": "Hi, you are through to Acme. How can I help?"
> }'

Step 2 — Handle the turn request

Each caller turn arrives as a signed POST with a BYOMVoiceWebhookRequest body. Verify (X-Voicebip-Signature = HMAC-SHA256 of "{timestamp}.{raw_body}", timestamp within ±300 s), run your model, return { "text": ... }:

1// Express — BYOM voice turn handler
2import express from "express";
3import crypto from "crypto";
4import OpenAI from "openai";
5
6const app = express();
7app.use(express.raw({ type: "application/json" }));
8const openai = new OpenAI();
9
10app.post("/voice-turn", async (req, res) => {
11 // 1. verify — fail CLOSED if it doesn't match
12 const sig = req.header("X-Voicebip-Signature");
13 const ts = req.header("X-Voicebip-Timestamp");
14 const mac = crypto.createHmac("sha256", process.env.VOICEBIP_SIGNING_SECRET)
15 .update(`${ts}.${req.body}`).digest("hex");
16 if (sig !== `sha256=${mac}`) return res.sendStatus(401);
17
18 const turn = JSON.parse(req.body);
19 // turn.messages = rolling history (role/text/timestamp); turn.transcription = latest caller utterance
20
21 // 2. run YOUR model with YOUR prompt (keep it under 5s)
22 const completion = await openai.chat.completions.create({
23 model: "gpt-4o",
24 messages: [
25 { role: "system", content: "You are Acme's support agent. Be concise." },
26 ...turn.messages.map(t => ({
27 role: t.role === "caller" ? "user" : "assistant", content: t.text
28 })),
29 { role: "user", content: turn.transcription },
30 ],
31 });
32
33 // 3. return reply text — Voicebip speaks it
34 res.json({ text: completion.choices[0].message.content });
35});
36app.listen(3000);

The inbound BYOMVoiceWebhookRequest:

1{
2 "event": "turn",
3 "agent_id": "agt_…",
4 "call_id": "call_5xk…",
5 "from_number": "+2348031234567",
6 "to_number": "+2341000000000",
7 "transcription": "How long do refunds take?",
8 "messages": [
9 { "role": "caller", "text": "Hi", "timestamp": "2026-07-02T09:13:00Z" },
10 { "role": "agent", "text": "Hi, you are through to Acme. How can I help?", "timestamp": "2026-07-02T09:13:02Z" }
11 ]
12}

Your response is a BYOMVoiceWebhookResponse. Only text is required; end_call, transfer_to, and dtmf are mutually exclusive with a normal reply:

1{ "text": "Refunds take 5 to 7 business days." }
1{ "text": "Transferring you now.", "transfer_to": "+2348000000000" }

Step 3 — Inbound screening (optional)

If the agent has screening_enabled, Voicebip fires one pre-greeting request with event: "call.screening" (empty messages, no transcription). Reply { "end_call": true } to reject the caller (optionally with text spoken before hangup) or { "end_call": false } to allow (optionally with text as a dynamic greeting override). On error/timeout, the agent’s screening_fail_closed policy decides.

Step 4 — Add RAG yourself (optional)

BYOM doesn’t auto-retrieve. Query a knowledge base and inject the chunks into your prompt before calling your model:

1const { chunks } = await queryKB(turn.transcription, 3);
2const context = chunks.map(c => c.content).join("\n---\n");
3// prepend `context` to your system message

Messaging BYOM

The same pattern covers WhatsApp/SMS: inbound messages arrive as a BYOMMessagingWebhookRequest ({agent_id, conversation_id, channel, from_number, to_number, inbound_message, history, metadata}) at the same webhook_url, and you return the reply. See WhatsApp AI agent.

Fail-closed signing

BYOM requests are signed, and Voicebip refuses to send unsigned — if your signing secret can’t be resolved, the request is dropped, not sent in the clear (tracked via voicebip_byom_signing_errors_total). On your side, verify every request and reject mismatches. Secret rotation moves the old secret to a 24-hour grace window (signing_secret_previous), so accept either the current or previous secret during rotation. Rotate via POST /v1/workspace/signing-secret/rotate (the plaintext is shown once).

Other stacks

Reference handlers for Flask, Go, and Next.js, plus Claude and OpenAI integrations, live in Code Examples — the same verify → model → reply shape.

Test it

BYOM turns are dispatched to your webhook_url during a live call, so they aren’t part of POST /v1/webhooks/test (which covers call.* / message.* lifecycle events). Test by POSTing a sample BYOMVoiceWebhookRequest (above) to your own endpoint with a valid signature, then place a real call in the sandbox to see it end-to-end.

Troubleshooting

SymptomLikely cause
Agent says nothing / hangs upYour endpoint took over 5 s or returned non-2xx; the circuit breaker may have opened.
401 storm in your logsSignature computed over parsed JSON, not the raw body; or wrong secret during rotation.
System prompt “ignored”Expected — the dashboard system_prompt doesn’t apply to BYOM; set it in your own code.
Reply not spokenResponse missing the required text field.

Next steps