Adding commercial guardrails to Vapi agents
You add commercial guardrails to Vapi agents by putting the authorization check inside your custom tool server: when Vapi sends a tool call to your endpoint, your handler drafts the response, posts it to EvalLayer's /authorize API, and returns only the approved version in the tool result the assistant speaks from. The assistant keeps its conversational freedom; the moments that can bind your business pass through policy first.
Why Vapi deployments need this layer
Vapi is the developer-first way to ship voice agents, and its flexibility is the point: you pick the model, voice, and transcriber, then wire tools to your own backend. Teams use it for sales lines, support desks, booking, and collections, all conversations where the agent states prices, offers credits, or promises follow-ups in real time. A spoken offer is instant and recorded; there is no review step between generation and the customer's ears.
Vapi's own control surface is solid on infrastructure: server URL authentication with managed credentials, HIPAA-oriented options, call recording controls, and three flavors of tools (custom tools that hit your server, code tools that run TypeScript on Vapi's side, and prebuilt integration tools). What none of that covers is commercial judgment, whether this specific discount is inside authority, whether that sentence is a delivery guarantee your ops team never agreed to. That check belongs in your tool server.
The interception point: your custom tool server
When a Vapi assistant invokes a custom tool, Vapi sends an HTTP request to the server URL you configured, carrying the tool call and a toolCallId. Your server does the work and responds with a result matched to that toolCallId; the assistant folds the result into what it says next. That round trip is where the guardrail lives. Nothing changes on the Vapi side except the tool definition; the enforcement is entirely in code you already control.
// Handler for Vapi custom tool calls on your server.
// Adapt the payload fields to Vapi's tool-call message shape.
app.post('/vapi/tools', async function (req, res) {
const toolCall = req.body.message.toolCallList[0];
const draft = buildOfferText(toolCall);
const check = await fetch('https://api.evallayer.ai/authorize', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.EVALLAYER_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
action_type: 'send_quote',
content: draft,
context: { channel: 'voice', platform: 'vapi' },
policy: {
max_discount_pct: 15,
commitment_authority_usd: 1000,
forbidden_commitments: ['delivery_date', 'indemnification'],
approved_claims: ['24_7_support'],
required_disclaimers: [],
blocked_topics: []
}
})
});
const result = await check.json();
let spoken;
if (result.decision === 'allow') {
spoken = draft;
} else if (result.decision === 'rewrite') {
spoken = result.approved_rewrite;
} else if (result.decision === 'require_approval') {
queueForHuman(draft, result.authorization_id);
spoken = 'I want to get that approved on our side first. We will confirm it right after this call.';
} else {
logBlocked(result.authorization_id, result.violations);
spoken = 'I cannot offer that here, but I can connect you with the team that can.';
}
return res.json({
results: [{ toolCallId: toolCall.id, result: spoken }]
});
});
The four decisions, in voice terms
- allow: the draft is inside policy; return it as the tool result.
- rewrite: return
approved_rewrite. The caller hears the compliant offer, capped discount and softened language included, with no awkward refusal. - require_approval: the model asked for something above its authority. Queue the draft with its
authorization_idand have the assistant commit to a follow-up, not the deal. - block: hard violation. The draft is never spoken; the assistant redirects, and the
authorization_idplusviolationsarray go to your logs.
Latency budget
Vapi tool calls are already an out-of-band trip to your server, so callers expect a beat of processing there. Deterministic-only policies (caps, authority limits, required text) add single-digit milliseconds. Semantic checks add roughly one to two seconds; configure the assistant to say a short filler line while the tool runs, the same trick you would use for a slow CRM lookup. The key architectural rule holds: check at the function-call moment where money moves, never on every utterance.
What to log
Persist the authorization_id next to Vapi's call ID from the tool-call message. Vapi's end-of-call report then joins cleanly with your authorization records: transcript on one side, policy decisions on the other. When a customer disputes what the assistant offered, you have the draft, the decision, the violations found, and the exact text that was actually spoken.
Start with the demo
Run a check from your terminal against /demo/authorize (three per day, no key) before touching your Vapi config, or watch the interactive demo catch three planted violations in a sample quote. When you wire it in, the authorization guide covers decision semantics and policy design covers choosing caps and forbidden commitments for a voice channel.