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": ... }:

// Express — BYOM voice turn handler
import express from "express";
import crypto from "crypto";
import OpenAI from "openai";
const app = express();
app.use(express.raw({ type: "application/json" }));
const openai = new OpenAI();
app.post("/voice-turn", async (req, res) => {
// 1. verify — fail CLOSED if it doesn't match
const sig = req.header("X-Voicebip-Signature");
const ts = req.header("X-Voicebip-Timestamp");
const mac = crypto.createHmac("sha256", process.env.VOICEBIP_SIGNING_SECRET)
.update(`${ts}.${req.body}`).digest("hex");
if (sig !== `sha256=${mac}`) return res.sendStatus(401);
const turn = JSON.parse(req.body);
// turn.messages = rolling history (role/text/timestamp); turn.transcription = latest caller utterance
// 2. run YOUR model with YOUR prompt (keep it under 5s)
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: "You are Acme's support agent. Be concise." },
...turn.messages.map(t => ({
role: t.role === "caller" ? "user" : "assistant", content: t.text
})),
{ role: "user", content: turn.transcription },
],
});
// 3. return reply text — Voicebip speaks it
res.json({ text: completion.choices[0].message.content });
});
app.listen(3000);

The inbound BYOMVoiceWebhookRequest:

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

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

{ "text": "Refunds take 5 to 7 business days." }
{ "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:

const { chunks } = await queryKB(turn.transcription, 3);
const context = chunks.map(c => c.content).join("\n---\n");
// 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