EvalLayerTry the demo

Adding commercial guardrails to Vapi agents

Published 2026-07-27 · EvalLayer

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_id and 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_id plus violations array 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.

Frequently asked questions

Should I authorize every assistant response in Vapi?

No. Check at the tool-call moment, when the assistant is about to produce a quote, refund, or commitment through one of your custom tools. Deterministic policy checks return in milliseconds; semantic checks take one to two seconds, which is acceptable inside a tool call but not on every conversational turn.

What if my Vapi assistant quotes a price without calling the tool?

Make the tool the only source of truth: put pricing behind it, keep numbers out of the prompt, and instruct the assistant that offers must come from the tool. The model then has nothing accurate to say without calling it. The tool boundary is enforceable in a way prompt instructions alone are not.

How do I try EvalLayer without a key?

POST to /demo/authorize, which allows three checks per day with no key, or use the interactive demo at /authorize-demo. Production traffic authenticates with a Bearer sk_ key, or pays $0.01 per request over x402 if you prefer usage-based access without account setup.

See the authorization layer live

Paste a message, set a policy, get allow / rewrite / require approval / block in seconds.

Try the demo