Skip to content
Open
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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A C# client library for interacting with the [CloudContactAI](https://cloudconta
- Brand registration and management for TCR verification
- Campaign registration and management for TCR carrier vetting
- Manage contact opt-out preferences (SetDoNotText)
- Validate email addresses (valid/invalid/risky) and phone numbers (valid/invalid/landline)
- Webhook management: register, list, update, delete
- Webhook signature verification
- Template variable substitution (`${firstName}`, `${lastName}`)
Expand Down Expand Up @@ -280,6 +281,40 @@ await ccai.Contact.SetDoNotTextAsync(false, phone: "+15551234567");
await ccai.Contact.SetDoNotTextAsync(true, contactId: "contact-abc-123");
```

### Contact Validator

Validate email addresses and phone numbers.

> Bulk endpoints accept up to 50 contacts per request and are processed server-side in chunks.

```csharp
using CCAI.NET;
using CCAI.NET.ContactValidator;

// Validate a single email
var emailResult = await ccai.ContactValidator.ValidateEmailAsync("user@example.com");
Console.WriteLine(emailResult.Status); // "valid" | "invalid" | "risky"

// Validate multiple emails (up to 50, processed server-side in chunks)
var bulkEmails = await ccai.ContactValidator.ValidateEmailsAsync(new[] {
"user@example.com",
"bad@invalid.xyz"
});
Console.WriteLine(bulkEmails.Summary.Total); // 2
Console.WriteLine(bulkEmails.Summary.Valid); // 1

// Validate a single phone number
var phoneResult = await ccai.ContactValidator.ValidatePhoneAsync("+15551234567", "US");
Console.WriteLine(phoneResult.Status); // "valid" | "invalid" | "landline"

