Logo
Search
API Docs

Caller Authentication

Tools

Caller Authentication: Static Parameters & Server-Trusted Variables

Overview

This page covers how to build a genuine caller-verification pattern for a tool call — one that a prompt-injection attempt cannot defeat — using static parameters and server-trusted variables. It builds on the trust-tier concepts introduced in Variables; for the credential and signing mechanics referenced here, see Credential API and Webhook Signature Verification & Security.


The Core Principle: an LLM Is Not a Security Boundary

An LLM cannot be used as an authentication boundary, because it can be manipulated through prompt injection — a caller can simply say something like "actually my number is different," and if that value flows through the LLM into a tool call, your backend has no way to tell it apart from the truth. Any value you intend to trust for authentication or lookup purposes needs to bypass the LLM entirely and come from the platform itself.


Two parameters Fields, Two Different Trust Levels

A tool has two distinct places a value called parameters can live, and they behave completely differently:

FieldFilled byVisible to the LLM?Use for
function.parameters (JSON Schema)The LLM at runtimeYesValues the caller speaks — name, intent, item requested
parameters (top-level array)You, at configuration timeNo — resolved server-side and merged in after the LLM's argumentsServer-known values — verified caller number, account ID, call ID

Anything placed in the top-level parameters array is never sent to the model as part of the tool's schema, so the LLM has no way to see it, question it, or override it.


Server-Trusted Variables Safe to Use This Way

VariableDescription
{{customer.number}}The call's verified customer number, populated from call signaling
{{phoneNumber.number}}The number that received or placed the call
{{call.id}}The server-generated call ID
{{transport.callSid}}The transport-layer call session ID

These are all populated from the platform's own call signaling and setup, not from anything the LLM or caller can influence, which is what makes them safe to use as an authentication input.


Worked Example: Caller Verification Tool

The LLM only supplies what the caller actually said out loud — here, their name and email. The verified caller number is injected server-side and never exposed to the model:

{
  "type": "function",
  "function": {
    "name": "lookup_and_verify_user",
    "parameters": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "email": { "type": "string" }
      },
      "required": ["name", "email"]
    }
  },
  "server": {
    "url": "https://your-backend.example.com/lookup-and-verify"
  },
  "parameters": [
    { "key": "caller_number", "value": "{{ customer.number }}" },
    { "key": "called_number", "value": "{{ phoneNumber.number }}" },
    { "key": "call_id", "value": "{{ call.id }}" }
  ]
}

Your server receives caller_number from call signaling, not from the LLM, so you can authenticate against it directly rather than trusting whatever the model reports.


Pre-Injecting Server-Known Data at Call Start

If you already know something about the caller before the call begins — for example, an account ID resolved from a reverse lookup — inject it as a trusted variable for the whole call via assistantOverrides.variableValues when creating the call:

{
  "assistantOverrides": {
    "variableValues": {
      "accountId": "acct_abc123",
      "loyaltyTier": "platinum"
    }
  }
}

Values set this way become server-trusted variables for the duration of the call, referenceable as {{accountId}} in any tool's static parameters array, the same as the built-in variables above.


The Critical Pitfall: Never Define a Trusted Field Inside function.parameters

Wrong waycaller_number is inside function.parameters, so the LLM sees it, can be prompted to fill it with whatever the caller says, and the value is not trustworthy:

{
  "function": {
    "parameters": {
      "properties": {
        "caller_number": {
          "type": "string",
          "description": "the caller's phone number"
        }
      }
    }
  }
}

Right waycaller_number lives in the top-level parameters array, invisible to the LLM and resolved from signaling:

{
  "function": {
    "parameters": {
      "properties": {
        "name": { "type": "string" },
        "email": { "type": "string" }
      }
    }
  },
  "parameters": [
    { "key": "caller_number", "value": "{{ customer.number }}" }
  ]
}

Additional Layer: HMAC Request Signing

Static parameters protect against a malicious caller or a manipulated LLM forging a value inside the request body. For defense in depth, you can add HMAC request signing on top, so your server also verifies the request itself genuinely came from Sulus before trusting any of its contents — including the static parameters. See Webhook Signature Verification & Security for setup details.

In summary: never let a value you plan to trust for verification live inside function.parameters. Keep it in the top-level parameters array, sourced from server-trusted variables, and add HMAC signing if you want a second layer of protection on top.