Skip to main content

livekit_api/services/
sip.rs

1// Copyright 2025 LiveKit, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use livekit_protocol as proto;
16use std::collections::HashMap;
17use std::time::Duration;
18
19use crate::access_token::{SIPGrants, VideoGrants};
20use crate::get_env_keys;
21use crate::services::dial_timeout::{dial_timeout, DEFAULT_RINGING_TIMEOUT};
22use crate::services::twirp_client::TwirpClient;
23use crate::services::{ServiceBase, ServiceResult, LIVEKIT_PACKAGE};
24use pbjson_types::Duration as ProtoDuration;
25
26const SVC: &str = "SIP";
27
28#[derive(Debug)]
29pub struct SIPClient {
30    base: ServiceBase,
31    client: TwirpClient,
32}
33
34#[deprecated]
35#[derive(Default, Clone, Debug)]
36pub struct CreateSIPTrunkOptions {
37    /// Human-readable name for the Trunk.
38    pub name: String,
39    /// Optional free-form metadata.
40    pub metadata: String,
41    /// CIDR or IPs that traffic is accepted from
42    /// An empty list means all inbound traffic is accepted.
43    pub inbound_addresses: Vec<String>,
44    /// Accepted `To` values. This Trunk will only accept a call made to
45    /// these numbers. This allows you to have distinct Trunks for different phone
46    /// numbers at the same provider.
47    pub inbound_numbers: Vec<String>,
48    /// Username and password used to authenticate inbound SIP invites
49    /// May be empty to have no Authentication
50    pub inbound_username: String,
51    pub inbound_password: String,
52
53    /// IP that SIP INVITE is sent too
54    pub outbound_address: String,
55    /// Username and password used to authenticate outbound SIP invites
56    /// May be empty to have no Authentication
57    pub outbound_username: String,
58    pub outbound_password: String,
59}
60
61#[derive(Default, Clone, Debug)]
62pub struct CreateSIPInboundTrunkOptions {
63    /// Optional free-form metadata.
64    pub metadata: Option<String>,
65    /// CIDR or IPs that traffic is accepted from
66    /// An empty list means all inbound traffic is accepted.
67    pub allowed_addresses: Option<Vec<String>>,
68    /// Accepted `To` values. This Trunk will only accept a call made to
69    /// these numbers. This allows you to have distinct Trunks for different phone
70    /// numbers at the same provider.
71    pub allowed_numbers: Option<Vec<String>>,
72    /// Username and password used to authenticate inbound SIP invites
73    /// May be empty to have no Authentication
74    pub auth_username: Option<String>,
75    pub auth_password: Option<String>,
76    pub headers: Option<HashMap<String, String>>,
77    pub headers_to_attributes: Option<HashMap<String, String>>,
78    pub attributes_to_headers: Option<HashMap<String, String>>,
79    pub max_call_duration: Option<Duration>,
80    pub ringing_timeout: Option<Duration>,
81    pub krisp_enabled: Option<bool>,
82    /// Authentication realm advertised on inbound SIP invites.
83    pub auth_realm: Option<String>,
84}
85
86#[derive(Default, Clone, Debug)]
87pub struct CreateSIPOutboundTrunkOptions {
88    pub transport: proto::SipTransport,
89    /// Optional free-form metadata.
90    pub metadata: String,
91    /// Username and password used to authenticate outbound SIP invites
92    /// May be empty to have no Authentication
93    pub auth_username: String,
94    pub auth_password: String,
95
96    pub headers: Option<HashMap<String, String>>,
97    pub headers_to_attributes: Option<HashMap<String, String>>,
98    pub attributes_to_headers: Option<HashMap<String, String>>,
99}
100
101#[deprecated]
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum ListSIPTrunkFilter {
104    All,
105}
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum ListSIPInboundTrunkFilter {
108    All,
109}
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum ListSIPOutboundTrunkFilter {
112    All,
113}
114
115#[derive(Default, Clone, Debug)]
116pub struct CreateSIPDispatchRuleOptions {
117    pub name: String,
118    pub metadata: String,
119    pub attributes: HashMap<String, String>,
120    /// What trunks are accepted for this dispatch rule
121    /// If empty all trunks will match this dispatch rule
122    pub trunk_ids: Vec<String>,
123    pub allowed_numbers: Vec<String>,
124    pub hide_phone_number: bool,
125    /// Room configuration for rooms created by this dispatch rule, including
126    /// agents to dispatch into the room.
127    pub room_config: Option<proto::RoomConfiguration>,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum ListSIPDispatchRuleFilter {
132    All,
133}
134
135#[derive(Default, Clone, Debug)]
136pub struct CreateSIPParticipantOptions {
137    /// Optional identity of the participant in LiveKit room
138    pub participant_identity: String,
139    /// Optionally set the name of the participant in a LiveKit room
140    pub participant_name: Option<String>,
141    /// Optionally set the free-form metadata of the participant in a LiveKit room
142    pub participant_metadata: Option<String>,
143    pub participant_attributes: Option<HashMap<String, String>>,
144    /// Optional custom caller ID shown to the callee. Requires SIP provider
145    /// support. If unset, the phone number is used; set it to an empty string to
146    /// trigger a CNAM lookup on providers that support it.
147    pub display_name: Option<String>,
148    // What number should be dialed via SIP
149    pub sip_number: Option<String>,
150    /// Optionally send following DTMF digits (extension codes) when making a call.
151    /// Character 'w' can be used to add a 0.5 sec delay.
152    pub dtmf: Option<String>,
153    /// Wait for the call to be answered before returning.
154    ///
155    /// When `true`, the request blocks until the call is answered or fails,
156    /// and returns SIP error codes (e.g., 486 Busy, 603 Decline) on failure.
157    /// When `false` (default), returns immediately while the call is still dialing.
158    pub wait_until_answered: Option<bool>,
159    /// Optionally play dialtone in the room as an audible indicator for existing participants
160    pub play_dialtone: Option<bool>,
161    pub hide_phone_number: Option<bool>,
162    pub ringing_timeout: Option<Duration>,
163    pub max_call_duration: Option<Duration>,
164    pub enable_krisp: Option<bool>,
165    /// SIP headers sent as-is on the INVITE; may help the SIP endpoint identify
166    /// the call as coming from LiveKit.
167    pub headers: Option<HashMap<String, String>>,
168    /// Which SIP response headers to map to `sip.h.*` participant attributes.
169    pub include_headers: Option<proto::SipHeaderOptions>,
170    /// Media encryption policy for the call.
171    pub media_encryption: Option<proto::SipMediaEncryption>,
172    /// Per-request timeout override. Defaults to a longer value when
173    /// `wait_until_answered` is set (dialing takes time), otherwise the client
174    /// default. Raised, if needed, to stay above `ringing_timeout`.
175    pub timeout: Option<Duration>,
176}
177
178#[derive(Default, Clone, Debug)]
179pub struct TransferSIPParticipantOptions {
180    /// Optionally play a dialtone to the SIP participant as an audible indicator
181    /// of being transferred.
182    pub play_dialtone: Option<bool>,
183    /// Max time for the transfer destination to answer the call.
184    pub ringing_timeout: Option<Duration>,
185    /// SIP headers added to the REFER SIP request.
186    pub headers: Option<HashMap<String, String>>,
187    /// Per-request timeout override. A transfer always dials (REFER) and blocks
188    /// until the destination answers, so this is raised, if needed, to stay above
189    /// `ringing_timeout`.
190    pub timeout: Option<Duration>,
191}
192
193impl SIPClient {
194    /// Authenticates with an API key and secret, signing a short-lived token per request.
195    pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
196        Self::build(
197            host,
198            ServiceBase::with_api_key(api_key, api_secret),
199            crate::http_client::Client::new(),
200        )
201    }
202
203    /// Authenticates with a pre-signed token, sent verbatim on every request.
204    pub fn with_token(host: &str, token: &str) -> Self {
205        Self::build(host, ServiceBase::with_token(token), crate::http_client::Client::new())
206    }
207
208    /// Builds the client from an already-constructed HTTP client so the unified
209    /// [`LiveKitApi`](super::LiveKitApi) can share one connection pool across services.
210    pub(crate) fn build(host: &str, base: ServiceBase, client: crate::http_client::Client) -> Self {
211        Self { base, client: TwirpClient::with_client(host, LIVEKIT_PACKAGE, None, client) }
212    }
213
214    #[cfg(test)]
215    pub(crate) fn with_default_headers(mut self, headers: http::HeaderMap) -> Self {
216        self.client = self.client.with_default_headers(headers);
217        self
218    }
219
220    /// Reads the API key and secret from the `LIVEKIT_API_KEY` and
221    /// `LIVEKIT_API_SECRET` environment variables.
222    pub fn new(host: &str) -> ServiceResult<Self> {
223        let (api_key, api_secret) = get_env_keys()?;
224        Ok(Self::with_api_key(host, &api_key, &api_secret))
225    }
226
227    /// Enables or disables region failover (enabled by default). Failover only
228    /// engages for LiveKit Cloud hosts.
229    pub fn with_failover(mut self, enabled: bool) -> Self {
230        self.client = self.client.with_failover(enabled);
231        self
232    }
233
234    /// Overrides the default per-request timeout (10s) for calls on this client.
235    /// `create_sip_participant` can still override it per call via
236    /// [`CreateSIPParticipantOptions::timeout`].
237    pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
238        self.client = self.client.with_request_timeout(timeout);
239        self
240    }
241
242    fn duration_to_proto(d: Option<Duration>) -> Option<ProtoDuration> {
243        d.map(|d| ProtoDuration { seconds: d.as_secs() as i64, nanos: d.subsec_nanos() as i32 })
244    }
245
246    pub async fn create_sip_inbound_trunk(
247        &self,
248        name: String,
249        numbers: Vec<String>,
250        options: CreateSIPInboundTrunkOptions,
251    ) -> ServiceResult<proto::SipInboundTrunkInfo> {
252        self.client
253            .request(
254                SVC,
255                "CreateSIPInboundTrunk",
256                proto::CreateSipInboundTrunkRequest {
257                    trunk: Some(proto::SipInboundTrunkInfo {
258                        sip_trunk_id: Default::default(),
259                        name,
260                        numbers,
261                        metadata: options.metadata.unwrap_or_default(),
262                        allowed_numbers: options.allowed_numbers.unwrap_or_default(),
263                        allowed_addresses: options.allowed_addresses.unwrap_or_default(),
264                        auth_username: options.auth_username.unwrap_or_default(),
265                        auth_password: options.auth_password.unwrap_or_default(),
266                        auth_realm: options.auth_realm.unwrap_or_default(),
267                        headers: options.headers.unwrap_or_default(),
268                        headers_to_attributes: options.headers_to_attributes.unwrap_or_default(),
269                        attributes_to_headers: options.attributes_to_headers.unwrap_or_default(),
270                        krisp_enabled: options.krisp_enabled.unwrap_or(false),
271                        max_call_duration: Self::duration_to_proto(options.max_call_duration),
272                        ringing_timeout: Self::duration_to_proto(options.ringing_timeout),
273
274                        // TODO: support these attributes
275                        include_headers: Default::default(),
276                        media_encryption: Default::default(),
277                        created_at: Default::default(),
278                        updated_at: Default::default(),
279                        media: Default::default(),
280                    }),
281                },
282                self.base.auth_header(
283                    Default::default(),
284                    Some(SIPGrants { admin: true, ..Default::default() }),
285                )?,
286            )
287            .await
288            .map_err(Into::into)
289    }
290
291    pub async fn create_sip_outbound_trunk(
292        &self,
293        name: String,
294        address: String,
295        numbers: Vec<String>,
296        options: CreateSIPOutboundTrunkOptions,
297    ) -> ServiceResult<proto::SipOutboundTrunkInfo> {
298        self.client
299            .request(
300                SVC,
301                "CreateSIPOutboundTrunk",
302                proto::CreateSipOutboundTrunkRequest {
303                    trunk: Some(proto::SipOutboundTrunkInfo {
304                        sip_trunk_id: Default::default(),
305                        name,
306                        address,
307                        numbers,
308                        transport: options.transport as i32,
309                        metadata: options.metadata,
310
311                        auth_username: options.auth_username.to_owned(),
312                        auth_password: options.auth_password.to_owned(),
313
314                        headers: options.headers.unwrap_or_default(),
315                        headers_to_attributes: options.headers_to_attributes.unwrap_or_default(),
316                        attributes_to_headers: options.attributes_to_headers.unwrap_or_default(),
317
318                        // TODO: support these attributes
319                        include_headers: Default::default(),
320                        media_encryption: Default::default(),
321                        destination_country: Default::default(),
322                        created_at: Default::default(),
323                        updated_at: Default::default(),
324                        from_host: Default::default(),
325                        media: Default::default(),
326                    }),
327                },
328                self.base.auth_header(
329                    Default::default(),
330                    Some(SIPGrants { admin: true, ..Default::default() }),
331                )?,
332            )
333            .await
334            .map_err(Into::into)
335    }
336
337    /// Updates specific fields of an existing SIP inbound trunk. Only the fields
338    /// set on `update` are changed; everything else is left as-is. Mirrors the
339    /// Python SDK's `update_inbound_trunk_fields` / server-sdk-go's
340    /// `SipInboundTrunkUpdate` action.
341    pub async fn update_sip_inbound_trunk(
342        &self,
343        trunk_id: String,
344        update: proto::SipInboundTrunkUpdate,
345    ) -> ServiceResult<proto::SipInboundTrunkInfo> {
346        self.client
347            .request(
348                SVC,
349                "UpdateSIPInboundTrunk",
350                proto::UpdateSipInboundTrunkRequest {
351                    sip_trunk_id: trunk_id,
352                    action: Some(proto::update_sip_inbound_trunk_request::Action::Update(update)),
353                },
354                self.base.auth_header(
355                    Default::default(),
356                    Some(SIPGrants { admin: true, ..Default::default() }),
357                )?,
358            )
359            .await
360            .map_err(Into::into)
361    }
362
363    /// Updates an existing SIP inbound trunk by replacing it entirely with
364    /// `trunk`. Mirrors the Python SDK's `update_inbound_trunk` / server-sdk-go's
365    /// `SipInboundTrunkInfo` (Replace) action.
366    pub async fn update_sip_inbound_trunk_replace(
367        &self,
368        trunk_id: String,
369        trunk: proto::SipInboundTrunkInfo,
370    ) -> ServiceResult<proto::SipInboundTrunkInfo> {
371        self.client
372            .request(
373                SVC,
374                "UpdateSIPInboundTrunk",
375                proto::UpdateSipInboundTrunkRequest {
376                    sip_trunk_id: trunk_id,
377                    action: Some(proto::update_sip_inbound_trunk_request::Action::Replace(trunk)),
378                },
379                self.base.auth_header(
380                    Default::default(),
381                    Some(SIPGrants { admin: true, ..Default::default() }),
382                )?,
383            )
384            .await
385            .map_err(Into::into)
386    }
387
388    /// Updates specific fields of an existing SIP outbound trunk. Only the fields
389    /// set on `update` are changed; everything else is left as-is.
390    pub async fn update_sip_outbound_trunk(
391        &self,
392        trunk_id: String,
393        update: proto::SipOutboundTrunkUpdate,
394    ) -> ServiceResult<proto::SipOutboundTrunkInfo> {
395        self.client
396            .request(
397                SVC,
398                "UpdateSIPOutboundTrunk",
399                proto::UpdateSipOutboundTrunkRequest {
400                    sip_trunk_id: trunk_id,
401                    action: Some(proto::update_sip_outbound_trunk_request::Action::Update(update)),
402                },
403                self.base.auth_header(
404                    Default::default(),
405                    Some(SIPGrants { admin: true, ..Default::default() }),
406                )?,
407            )
408            .await
409            .map_err(Into::into)
410    }
411
412    /// Updates an existing SIP outbound trunk by replacing it entirely with
413    /// `trunk`.
414    pub async fn update_sip_outbound_trunk_replace(
415        &self,
416        trunk_id: String,
417        trunk: proto::SipOutboundTrunkInfo,
418    ) -> ServiceResult<proto::SipOutboundTrunkInfo> {
419        self.client
420            .request(
421                SVC,
422                "UpdateSIPOutboundTrunk",
423                proto::UpdateSipOutboundTrunkRequest {
424                    sip_trunk_id: trunk_id,
425                    action: Some(proto::update_sip_outbound_trunk_request::Action::Replace(trunk)),
426                },
427                self.base.auth_header(
428                    Default::default(),
429                    Some(SIPGrants { admin: true, ..Default::default() }),
430                )?,
431            )
432            .await
433            .map_err(Into::into)
434    }
435
436    #[deprecated]
437    pub async fn list_sip_trunk(
438        &self,
439        filter: ListSIPTrunkFilter,
440    ) -> ServiceResult<Vec<proto::SipTrunkInfo>> {
441        let resp: proto::ListSipTrunkResponse = self
442            .client
443            .request(
444                SVC,
445                "ListSIPTrunk",
446                proto::ListSipTrunkRequest {
447                    // TODO support these attributes
448                    page: Default::default(),
449                },
450                self.base.auth_header(
451                    Default::default(),
452                    Some(SIPGrants { admin: true, ..Default::default() }),
453                )?,
454            )
455            .await?;
456
457        Ok(resp.items)
458    }
459
460    pub async fn list_sip_inbound_trunk(
461        &self,
462        filter: ListSIPInboundTrunkFilter,
463    ) -> ServiceResult<Vec<proto::SipInboundTrunkInfo>> {
464        let resp: proto::ListSipInboundTrunkResponse = self
465            .client
466            .request(
467                SVC,
468                "ListSIPInboundTrunk",
469                proto::ListSipInboundTrunkRequest {
470                    // TODO: support these attributes
471                    page: Default::default(),
472                    trunk_ids: Default::default(),
473                    numbers: Default::default(),
474                },
475                self.base.auth_header(
476                    Default::default(),
477                    Some(SIPGrants { admin: true, ..Default::default() }),
478                )?,
479            )
480            .await?;
481
482        Ok(resp.items)
483    }
484
485    pub async fn list_sip_outbound_trunk(
486        &self,
487        filter: ListSIPOutboundTrunkFilter,
488    ) -> ServiceResult<Vec<proto::SipOutboundTrunkInfo>> {
489        let resp: proto::ListSipOutboundTrunkResponse = self
490            .client
491            .request(
492                SVC,
493                "ListSIPOutboundTrunk",
494                proto::ListSipOutboundTrunkRequest {
495                    // TODO: support these attributes
496                    page: Default::default(),
497                    trunk_ids: Default::default(),
498                    numbers: Default::default(),
499                },
500                self.base.auth_header(
501                    Default::default(),
502                    Some(SIPGrants { admin: true, ..Default::default() }),
503                )?,
504            )
505            .await?;
506
507        Ok(resp.items)
508    }
509
510    pub async fn delete_sip_trunk(&self, sip_trunk_id: &str) -> ServiceResult<proto::SipTrunkInfo> {
511        self.client
512            .request(
513                SVC,
514                "DeleteSIPTrunk",
515                proto::DeleteSipTrunkRequest { sip_trunk_id: sip_trunk_id.to_owned() },
516                self.base.auth_header(
517                    Default::default(),
518                    Some(SIPGrants { admin: true, ..Default::default() }),
519                )?,
520            )
521            .await
522            .map_err(Into::into)
523    }
524
525    pub async fn create_sip_dispatch_rule(
526        &self,
527        rule: proto::sip_dispatch_rule::Rule,
528        options: CreateSIPDispatchRuleOptions,
529    ) -> ServiceResult<proto::SipDispatchRuleInfo> {
530        self.client
531            .request(
532                SVC,
533                "CreateSIPDispatchRule",
534                proto::CreateSipDispatchRuleRequest {
535                    dispatch_rule: Some(proto::SipDispatchRuleInfo {
536                        rule: Some(proto::SipDispatchRule { rule: Some(rule) }),
537                        name: options.name,
538                        metadata: options.metadata,
539                        attributes: options.attributes,
540                        trunk_ids: options.trunk_ids,
541                        inbound_numbers: options.allowed_numbers,
542                        hide_phone_number: options.hide_phone_number,
543                        room_config: options.room_config,
544                        ..Default::default()
545                    }),
546                    ..Default::default()
547                },
548                self.base.auth_header(
549                    Default::default(),
550                    Some(SIPGrants { admin: true, ..Default::default() }),
551                )?,
552            )
553            .await
554            .map_err(Into::into)
555    }
556
557    /// Updates specific fields of an existing SIP dispatch rule. Only the fields
558    /// set on `update` are changed; everything else is left as-is.
559    pub async fn update_sip_dispatch_rule(
560        &self,
561        dispatch_rule_id: String,
562        update: proto::SipDispatchRuleUpdate,
563    ) -> ServiceResult<proto::SipDispatchRuleInfo> {
564        self.client
565            .request(
566                SVC,
567                "UpdateSIPDispatchRule",
568                proto::UpdateSipDispatchRuleRequest {
569                    sip_dispatch_rule_id: dispatch_rule_id,
570                    action: Some(proto::update_sip_dispatch_rule_request::Action::Update(update)),
571                },
572                self.base.auth_header(
573                    Default::default(),
574                    Some(SIPGrants { admin: true, ..Default::default() }),
575                )?,
576            )
577            .await
578            .map_err(Into::into)
579    }
580
581    /// Updates an existing SIP dispatch rule by replacing it entirely with
582    /// `rule`.
583    pub async fn update_sip_dispatch_rule_replace(
584        &self,
585        dispatch_rule_id: String,
586        rule: proto::SipDispatchRuleInfo,
587    ) -> ServiceResult<proto::SipDispatchRuleInfo> {
588        self.client
589            .request(
590                SVC,
591                "UpdateSIPDispatchRule",
592                proto::UpdateSipDispatchRuleRequest {
593                    sip_dispatch_rule_id: dispatch_rule_id,
594                    action: Some(proto::update_sip_dispatch_rule_request::Action::Replace(rule)),
595                },
596                self.base.auth_header(
597                    Default::default(),
598                    Some(SIPGrants { admin: true, ..Default::default() }),
599                )?,
600            )
601            .await
602            .map_err(Into::into)
603    }
604
605    pub async fn list_sip_dispatch_rule(
606        &self,
607        filter: ListSIPDispatchRuleFilter,
608    ) -> ServiceResult<Vec<proto::SipDispatchRuleInfo>> {
609        let resp: proto::ListSipDispatchRuleResponse = self
610            .client
611            .request(
612                SVC,
613                "ListSIPDispatchRule",
614                proto::ListSipDispatchRuleRequest {
615                    // TODO: support these attributes
616                    page: Default::default(),
617                    dispatch_rule_ids: Default::default(),
618                    trunk_ids: Default::default(),
619                },
620                self.base.auth_header(
621                    Default::default(),
622                    Some(SIPGrants { admin: true, ..Default::default() }),
623                )?,
624            )
625            .await?;
626
627        Ok(resp.items)
628    }
629
630    pub async fn delete_sip_dispatch_rule(
631        &self,
632        sip_dispatch_rule_id: &str,
633    ) -> ServiceResult<proto::SipDispatchRuleInfo> {
634        self.client
635            .request(
636                SVC,
637                "DeleteSIPDispatchRule",
638                proto::DeleteSipDispatchRuleRequest {
639                    sip_dispatch_rule_id: sip_dispatch_rule_id.to_owned(),
640                },
641                self.base.auth_header(
642                    Default::default(),
643                    Some(SIPGrants { admin: true, ..Default::default() }),
644                )?,
645            )
646            .await
647            .map_err(Into::into)
648    }
649
650    pub async fn create_sip_participant(
651        &self,
652        sip_trunk_id: String,
653        call_to: String,
654        room_name: String,
655        options: CreateSIPParticipantOptions,
656        outbound_trunk_config: Option<proto::SipOutboundConfig>,
657    ) -> ServiceResult<proto::SipParticipantInfo> {
658        let wait_until_answered = options.wait_until_answered.unwrap_or(false);
659        let user_timeout = options.timeout;
660        // When waiting for an answer, pin the ring window explicitly so our request
661        // timeout doesn't depend on the server's default (which could change).
662        let ringing_timeout =
663            options.ringing_timeout.or(wait_until_answered.then_some(DEFAULT_RINGING_TIMEOUT));
664        let request = proto::CreateSipParticipantRequest {
665            sip_trunk_id: sip_trunk_id.to_owned(),
666            trunk: outbound_trunk_config,
667            sip_call_to: call_to.to_owned(),
668            sip_number: options.sip_number.to_owned().unwrap_or_default(),
669            room_name: room_name.to_owned(),
670            participant_identity: options.participant_identity.to_owned(),
671            participant_name: options.participant_name.to_owned().unwrap_or_default(),
672            participant_metadata: options.participant_metadata.to_owned().unwrap_or_default(),
673            participant_attributes: options.participant_attributes.to_owned().unwrap_or_default(),
674            display_name: options.display_name.to_owned(),
675            dtmf: options.dtmf.to_owned().unwrap_or_default(),
676            wait_until_answered,
677            play_ringtone: options.play_dialtone.unwrap_or(false),
678            play_dialtone: options.play_dialtone.unwrap_or(false),
679            hide_phone_number: options.hide_phone_number.unwrap_or(false),
680            max_call_duration: Self::duration_to_proto(options.max_call_duration),
681            ringing_timeout: Self::duration_to_proto(ringing_timeout),
682            krisp_enabled: options.enable_krisp.unwrap_or(false),
683            headers: options.headers.unwrap_or_default(),
684            include_headers: options.include_headers.map(|h| h as i32).unwrap_or_default(),
685            media_encryption: options.media_encryption.map(|e| e as i32).unwrap_or_default(),
686            ..Default::default()
687        };
688        let headers = self.base.auth_header(
689            Default::default(),
690            Some(SIPGrants { call: true, ..Default::default() }),
691        )?;
692
693        // A user-specified timeout wins; otherwise waiting for an answer dials a
694        // phone, which takes longer and must outlast ringing. Without waiting the
695        // request returns immediately, so the client default applies.
696        if wait_until_answered {
697            self.client
698                .request_with_timeout(
699                    SVC,
700                    "CreateSIPParticipant",
701                    request,
702                    headers,
703                    dial_timeout(user_timeout, ringing_timeout),
704                )
705                .await
706                .map_err(Into::into)
707        } else if let Some(timeout) = user_timeout {
708            self.client
709                .request_with_timeout(SVC, "CreateSIPParticipant", request, headers, timeout)
710                .await
711                .map_err(Into::into)
712        } else {
713            self.client
714                .request(SVC, "CreateSIPParticipant", request, headers)
715                .await
716                .map_err(Into::into)
717        }
718    }
719
720    /// Transfers a SIP participant to another number via a SIP REFER. This always
721    /// dials the transfer destination and blocks until it answers or fails, so
722    /// the request must outlast the ring window.
723    pub async fn transfer_sip_participant(
724        &self,
725        room_name: String,
726        participant_identity: String,
727        transfer_to: String,
728        options: TransferSIPParticipantOptions,
729    ) -> ServiceResult<()> {
730        // Pin the ring window explicitly so the request timeout doesn't depend on
731        // the server's default (which could change).
732        let ringing_timeout = options.ringing_timeout.or(Some(DEFAULT_RINGING_TIMEOUT));
733        let request = proto::TransferSipParticipantRequest {
734            participant_identity,
735            room_name: room_name.to_owned(),
736            transfer_to,
737            play_dialtone: options.play_dialtone.unwrap_or(false),
738            headers: options.headers.unwrap_or_default(),
739            ringing_timeout: Self::duration_to_proto(ringing_timeout),
740        };
741        let headers = self.base.auth_header(
742            VideoGrants { room_admin: true, room: room_name, ..Default::default() },
743            Some(SIPGrants { call: true, ..Default::default() }),
744        )?;
745
746        self.client
747            .request_with_timeout(
748                SVC,
749                "TransferSIPParticipant",
750                request,
751                headers,
752                dial_timeout(options.timeout, ringing_timeout),
753            )
754            .await
755            .map_err(Into::into)
756    }
757}