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