Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

## Unreleased
- Added support for the `<Refer>` BXML verb (including `<SipUri>` child, `referCompleteUrl`, `referCompleteMethod`, and optional `tag`).
- Added support for the `referComplete` webhook via `ReferCompleteCallback`.
- Added support for `referCallStatus`, optional `referSipResponseCode`, and optional `notifySipResponseCode` fields.

> Note: This change should not be released before https://github.com/Bandwidth/api-specs/pull/2142 is merged.
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,33 @@ namespace Example
}
```

## BXML `<Refer>` example

```csharp
using Bandwidth.Standard.Model.Bxml;
using Bandwidth.Standard.Model.Bxml.Verbs;

var refer = new Refer()
.WithSipUri("sip:alice@atlanta.example.com")
.WithReferCompleteUrl("https://example.com/handleRefer")
.WithReferCompleteMethod("POST")
.WithTag("handoff-123");

var bxml = new Response(refer).ToBXML();
```

When a REFER succeeds, the remote SIP endpoint redirects away from Bandwidth and the call is terminated. Do not expect post-success BXML execution on that call leg.

## `referComplete` failure recovery example

```csharp
// Example callback handler pseudocode
if (callback.ReferCallStatus == ReferCallStatusEnum.Failure)
{
// Recover only on failure (e.g. fallback flow, retry, alternate route)
}
```

<a id="documentation-for-api-endpoints"></a>
## Documentation for API Endpoints

Expand Down Expand Up @@ -328,6 +355,8 @@ Class | Method | HTTP request | Description
- [Model.RecordingStateEnum](docs/RecordingStateEnum.md)
- [Model.RecordingTranscriptionMetadata](docs/RecordingTranscriptionMetadata.md)
- [Model.RecordingTranscriptions](docs/RecordingTranscriptions.md)
- [Model.ReferCallStatusEnum](docs/ReferCallStatusEnum.md)
- [Model.ReferCompleteCallback](docs/ReferCompleteCallback.md)
- [Model.RedirectCallback](docs/RedirectCallback.md)
- [Model.RedirectMethodEnum](docs/RedirectMethodEnum.md)
- [Model.SipConnectionMetadata](docs/SipConnectionMetadata.md)
Expand Down Expand Up @@ -391,4 +420,3 @@ Authentication schemes defined for the API:
- **Flow**: application
- **Authorization URL**: https://api.bandwidth.com/api/v1/oauth2/token
- **Scopes**: N/A

8 changes: 8 additions & 0 deletions docs/ReferCallStatusEnum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Bandwidth.Standard.Model.ReferCallStatusEnum

## Enum

* `Success` (value: `success`)
* `Failure` (value: `failure`)

[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
24 changes: 24 additions & 0 deletions docs/ReferCompleteCallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Bandwidth.Standard.Model.ReferCompleteCallback
This event is sent to the referCompleteUrl of the A-leg's <Refer> verb when the REFER attempt completes.

## Properties

Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**EventType** | **string** | The event type. Always `referComplete`. | [optional]
**EventTime** | **DateTime** | The approximate UTC date and time when the event was generated by the Bandwidth server. | [optional]
**AccountId** | **string** | The user account associated with the call. | [optional]
**ApplicationId** | **string** | The id of the application associated with the call. | [optional]
**From** | **string** | The provided identifier of the caller. | [optional]
**To** | **string** | The phone number that received the call. | [optional]
**Direction** | **CallDirectionEnum** | Call direction (`inbound`). | [optional]
**CallId** | **string** | The call id associated with the event. | [optional]
**CallUrl** | **string** | The URL of the call associated with the event. | [optional]
**StartTime** | **DateTime** | Time the call was started. | [optional]
**AnswerTime** | **DateTime** | Time the call was answered. | [optional]
**ReferCallStatus** | **ReferCallStatusEnum** | Whether REFER succeeded or failed. | [optional]
**Tag** | **string** | Optional custom string included in callbacks. | [optional]
**ReferSipResponseCode** | **int?** | Optional SIP response code from the REFER transaction. | [optional]
**NotifySipResponseCode** | **int?** | Optional SIP response code from the NOTIFY transaction. | [optional]

[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
46 changes: 46 additions & 0 deletions src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.IO;
using System.Xml.Serialization;
using Bandwidth.Standard.Model.Bxml;
using Bandwidth.Standard.Model.Bxml.Verbs;
using Xunit;

namespace Bandwidth.Standard.Test.Unit.Model.Bxml
{
public class TestRefer
{
[Fact]
public void ReferRoundTripTest()
{
var expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Response> <Refer referCompleteUrl=\"https://example.com/handleRefer\" referCompleteMethod=\"POST\" tag=\"refer-tag\"> <SipUri>sip:alice@atlanta.example.com</SipUri> </Refer></Response>";

var refer = new Refer()
.WithSipUri("sip:alice@atlanta.example.com")
.WithReferCompleteUrl("https://example.com/handleRefer")
.WithReferCompleteMethod("POST")
.WithTag("refer-tag");

var actual = new Response(refer).ToBXML();
Assert.Equal(expected, actual.Replace("\n", "").Replace("\r", ""));

const string referOnlyXml = "<Refer referCompleteUrl=\"https://example.com/handleRefer\" referCompleteMethod=\"POST\" tag=\"refer-tag\"><SipUri>sip:alice@atlanta.example.com</SipUri></Refer>";
var serializer = new XmlSerializer(typeof(Refer), "");
Refer deserializedRefer;
using (var reader = new StringReader(referOnlyXml))
{
deserializedRefer = (Refer)serializer.Deserialize(reader);
}

Assert.Equal("sip:alice@atlanta.example.com", deserializedRefer.SipUriElement.Uri);
var roundTrip = new Response(deserializedRefer).ToBXML();
Assert.Equal(expected, roundTrip.Replace("\n", "").Replace("\r", ""));
}

[Fact]
public void ReferSipUriMustStartWithSipScheme()
{
var refer = new Refer();
Assert.Throws<ArgumentException>(() => refer.WithSipUri("tel:+15551234567"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Bandwidth.Standard.Model;
using Newtonsoft.Json;
using Xunit;

namespace Bandwidth.Standard.Test.Unit.Model
{
public class ReferCompleteCallbackTests
{
[Fact]
public void ReferCompleteSuccessTest()
{
var callback = Deserialize("{\"eventType\":\"referComplete\",\"eventTime\":\"2024-01-01T00:00:00.000Z\",\"accountId\":\"9900000\",\"applicationId\":\"04e88489-df02-4e34-a0ee-27a91849555f\",\"from\":\"+15555550100\",\"to\":\"+15555550101\",\"direction\":\"inbound\",\"callId\":\"c-123\",\"callUrl\":\"https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-123\",\"startTime\":\"2024-01-01T00:00:00.000Z\",\"answerTime\":\"2024-01-01T00:00:01.000Z\",\"referCallStatus\":\"success\"}");

Assert.Equal(ReferCallStatusEnum.Success, callback.ReferCallStatus);
Assert.Null(callback.ReferSipResponseCode);
Assert.Null(callback.NotifySipResponseCode);
}

[Fact]
public void ReferCompleteReferRejectedTest()
{
var callback = Deserialize("{\"eventType\":\"referComplete\",\"eventTime\":\"2024-01-01T00:00:00.000Z\",\"accountId\":\"9900000\",\"applicationId\":\"04e88489-df02-4e34-a0ee-27a91849555f\",\"from\":\"+15555550100\",\"to\":\"+15555550101\",\"direction\":\"inbound\",\"callId\":\"c-123\",\"callUrl\":\"https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-123\",\"startTime\":\"2024-01-01T00:00:00.000Z\",\"answerTime\":\"2024-01-01T00:00:01.000Z\",\"referCallStatus\":\"failure\",\"referSipResponseCode\":405}");

Assert.Equal(ReferCallStatusEnum.Failure, callback.ReferCallStatus);
Assert.Equal(405, callback.ReferSipResponseCode);
Assert.Null(callback.NotifySipResponseCode);
}

[Fact]
public void ReferCompleteDestinationUnreachableTest()
{
var callback = Deserialize("{\"eventType\":\"referComplete\",\"eventTime\":\"2024-01-01T00:00:00.000Z\",\"accountId\":\"9900000\",\"applicationId\":\"04e88489-df02-4e34-a0ee-27a91849555f\",\"from\":\"+15555550100\",\"to\":\"+15555550101\",\"direction\":\"inbound\",\"callId\":\"c-123\",\"callUrl\":\"https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-123\",\"startTime\":\"2024-01-01T00:00:00.000Z\",\"answerTime\":\"2024-01-01T00:00:01.000Z\",\"referCallStatus\":\"failure\",\"referSipResponseCode\":202,\"notifySipResponseCode\":486}");

Assert.Equal(ReferCallStatusEnum.Failure, callback.ReferCallStatus);
Assert.Equal(202, callback.ReferSipResponseCode);
Assert.Equal(486, callback.NotifySipResponseCode);
}

[Fact]
public void ReferCompleteNotifyTimeoutTest()
{
var callback = Deserialize("{\"eventType\":\"referComplete\",\"eventTime\":\"2024-01-01T00:00:00.000Z\",\"accountId\":\"9900000\",\"applicationId\":\"04e88489-df02-4e34-a0ee-27a91849555f\",\"from\":\"+15555550100\",\"to\":\"+15555550101\",\"direction\":\"inbound\",\"callId\":\"c-123\",\"callUrl\":\"https://voice.bandwidth.com/api/v2/accounts/9900000/calls/c-123\",\"startTime\":\"2024-01-01T00:00:00.000Z\",\"answerTime\":\"2024-01-01T00:00:01.000Z\",\"referCallStatus\":\"failure\",\"referSipResponseCode\":202}");

Assert.Equal(ReferCallStatusEnum.Failure, callback.ReferCallStatus);
Assert.Equal(202, callback.ReferSipResponseCode);
Assert.Null(callback.NotifySipResponseCode);
}

private static ReferCompleteCallback Deserialize(string json)
{
var callback = JsonConvert.DeserializeObject<ReferCompleteCallback>(json);
Assert.NotNull(callback);
Assert.Equal("referComplete", callback.EventType);
Assert.Equal(CallDirectionEnum.Inbound, callback.Direction);
return callback;
}
}
}
130 changes: 130 additions & 0 deletions src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using Bandwidth.Standard.Model.Bxml;
using System;
using System.Xml.Serialization;

namespace Bandwidth.Standard.Model.Bxml.Verbs
{
/// <summary>
/// The Refer verb is used to hand off a call to a SIP endpoint.
/// <para><seealso href="https://dev.bandwidth.com/docs/voice/bxml/refer.html"/></para>
/// </summary>
public class Refer : IVerb
{
private string _referCompleteMethod;

/// <summary>
/// URL to receive the refer complete callback.
/// </summary>
[XmlAttribute("referCompleteUrl")]
public string ReferCompleteUrl { get; set; }

/// <summary>
/// HTTP method to send the refer complete callback.
/// </summary>
[XmlAttribute("referCompleteMethod")]
public string ReferCompleteMethod
{
get { return _referCompleteMethod; }
set
{
if (value != null && value != "GET" && value != "POST")
{
throw new ArgumentException("ReferCompleteMethod must be either 'GET' or 'POST'.");
}

_referCompleteMethod = value;
}
}

/// <summary>
/// Optional custom string to include in callbacks.
/// </summary>
[XmlAttribute("tag")]
public string Tag { get; set; }

/// <summary>
/// SIP URI destination for the REFER.
/// </summary>
[XmlElement("SipUri")]
public SipUri SipUriElement { get; set; }

/// <summary>
/// Initializes a new instance of the Refer class.
/// </summary>
public Refer()
{
ReferCompleteMethod = "POST";
}

/// <summary>
/// Sets the SIP URI destination.
/// </summary>
public Refer WithSipUri(string sipUri)
{
SipUriElement = new SipUri { Uri = sipUri };
return this;
}

/// <summary>
/// Sets the SIP URI destination.
/// </summary>
public Refer WithSipUri(SipUri sipUri)
{
SipUriElement = sipUri;
return this;
}

/// <summary>
/// Sets referCompleteUrl.
/// </summary>
public Refer WithReferCompleteUrl(string referCompleteUrl)
{
ReferCompleteUrl = referCompleteUrl;
return this;
}

/// <summary>
/// Sets referCompleteMethod.
/// </summary>
public Refer WithReferCompleteMethod(string referCompleteMethod)
{
ReferCompleteMethod = referCompleteMethod;
return this;
}

/// <summary>
/// Sets tag.
/// </summary>
public Refer WithTag(string tag)
{
Tag = tag;
return this;
}

/// <summary>
/// BXML tag to represent a SIP URI for the refer verb.
/// </summary>
public class SipUri : IVerb
{
private string _uri;

/// <summary>
/// SIP URI to refer the call to (must start with sip:).
/// </summary>
[XmlText]
public string Uri
{
get { return _uri; }
set
{
if (value != null && !value.StartsWith("sip:", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("SipUri must start with 'sip:'.");
}

_uri = value;
}
}
}
}
}
25 changes: 25 additions & 0 deletions src/Bandwidth.Standard/Model/ReferCallStatusEnum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Bandwidth.Standard.Model
{
/// <summary>
/// Result of a refer attempt.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum ReferCallStatusEnum
{
/// <summary>
/// Enum Success for value: success
/// </summary>
[EnumMember(Value = "success")]
Success = 1,

/// <summary>
/// Enum Failure for value: failure
/// </summary>
[EnumMember(Value = "failure")]
Failure = 2
}
}
Loading
Loading