// Validate multiple phone numbers (up to 50, processed server-side in chunks)
var bulkPhones = await ccai.ContactValidator.ValidatePhonesAsync(new[] {
new PhoneInput { Phone = "+15551234567" },
new PhoneInput { Phone = "+15559876543", CountryCode = "US" }
});
Console.WriteLine(bulkPhones.Summary.Landline); // 1
```

### Webhook Management

#### CloudContact Webhook Events (New Format)
Expand Down
10 changes: 10 additions & 0 deletions RELEASE-NOTES-v1.5.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Release Notes - Version 1.5.0

## New Features
- Added `ContactValidator` service for validating email addresses and phone numbers
- `ValidateEmailAsync` / `ValidateEmail` — validate a single email, returns `valid`, `invalid`, or `risky` status with `safe_to_send` and `ai_verdict` metadata
- `ValidateEmailsAsync` / `ValidateEmails` — bulk email validation (up to 50 addresses) with summary counts
- `ValidatePhoneAsync` / `ValidatePhone` — validate a single phone number, returns `valid`, `invalid`, or `landline` status with carrier metadata
- `ValidatePhonesAsync` / `ValidatePhones` — bulk phone validation (up to 50 numbers) with summary counts including landline count
- New models: `EmailValidationResult`, `PhoneValidationResult`, `ValidationSummary`, `BulkEmailValidationResult`, `BulkPhoneValidationResult`, `PhoneInput`
- `IContactValidatorService` interface for dependency injection and testing
4 changes: 2 additions & 2 deletions src/CCAI.NET/CCAI.NET.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<PackageId>CloudContactAI.CCAI.NET</PackageId>
<Version>1.4.5</Version>
<Version>1.5.0</Version>
<Authors>CloudContactAI LLC</Authors>
<Company>CloudContactAI LLC</Company>
<Description>C# client for CloudContactAI API with SMS, MMS, Email, and Webhook support. Enhanced webhook support with new CloudContact event format, contact.unsubscribed events, environment variable configuration, and comprehensive examples.</Description>
Expand All @@ -15,7 +15,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RepositoryUrl>https://github.com/CloudContactAI/CCAI.NET</RepositoryUrl>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../RELEASE-NOTES-v1.4.5.md"))</PackageReleaseNotes>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../RELEASE-NOTES-v1.5.0.md"))</PackageReleaseNotes>
<PackageProjectUrl>https://github.com/CloudContactAI/CCAI.NET</PackageProjectUrl>
<RepositoryType>git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand Down
12 changes: 12 additions & 0 deletions src/CCAI.NET/CCAIClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using CCAI.NET.Brands;
using CCAI.NET.Campaigns;
using CCAI.NET.Contact;
using CCAI.NET.ContactValidator;
using CCAI.NET.Email;
using CCAI.NET.SMS;
using CCAI.NET.Webhook;
Expand Down Expand Up @@ -60,6 +61,11 @@ public interface ICCAIClient : IDisposable
/// </summary>
CampaignService Campaigns { get; }

/// <summary>
/// Contact validator service for validating email and phone contacts
/// </summary>
IContactValidatorService ContactValidator { get; }

/// <summary>
/// Get the client ID
/// </summary>
Expand Down Expand Up @@ -284,6 +290,11 @@ public class CCAIClient : ICCAIClient
/// </summary>
public CampaignService Campaigns { get; }

/// <summary>
/// Contact validator service for validating email and phone contacts
/// </summary>
public IContactValidatorService ContactValidator { get; }

/// <summary>
/// Create a new CCAI client instance
/// </summary>
Expand Down Expand Up @@ -333,6 +344,7 @@ public CCAIClient(CCAIConfig config, HttpClient? httpClient = null)
Contact = new ContactService(this);
Brands = new BrandService(this);
Campaigns = new CampaignService(this);
ContactValidator = new ContactValidatorService(this);
}

/// <summary>
Expand Down
139 changes: 139 additions & 0 deletions src/CCAI.NET/ContactValidator/ContactValidatorModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) 2025 CloudContactAI LLC
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Text.Json;
using System.Text.Json.Serialization;

namespace CCAI.NET.ContactValidator;

/// <summary>
/// Validation result for a single email address
/// </summary>
public class EmailValidationResult
{
/// <summary>
/// The validated email address
/// </summary>
[JsonPropertyName("contactField")]
public string ContactField { get; set; } = string.Empty;

/// <summary>
/// Contact type — always "email"
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;

/// <summary>
/// Validation status: "valid", "invalid", or "risky"
/// </summary>
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;

/// <summary>
/// Additional metadata (e.g. safe_to_send, ai_verdict)
/// </summary>
[JsonPropertyName("metadata")]
public Dictionary<string, JsonElement>? Metadata { get; set; }
}

/// <summary>
/// Validation result for a single phone number
/// </summary>
public class PhoneValidationResult
{
/// <summary>
/// The validated phone number
/// </summary>
[JsonPropertyName("contactField")]
public string ContactField { get; set; } = string.Empty;

/// <summary>
/// Contact type — always "phone"
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;

/// <summary>
/// Validation status: "valid", "invalid", or "landline"
/// </summary>
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;

/// <summary>
/// Additional metadata (e.g. country_code, national_number, carrier_type)
/// </summary>
[JsonPropertyName("metadata")]
public Dictionary<string, JsonElement>? Metadata { get; set; }
}

/// <summary>
/// Aggregate counts for a bulk validation response
/// </summary>
public class ValidationSummary
{
/// <summary>Total contacts validated</summary>
[JsonPropertyName("total")]
public int Total { get; set; }

/// <summary>Number of valid contacts</summary>
[JsonPropertyName("valid")]
public int Valid { get; set; }

/// <summary>Number of invalid contacts</summary>
[JsonPropertyName("invalid")]
public int Invalid { get; set; }

/// <summary>Number of risky contacts (emails only)</summary>
[JsonPropertyName("risky")]
public int Risky { get; set; }

/// <summary>Number of landline numbers (phones only)</summary>
[JsonPropertyName("landline")]
public int Landline { get; set; }
}

/// <summary>
/// Response for a bulk email validation request
/// </summary>
public class BulkEmailValidationResult
{
/// <summary>Individual validation results</summary>
[JsonPropertyName("results")]
public List<EmailValidationResult> Results { get; set; } = new();

/// <summary>Aggregate summary</summary>
[JsonPropertyName("summary")]
public ValidationSummary Summary { get; set; } = new();
}

/// <summary>
/// Response for a bulk phone validation request
/// </summary>
public class BulkPhoneValidationResult
{
/// <summary>Individual validation results</summary>
[JsonPropertyName("results")]
public List<PhoneValidationResult> Results { get; set; } = new();

/// <summary>Aggregate summary</summary>
[JsonPropertyName("summary")]
public ValidationSummary Summary { get; set; } = new();
}

/// <summary>
/// Phone number input for bulk validation
/// </summary>
public class PhoneInput
{
/// <summary>
/// Phone number in E.164 format (e.g. +15551234567)
/// </summary>
[JsonPropertyName("phone")]
public string Phone { get; set; } = string.Empty;

/// <summary>
/// Optional ISO 3166-1 alpha-2 country code (e.g. "US")
/// </summary>
[JsonPropertyName("countryCode")]
public string? CountryCode { get; set; }
}
104 changes: 104 additions & 0 deletions src/CCAI.NET/ContactValidator/ContactValidatorService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) 2025 CloudContactAI LLC
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace CCAI.NET.ContactValidator;

/// <summary>
/// Interface for the contact validator service
/// </summary>
public interface IContactValidatorService
{
/// <summary>Validate a single email address (async)</summary>
Task<EmailValidationResult> ValidateEmailAsync(string email, CancellationToken cancellationToken = default);

/// <summary>Validate multiple email addresses up to the configured bulk limit (async)</summary>
Task<BulkEmailValidationResult> ValidateEmailsAsync(IEnumerable<string> emails, CancellationToken cancellationToken = default);

/// <summary>Validate a single phone number in E.164 format (async)</summary>
Task<PhoneValidationResult> ValidatePhoneAsync(string phone, string? countryCode = null, CancellationToken cancellationToken = default);

/// <summary>Validate multiple phone numbers up to the configured bulk limit (async)</summary>
Task<BulkPhoneValidationResult> ValidatePhonesAsync(IEnumerable<PhoneInput> phones, CancellationToken cancellationToken = default);

/// <summary>Validate a single email address (synchronous)</summary>
EmailValidationResult ValidateEmail(string email);

/// <summary>Validate multiple email addresses up to the configured bulk limit (synchronous)</summary>
BulkEmailValidationResult ValidateEmails(IEnumerable<string> emails);

/// <summary>Validate a single phone number in E.164 format (synchronous)</summary>
PhoneValidationResult ValidatePhone(string phone, string? countryCode = null);

/// <summary>Validate multiple phone numbers up to the configured bulk limit (synchronous)</summary>
BulkPhoneValidationResult ValidatePhones(IEnumerable<PhoneInput> phones);
}

/// <summary>
/// Service for validating email addresses and phone numbers through the CCAI API
/// </summary>
public class ContactValidatorService : IContactValidatorService
{
private readonly ICCAIClient _client;

/// <summary>
/// Create a new ContactValidatorService instance
/// </summary>
/// <param name="client">The parent CCAI client</param>
public ContactValidatorService(ICCAIClient client)
{
_client = client;
}

/// <summary>
/// Validate a single email address
/// </summary>
/// <param name="email">Email address to validate</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Validation result with status and metadata</returns>
public Task<EmailValidationResult> ValidateEmailAsync(string email, CancellationToken cancellationToken = default) =>
_client.RequestAsync<EmailValidationResult>(HttpMethod.Post, "/v1/contact-validator/email", new { email }, cancellationToken);

/// <summary>
/// Validate multiple email addresses (up to the configured bulk limit)
/// </summary>
/// <param name="emails">List of email addresses to validate</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Bulk validation results with summary</returns>
public Task<BulkEmailValidationResult> ValidateEmailsAsync(IEnumerable<string> emails, CancellationToken cancellationToken = default) =>
_client.RequestAsync<BulkEmailValidationResult>(HttpMethod.Post, "/v1/contact-validator/emails", new { emails }, cancellationToken);

/// <summary>
/// Validate a single phone number
/// </summary>
/// <param name="phone">Phone number in E.164 format (e.g. +15551234567)</param>
/// <param name="countryCode">Optional ISO 3166-1 alpha-2 country code (e.g. "US")</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Validation result with status and metadata</returns>
public Task<PhoneValidationResult> ValidatePhoneAsync(string phone, string? countryCode = null, CancellationToken cancellationToken = default) =>
_client.RequestAsync<PhoneValidationResult>(HttpMethod.Post, "/v1/contact-validator/phone", new { phone, countryCode }, cancellationToken);

/// <summary>
/// Validate multiple phone numbers (up to the configured bulk limit)
/// </summary>
/// <param name="phones">List of phone inputs with optional country codes</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Bulk validation results with summary</returns>
public Task<BulkPhoneValidationResult> ValidatePhonesAsync(IEnumerable<PhoneInput> phones, CancellationToken cancellationToken = default) =>
_client.RequestAsync<BulkPhoneValidationResult>(HttpMethod.Post, "/v1/contact-validator/phones", new { phones }, cancellationToken);

/// <inheritdoc/>
public EmailValidationResult ValidateEmail(string email) =>
ValidateEmailAsync(email).GetAwaiter().GetResult();

/// <inheritdoc/>
public BulkEmailValidationResult ValidateEmails(IEnumerable<string> emails) =>
ValidateEmailsAsync(emails).GetAwaiter().GetResult();

/// <inheritdoc/>
public PhoneValidationResult ValidatePhone(string phone, string? countryCode = null) =>
ValidatePhoneAsync(phone, countryCode).GetAwaiter().GetResult();

/// <inheritdoc/>
public BulkPhoneValidationResult ValidatePhones(IEnumerable<PhoneInput> phones) =>
ValidatePhonesAsync(phones).GetAwaiter().GetResult();
}