Identity verification for inbound SIP callers in LiveKit happens at three layers: the trunk, the dispatch rule, and the agent itself. Each layer gives you progressively more granular control, and in practice you'd combine them.
If you just want to know how to verify the caller from within the agent, skip to the Agent-driven verification section.
1. Trunk-level filtering (before LiveKit accepts the call)
When you create an inbound SIP trunk, you can answer two separate questions before LiveKit accepts the call: is the trunk itself trustworthy? and is the caller who they claim to be?.
Verifying the trunk
Many of the parameters you specify when creating the trunk verify the provider, not the caller. This prevents someone from bypassing your SIP carrier and injecting rogue INVITEs directly at your LiveKit endpoint. For example, LiveKit uses auth_username / auth_password to authenticate inbound INVITEs from your provider.
For a full list of supported parameters, see the docs for CreateSIPInboundTrunk. LiveKit offers several provider-specific quickstarts that detail any considerations particular to your SIP provider.
Verifying the caller
If you need to restrict access to a defined list of callers, you can specify allowed_numbers on your incoming SIP trunk. Calls from any other number are rejected at the trunk:
1{2"trunk": {3"name": "My inbound trunk",4...5"allowedNumbers": ["+13105550100", "+17145550100"]6}7}
This is a coarse filter for expected callers, not strong proof of identity since the caller ID over the PSTN is trivially spoofable.
You can leave allowed_numbers empty to accept calls from any number.
2. Dispatch-rule-level controls (when routing into a room)
The dispatch rule decides which LiveKit room an accepted call joins, and it has its own configurable parameters.
PIN-protected rooms
If you need to restrict access to users based on a shared secret, you can prompt them to enter a PIN using their keypad (DTMF) before being allowed to join the room. Define a pin on the dispatch rule as follows:
1{2"dispatch_rule": {3"name": "protected room rule",4"trunk_ids": [],5"rule": {6"dispatchRuleDirect": {7"roomName": "protected-room",8"pin": "12345"9}10}11}12}
Inbound numbers (inbound_numbers)
Similar to allowed_numbers at the trunk level, inbound_numbers matches against a defined list of caller numbers, allowing you to apply different policies per caller, such as routing them to a specific room, or dispatching a separate agent.
This is a useful feature if you expect calls from a defined set of numbers, but this parameter can be left blank to accept calls from any number. As stated previously, rules based on the caller's number should be treated as a routing hint rather than secure authentication.
1{2"dispatch_rule": {3"name": "VIP dispatch rule",4"inbound_numbers": ["+13105550100", "+17145550100"],5"rule": {6"dispatchRuleIndividual": {7"roomPrefix": "vip-"8}9},10"roomConfig": {11"agents": [{ "agentName": "vip-agent" }]12}13}14}
3. Participant-level identification (inside the agent)
Once the call is accepted, it becomes a SIP participant in the room with kind == SIP and a bundle of auto-populated sip.* attributes the agent can read.
SIP phone number (sip.phoneNumber)
The caller's phone number (ANI) is the most common form of caller identification you can use to look up account info. As mentioned previously, the caller's number it should not be used on its own to securely identify the user. Also note that this attribute isn't available if hide_phone_number is set on the dispatch rule.
1from livekit import rtc23@ctx.on("participant_connected")4async def on_participant(participant: rtc.RemoteParticipant):5if participant.kind != rtc.ParticipantKind.PARTICIPANT_KIND_SIP:6return78caller_number = participant.attributes.get("sip.phoneNumber")
Other SIP attributes
The full list of sip.* attributes is available from the SIP participant docs.
Some of the relevant attributes for caller identification are as follows:
| Attribute | Purpose |
|---|---|
sip.phoneNumber | Caller's number, as detailed above |
sip.trunkPhoneNumber | Number the caller dialed, which can be useful for multi-tenant routing |
sip.trunkID | Trunk ID. Use this to distinguish an "authenticated-customer trunk" from the "public support trunk" |
sip.ruleID | Dispatch rule ID that matched |
Custom SIP headers
Any X-* headers on the INVITE can be mapped to participant attributes via headers_to_attributes on the trunk. This is how you propagate things like an already-authenticated customer ID from an upstream PBX or IVR:
1{2"trunk": {3"name": "Authenticated IVR trunk",4"numbers": ["+15105550100"],5"headers_to_attributes": {6"X-Customer-Id": "customer.id",7"X-Auth-Token": "customer.auth_token"8}9}10}
Then in the agent:
1customer_id = participant.attributes.get("customer.id")2upstream_token = participant.attributes.get("customer.auth_token")34if customer_id and await verify_upstream_token(customer_id, upstream_token):5logger.info(f"Caller pre-authenticated by upstream IVR as {customer_id}")
Agent-driven verification
Finally, the agent can verify identity in the conversation itself: ask for an account number, date of birth, shared secret, or OTP and authenticate against your backend via tool calls. This is the only layer that doesn't rely on the signalling path, and the agent should call it before discussing anything account-sensitive:
1# Verify identity based on knowledge only the user should know2@llm.function_tool3async def verify_caller_identity(4context: RunContext,5account_number: str,6passphrase: str,7date_of_birth: str,8) -> str:9"""Verify the caller's identity against their account record.10Call this before discussing any account-specific details."""11phone = context.session.participant.attributes.get("sip.phoneNumber")1213ok = await crm.verify(14phone=phone,15account_number=account_number,16passphrase=passphrase,17date_of_birth=date_of_birth,18)19if ok:20context.session.userdata["verified"] = True21return "Identity verified. You may now discuss account details."22return "Verification failed. Do not share account-specific information."
1# Verify identity based on SMS2@llm.function_tool3async def send_otp(context: RunContext) -> str:4"""Send a one-time code by SMS to the caller's number on file."""5phone = context.session.participant.attributes.get("sip.phoneNumber")6code = await otp_service.send(phone)7context.session.userdata["pending_otp"] = code8return f"A 6-digit code has been sent to {phone[-4:]}. Please read it back."
Use Tasks to verify the user's identity before handing off to another agent to handle sensitive requests.
Putting it together
Trunk and dispatch-rule controls decide who gets through and where the call lands. They're useful filters, but they lean on data the caller or upstream carrier can influence. sip.phoneNumber and custom header attributes inside the agent are good for a first-pass CRM lookup, but they carry the same caveat: none of them are proof of who is actually on the line.
Before your agent discusses anything sensitive, confirm the caller's identity in the conversation itself. Treat everything upstream of the agent as filtering and routing, and treat in-conversation verification as mandatory.