diff --git a/.editorconfig b/.editorconfig index 9d52482..a2a4e25 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,7 +24,7 @@ dotnet_sort_system_directives_first = true dotnet_separate_import_directive_groups = false # this. preferences -dotnet_style_qualification_for_field = true +dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_property = false:silent dotnet_style_qualification_for_method = false:silent dotnet_style_qualification_for_event = false:silent diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c23cb6..4f7aca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.8.0] - 2024-01-08 +### Added +- Implementing OTP Request and verify + ## [2.7.0] - 2023-06-26 ### Added - Whatsapp Multi-Product Template Messages diff --git a/CM.Text.Tests/CM.Text.Tests.csproj b/CM.Text.Tests/CM.Text.Tests.csproj index 218649b..76e45a1 100644 --- a/CM.Text.Tests/CM.Text.Tests.csproj +++ b/CM.Text.Tests/CM.Text.Tests.csproj @@ -9,11 +9,14 @@ - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/CM.Text/BusinessMessaging/CarouselBuilder.cs b/CM.Text/BusinessMessaging/CarouselBuilder.cs index 5c43d3c..5431e1f 100644 --- a/CM.Text/BusinessMessaging/CarouselBuilder.cs +++ b/CM.Text/BusinessMessaging/CarouselBuilder.cs @@ -19,7 +19,7 @@ public class CarouselBuilder /// public CarouselBuilder(CarouselCardWidth carouselCardWidth) { - this._carouselCardWidth = carouselCardWidth; + _carouselCardWidth = carouselCardWidth; } /// @@ -29,7 +29,7 @@ public CarouselBuilder(CarouselCardWidth carouselCardWidth) /// public CarouselBuilder AddCard(RichCard card) { - this._cards.Add(card); + _cards.Add(card); return this; } @@ -41,7 +41,7 @@ public CarouselMessage Build() { return new CarouselMessage { - Carousel = new Carousel {Cards = this._cards.ToArray(), CarouselCardWidth = this._carouselCardWidth} + Carousel = new Carousel {Cards = _cards.ToArray(), CarouselCardWidth = _carouselCardWidth} }; } } diff --git a/CM.Text/BusinessMessaging/MessageBuilder.cs b/CM.Text/BusinessMessaging/MessageBuilder.cs index e3b6839..9a96f17 100644 --- a/CM.Text/BusinessMessaging/MessageBuilder.cs +++ b/CM.Text/BusinessMessaging/MessageBuilder.cs @@ -24,7 +24,7 @@ public class MessageBuilder /// public MessageBuilder(string messageText, string from, params string[] to) { - this._message = new Message + _message = new Message { Body = new Body { @@ -44,8 +44,8 @@ public MessageBuilder(string messageText, string from, params string[] to) /// public Message Build() { - this._message.RichContent = this._richContent; - return this._message; + _message.RichContent = _richContent; + return _message; } /// @@ -59,7 +59,7 @@ public Message Build() /// public MessageBuilder WithAllowedChannels(params Channel[] channels) { - this._message.AllowedChannels = channels; + _message.AllowedChannels = channels; return this; } @@ -70,7 +70,7 @@ public MessageBuilder WithAllowedChannels(params Channel[] channels) /// public MessageBuilder WithReference(string reference) { - this._message.Reference = reference; + _message.Reference = reference; return this; } @@ -97,7 +97,7 @@ public MessageBuilder WithReference(string reference) /// public MessageBuilder WithValidityPeriod(string period) { - this._message.Validity = period; + _message.Validity = period; return this; } @@ -110,10 +110,10 @@ public MessageBuilder WithValidityPeriod(string period) /// public MessageBuilder WithRichMessage(IRichMessage richMessage) { - if (this._richContent == null) - this._richContent = new RichContent(); + if (_richContent == null) + _richContent = new RichContent(); - this._richContent.AddConversationPart(richMessage); + _richContent.AddConversationPart(richMessage); return this; } @@ -125,10 +125,10 @@ public MessageBuilder WithRichMessage(IRichMessage richMessage) /// public MessageBuilder WithSuggestions(params SuggestionBase[] suggestions) { - if (this._richContent == null) - this._richContent = new RichContent(); + if (_richContent == null) + _richContent = new RichContent(); - this._richContent.Suggestions = suggestions; + _richContent.Suggestions = suggestions; return this; } @@ -138,7 +138,7 @@ public MessageBuilder WithSuggestions(params SuggestionBase[] suggestions) /// public MessageBuilder WitHybridAppKey(Guid appKey) { - this._message.HybridAppKey = appKey; + _message.HybridAppKey = appKey; return this; } @@ -150,10 +150,10 @@ public MessageBuilder WitHybridAppKey(Guid appKey) /// public MessageBuilder WithTemplate(TemplateMessage template) { - if (this._richContent == null) - this._richContent = new RichContent(); + if (_richContent == null) + _richContent = new RichContent(); - this._richContent.AddConversationPart(template); + _richContent.AddConversationPart(template); return this; } @@ -164,10 +164,10 @@ public MessageBuilder WithTemplate(TemplateMessage template) /// public MessageBuilder WithInteractive(WhatsAppInteractiveMessage interactive) { - if (this._richContent == null) - this._richContent = new RichContent(); + if (_richContent == null) + _richContent = new RichContent(); - this._richContent.AddConversationPart(interactive); + _richContent.AddConversationPart(interactive); return this; } @@ -178,10 +178,10 @@ public MessageBuilder WithInteractive(WhatsAppInteractiveMessage interactive) /// public MessageBuilder WithApplePay(ApplePayRequest applePayRequest) { - if (this._richContent == null) - this._richContent = new RichContent(); + if (_richContent == null) + _richContent = new RichContent(); - this._richContent.AddConversationPart(applePayRequest); + _richContent.AddConversationPart(applePayRequest); return this; } } diff --git a/CM.Text/BusinessMessaging/Model/MultiChannel/MediaContent.cs b/CM.Text/BusinessMessaging/Model/MultiChannel/MediaContent.cs index 8a43784..599c54d 100644 --- a/CM.Text/BusinessMessaging/Model/MultiChannel/MediaContent.cs +++ b/CM.Text/BusinessMessaging/Model/MultiChannel/MediaContent.cs @@ -24,9 +24,9 @@ public MediaContent() /// public MediaContent(string mediaName, string mediaUri, string mimeType) { - this.MediaName = mediaName; - this.MediaUri = mediaUri; - this.MimeType = mimeType; + MediaName = mediaName; + MediaUri = mediaUri; + MimeType = mimeType; } /// diff --git a/CM.Text/BusinessMessaging/Model/MultiChannel/MediaMessage.cs b/CM.Text/BusinessMessaging/Model/MultiChannel/MediaMessage.cs index 6a14e04..b4aed6d 100644 --- a/CM.Text/BusinessMessaging/Model/MultiChannel/MediaMessage.cs +++ b/CM.Text/BusinessMessaging/Model/MultiChannel/MediaMessage.cs @@ -25,7 +25,7 @@ public MediaMessage() /// public MediaMessage(string mediaName, string mediaUri, string mimeType) { - this.Media = new MediaContent(mediaName, mediaUri, mimeType); + Media = new MediaContent(mediaName, mediaUri, mimeType); } /// diff --git a/CM.Text/BusinessMessaging/Model/MultiChannel/RichContent.cs b/CM.Text/BusinessMessaging/Model/MultiChannel/RichContent.cs index 3018429..01b6ff9 100644 --- a/CM.Text/BusinessMessaging/Model/MultiChannel/RichContent.cs +++ b/CM.Text/BusinessMessaging/Model/MultiChannel/RichContent.cs @@ -16,8 +16,8 @@ public class RichContent /// public RichContent() { - this.Conversation = null; - this.Suggestions = null; + Conversation = null; + Suggestions = null; } /// @@ -38,14 +38,14 @@ public RichContent() /// public void AddConversationPart(IRichMessage part) { - if (this.Conversation == null) - this.Conversation = new[] {part}; + if (Conversation == null) + Conversation = new[] {part}; else { - var newArr = this.Conversation; - Array.Resize(ref newArr, this.Conversation.Length + 1); + var newArr = Conversation; + Array.Resize(ref newArr, Conversation.Length + 1); newArr[newArr.Length - 1] = part; - this.Conversation = newArr; + Conversation = newArr; } } @@ -55,14 +55,14 @@ public void AddConversationPart(IRichMessage part) /// public void AddSuggestion(SuggestionBase suggestion) { - if (this.Suggestions == null) - this.Suggestions = new[] {suggestion}; + if (Suggestions == null) + Suggestions = new[] {suggestion}; else { - var newArr = this.Suggestions; - Array.Resize(ref newArr, this.Suggestions.Length + 1); + var newArr = Suggestions; + Array.Resize(ref newArr, Suggestions.Length + 1); newArr[newArr.Length - 1] = suggestion; - this.Suggestions = newArr; + Suggestions = newArr; } } } diff --git a/CM.Text/BusinessMessaging/Model/MultiChannel/TextMessage.cs b/CM.Text/BusinessMessaging/Model/MultiChannel/TextMessage.cs index 2490384..5287a68 100644 --- a/CM.Text/BusinessMessaging/Model/MultiChannel/TextMessage.cs +++ b/CM.Text/BusinessMessaging/Model/MultiChannel/TextMessage.cs @@ -23,7 +23,7 @@ public TextMessage() /// public TextMessage(string text) { - this.Text = text; + Text = text; } /// diff --git a/CM.Text/CM.Text.csproj b/CM.Text/CM.Text.csproj index 91933e3..ce4b5ac 100644 --- a/CM.Text/CM.Text.csproj +++ b/CM.Text/CM.Text.csproj @@ -13,12 +13,12 @@ LICENSE icon.png $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../CHANGELOG.md")) - 2.7.0 + 2.8.0 https://github.com/cmdotcom/text-sdk-dotnet en true - 2.7.0 - 2.7.0 + 2.8.0 + 2.8.0 True @@ -63,7 +63,7 @@ - + diff --git a/CM.Text/Common/Constant.cs b/CM.Text/Common/Constant.cs index 14e53d2..4800299 100644 --- a/CM.Text/Common/Constant.cs +++ b/CM.Text/Common/Constant.cs @@ -5,6 +5,10 @@ internal static class Constant internal static readonly string TextSdkReference = $"text-sdk-dotnet-{typeof(TextClient).Assembly.GetName().Version}"; internal const string BusinessMessagingGatewayJsonEndpoint = "https://gw.cmtelecom.com/v1.0/message"; + + internal const string OtpRequestEndpoint = "https://api.cm.com/otp/v2/otp"; + internal const string OtpVerifyEndpointFormatter = "https://api.cm.com/otp/v2/otp/{0}/verify"; + internal static readonly string BusinessMessagingGatewayMediaTypeJson = "application/json"; internal static readonly string BusinessMessagingBodyTypeAuto = "AUTO"; internal static readonly int BusinessMessagingMessagePartsMinDefault = 1; diff --git a/CM.Text/Identity/OtpRequest.cs b/CM.Text/Identity/OtpRequest.cs new file mode 100644 index 0000000..14c8b05 --- /dev/null +++ b/CM.Text/Identity/OtpRequest.cs @@ -0,0 +1,83 @@ +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace CM.Text.Identity +{ + /// + /// A request to send an OTP towards an end-user. + /// + [PublicAPI] + public class OtpRequest + { + /// + /// Required: This is the sender name. + /// The maximum length is 11 alphanumerical characters or 16 digits. Example: 'MyCompany' + /// + [JsonPropertyName("from")] + public string From { get; set; } + + /// + /// Required: The destination mobile numbers. + /// This value should be in international format. + /// A single mobile number per request. Example: '00447911123456' + /// + [JsonPropertyName("to")] + public string To { get; set; } + + /// + /// The length of the code (min 4, max 10). default: 5. + /// + [JsonPropertyName("digits")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Digits { get; set; } + + /// + /// The expiry in seconds (min 10, max 3600). default: 60 seconds. + /// + [JsonPropertyName("expiry")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Expiry { get; set; } + + /// + /// The channel to send the code. + /// Supported values: auto, sms, push, whatsapp, voice, email. + /// Channel auto is only available with a SOLiD subscription. + /// + [JsonPropertyName("channel")] + public string Channel { get; set; } = "sms"; + + /// + /// The locale, for WhatsApp supported values: en, nl, fr, de, it, es. + /// Default: en + /// + /// For Voice: the spoken language in the voice call, + /// supported values: de-DE, en-AU, en-GB, en-IN, en-US, es-ES, fr-CA, fr-FR, it-IT, ja-JP, nl-NL + /// Default: en-GB. + /// + /// For Email: The locale for the email template. + /// + [JsonPropertyName("locale")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [CanBeNull] + public string Locale { get; set; } + + /// + /// The app key, when is 'push' + /// + [JsonPropertyName("pushAppKey")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [CanBeNull] + public string PushAppKey { get; set; } + + /// + /// For WhatsApp, set a custom message. You can use the placeholder {code}, this will be replaced by the actual code. + /// Example: Your code is: {code}. This is only used as a fallback in case the message could not be delivered via WhatsApp. + /// + /// For email, Set a custom message to be used in the email message. Do not include the {code} placeholder. + /// + [JsonPropertyName("message")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [CanBeNull] + public string Message { get; set; } + } +} diff --git a/CM.Text/Identity/OtpRequestBuilder.cs b/CM.Text/Identity/OtpRequestBuilder.cs new file mode 100644 index 0000000..b15a16d --- /dev/null +++ b/CM.Text/Identity/OtpRequestBuilder.cs @@ -0,0 +1,105 @@ + +using JetBrains.Annotations; + +namespace CM.Text.Identity +{ + /// + /// Builder class to construct messages + /// + [PublicAPI] + public class OtpRequestBuilder + { + private readonly OtpRequest _otpRequest; + + /// + /// Creates a new OtpRequestBuilder + /// + /// + /// + public OtpRequestBuilder(string from, string to) + { + _otpRequest = new OtpRequest { From = from, To = to }; + } + + /// + /// Constructs the request. + /// + /// + public OtpRequest Build() + { + return _otpRequest; + } + + /// + /// Set the channel + /// + public OtpRequestBuilder WithChannel(string channel) + { + _otpRequest.Channel = channel; + return this; + } + + /// + /// Sets The length of the code (min 4, max 10). default: 5. + /// + /// + /// + public OtpRequestBuilder WithDigits(int digits) + { + _otpRequest.Digits = digits; + return this; + } + + /// + /// The expiry in seconds (min 10, max 3600). default: 60 seconds. + /// + public OtpRequestBuilder WithExpiry(int expiryInSeconds) + { + _otpRequest.Expiry = expiryInSeconds; + return this; + } + + /// + /// The locale, for WhatsApp supported values: en, nl, fr, de, it, es. + /// Default: en + /// + /// For Voice: the spoken language in the voice call, + /// supported values: de-DE, en-AU, en-GB, en-IN, en-US, es-ES, fr-CA, fr-FR, it-IT, ja-JP, nl-NL + /// Default: en-GB. + /// + /// For Email: The locale for the email template. + /// + /// + /// + public OtpRequestBuilder WithLocale(string locale) + { + _otpRequest.Locale = locale; + return this; + } + + /// + /// The app key, when the channel is 'push' + /// + /// + /// + public OtpRequestBuilder WithPushAppKey(string pushAppKey) + { + _otpRequest.PushAppKey = pushAppKey; + return this; + } + + /// + /// For WhatsApp, set a custom message. You can use the placeholder {code}, this will be replaced by the actual code. + /// Example: Your code is: {code}. This is only used as a fallback in case the message could not be delivered via WhatsApp. + /// + /// For email, Set a custom message to be used in the email message. Do not include the {code} placeholder. + /// + /// + /// + public OtpRequestBuilder WithMessage(string message) + { + _otpRequest.Message = message; + return this; + } + } +} diff --git a/CM.Text/Identity/OtpResult.cs b/CM.Text/Identity/OtpResult.cs new file mode 100644 index 0000000..966408d --- /dev/null +++ b/CM.Text/Identity/OtpResult.cs @@ -0,0 +1,37 @@ +using System; +using System.Text.Json.Serialization; + +namespace CM.Text.Identity +{ + /// + /// The result of an OTP request. + /// + public class OtpResult + { + /// + /// The identifier of the OTP. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + /// + /// The channel used to send the code. + /// + [JsonPropertyName("channel")] + public string Channel { get; set; } + /// + /// Indicates if the code was valid. + /// + [JsonPropertyName("verified")] + public bool Verified { get; set; } + /// + /// The date the OTP was created. + /// + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } + /// + /// The date the OTP will expire. + /// + [JsonPropertyName("expiresAt")] + public DateTime ExpiresAt { get; set; } + } +} diff --git a/CM.Text/TextClient.cs b/CM.Text/TextClient.cs index a3afb47..d7d549c 100644 --- a/CM.Text/TextClient.cs +++ b/CM.Text/TextClient.cs @@ -3,11 +3,13 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using CM.Text.BusinessMessaging; using CM.Text.BusinessMessaging.Model; using CM.Text.Common; +using CM.Text.Identity; using CM.Text.Interfaces; using JetBrains.Annotations; @@ -54,9 +56,9 @@ public TextClient(Guid apiKey, [CanBeNull] HttpClient httpClient): this(apiKey, [PublicAPI] public TextClient(Guid apiKey, [CanBeNull] HttpClient httpClient, [CanBeNull] Uri endPointOverride) { - this._apiKey = apiKey; - this._httpClient = httpClient ?? ClientSingletonLazy.Value; - this._endPointOverride = endPointOverride; + _apiKey = apiKey; + _httpClient = httpClient ?? ClientSingletonLazy.Value; + _endPointOverride = endPointOverride; } /// @@ -70,16 +72,16 @@ public async Task SendMessageAsync( { using (var request = new HttpRequestMessage( HttpMethod.Post, - this._endPointOverride ?? new Uri(Constant.BusinessMessagingGatewayJsonEndpoint) + _endPointOverride ?? new Uri(Constant.BusinessMessagingGatewayJsonEndpoint) )) { request.Content = new StringContent( - BusinessMessagingApi.GetHttpPostBody(this._apiKey, messageText, from, to, reference), + BusinessMessagingApi.GetHttpPostBody(_apiKey, messageText, from, to, reference), Encoding.UTF8, Constant.BusinessMessagingGatewayMediaTypeJson ); - using (var requestResult = await this._httpClient.SendAsync(request, cancellationToken) + using (var requestResult = await _httpClient.SendAsync(request, cancellationToken) .ConfigureAwait(false)) { cancellationToken.ThrowIfCancellationRequested(); @@ -105,16 +107,16 @@ public async Task SendMessageAsync( { using (var request = new HttpRequestMessage( HttpMethod.Post, - this._endPointOverride ?? new Uri(Constant.BusinessMessagingGatewayJsonEndpoint) + _endPointOverride ?? new Uri(Constant.BusinessMessagingGatewayJsonEndpoint) )) { request.Content = new StringContent( - BusinessMessagingApi.GetHttpPostBody(this._apiKey, message), + BusinessMessagingApi.GetHttpPostBody(_apiKey, message), Encoding.UTF8, Constant.BusinessMessagingGatewayMediaTypeJson ); - using (var requestResult = await this._httpClient.SendAsync(request, cancellationToken) + using (var requestResult = await _httpClient.SendAsync(request, cancellationToken) .ConfigureAwait(false)) { cancellationToken.ThrowIfCancellationRequested(); @@ -126,5 +128,75 @@ await requestResult.Content.ReadAsStringAsync() } } } + + /// + /// Sends an One Time Password asynchronously. + /// + /// The otp to send. + /// The cancellation token. + /// + [PublicAPI] + public async Task SendOtpAsync( + OtpRequest otpRequest, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var request = new HttpRequestMessage( + HttpMethod.Post, + _endPointOverride ?? new Uri(Constant.OtpRequestEndpoint) + )) + { + request.Content = new StringContent( + JsonSerializer.Serialize(otpRequest), + Encoding.UTF8, + Constant.BusinessMessagingGatewayMediaTypeJson + ); + + return await SendOtpApiRequestAsync(request, cancellationToken); + } + } + + /// + /// Checks an One Time Password asynchronously. + /// + /// id of the OTP to check. + /// The code the end user used + /// The cancellation token. + /// + [PublicAPI] + public async Task VerifyOtpAsync( + string id, + string code, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var request = new HttpRequestMessage( + HttpMethod.Post, + _endPointOverride ?? new Uri(string.Format(Constant.OtpVerifyEndpointFormatter, id)) + )) + { + request.Content = new StringContent( + JsonSerializer.Serialize(new { code = code } ), + Encoding.UTF8, + Constant.BusinessMessagingGatewayMediaTypeJson + ); + + return await SendOtpApiRequestAsync(request, cancellationToken); + } + } + + private async Task SendOtpApiRequestAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + request.Headers.Add("X-CM-ProductToken", _apiKey.ToString()); + using (var requestResult = await _httpClient.SendAsync(request, cancellationToken) + .ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + + return JsonSerializer.Deserialize( + await requestResult.Content.ReadAsStringAsync() + .ConfigureAwait(false) + ); + } + } } } diff --git a/CM.Text/TextClientFactory.cs b/CM.Text/TextClientFactory.cs index 028c429..82ec0d8 100644 --- a/CM.Text/TextClientFactory.cs +++ b/CM.Text/TextClientFactory.cs @@ -35,14 +35,14 @@ public class TextClientFactory : ITextClientFactory /// (Optional) The end point to use, instead of the default "https://gw.cmtelecom.com/v1.0/message". public TextClientFactory(HttpClient httpClient, Uri endPointOverride = null) { - this._httpClient = httpClient; - this._endPointOverride = endPointOverride; + _httpClient = httpClient; + _endPointOverride = endPointOverride; } /// public ITextClient GetClient(Guid productToken) { - return new TextClient(productToken, this._httpClient, this._endPointOverride); + return new TextClient(productToken, _httpClient, _endPointOverride); } } } diff --git a/CM.Text/[JetBrains.Annotations]/JetBrains.Annotations.cs b/CM.Text/[JetBrains.Annotations]/JetBrains.Annotations.cs index 9b9858e..90159d9 100644 --- a/CM.Text/[JetBrains.Annotations]/JetBrains.Annotations.cs +++ b/CM.Text/[JetBrains.Annotations]/JetBrains.Annotations.cs @@ -1272,4 +1272,4 @@ internal sealed class RazorWriteMethodParameterAttribute : Attribute internal sealed class NoReorder : Attribute { } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 65a5772..98e61b3 100644 --- a/README.md +++ b/README.md @@ -415,3 +415,20 @@ var client = new TextClient(apiKey); var message = builder.Build(); var result = await client.SendMessageAsync(message); ``` + +## Using the OTP API +Send a simple OTP code +```cs + var client = new TextClient(new Guid(ConfigurationManager.AppSettings["ApiKey"])); + var otpBuilder = new OtpRequestBuilder("Sender_name", "Recipient_PhoneNumber"); + otpBuilder.WithMessage("Your otp code is {code}."); + var result = await textClient.SendOtpAsync(otpBuilder.Build()); +``` + +Verify the response code +```cs + var verifyResult = client.VerifyOtp("OTP-ID", "code"); + bool isValid = verifyResult.Verified; +``` + +For more advanced scenarios see also https://developers.cm.com/identity/docs/one-time-password-create \ No newline at end of file