Skip to main content

.NET SDK

The Azotte .NET SDK provides a comprehensive interface for integrating subscription billing and payment processing into .NET applications, supporting both .NET Framework and .NET Core.

Installation

Package Manager Console

Install-Package Azotte.Net

.NET CLI

dotnet add package Azotte.Net

PackageReference

<PackageReference Include="Azotte.Net" Version="2.1.0" />

Quick Start

Basic Setup

using Azotte;
using Azotte.Models;

// Initialize the client
var client = new AzotteClient(new AzotteConfiguration
{
ApiKey = "sk_live_...",
TenantId = "tenant_123", // Optional for multi-tenant
Environment = Environment.Production
});

// Create a customer
var customer = await client.Customers.CreateAsync(new CustomerCreateRequest
{
Name = "John Doe",
Email = "john@example.com",
Phone = "+1234567890"
});

// Create a subscription
var subscription = await client.Subscriptions.CreateAsync(new SubscriptionCreateRequest
{
CustomerId = customer.Id,
PlanId = "plan_basic_monthly",
PaymentMethodId = "pm_card_visa"
});

Console.WriteLine($"Subscription created: {subscription.Id}");

Configuration Options

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;

// Dependency Injection setup
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AzotteConfiguration>(Configuration.GetSection("Azotte"));
services.AddSingleton<IAzotteClient, AzotteClient>();

// With custom HTTP client
services.AddHttpClient<AzotteClient>(client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
});
}

// appsettings.json
{
"Azotte": {
"ApiKey": "sk_live_...",
"TenantId": "tenant_123",
"BaseUrl": "https://api.azotte.com",
"Timeout": "00:00:30",
"RetryPolicy": {
"MaxRetries": 3,
"BackoffFactor": 2
}
}
}

Customer Management

Create Customer

var customer = await client.Customers.CreateAsync(new CustomerCreateRequest
{
Name = "Jane Smith",
Email = "jane@example.com",
Phone = "+1987654321",
Metadata = new Dictionary<string, string>
{
{ "source", "website" },
{ "campaign", "summer_2024" }
},
Address = new Address
{
Line1 = "123 Main St",
City = "San Francisco",
State = "CA",
PostalCode = "94105",
Country = "US"
}
});

Retrieve and Update

// Get customer by ID
var customer = await client.Customers.GetAsync("cus_123");

// Get with expanded data
var customerWithSubs = await client.Customers.GetAsync("cus_123",
new CustomerGetRequest
{
Expand = new[] { "subscriptions", "payment_methods" }
});

// Update customer
var updatedCustomer = await client.Customers.UpdateAsync("cus_123",
new CustomerUpdateRequest
{
Name = "Jane Doe",
Email = "jane.doe@example.com"
});

List Customers

// Basic listing
var customers = await client.Customers.ListAsync(new CustomerListRequest
{
Limit = 10
});

// With filters
var filteredCustomers = await client.Customers.ListAsync(new CustomerListRequest
{
CreatedAfter = DateTime.Parse("2024-01-01"),
EmailContains = "@company.com",
HasSubscription = true,
Limit = 50
});

// Auto-pagination
await foreach (var customer in client.Customers.ListAllAsync())
{
Console.WriteLine($"Processing customer: {customer.Id}");
}

Subscription Management

Create Subscription

var subscription = await client.Subscriptions.CreateAsync(new SubscriptionCreateRequest
{
CustomerId = "cus_123",
PlanId = "plan_premium_annual",
TrialPeriodDays = 14,
StartDate = DateTime.Now.AddDays(7),
PaymentMethodId = "pm_card_123",
Metadata = new Dictionary<string, string>
{
{ "promo_code", "WELCOME20" }
}
});

Multi-Item Subscription

var subscription = await client.Subscriptions.CreateAsync(new SubscriptionCreateRequest
{
CustomerId = "cus_123",
Items = new[]
{
new SubscriptionItemRequest { PlanId = "plan_basic", Quantity = 1 },
new SubscriptionItemRequest { PlanId = "addon_analytics", Quantity = 1 }
},
DiscountCoupon = "SAVE10",
BillingCycleAnchor = BillingCycleAnchor.Now
});

Subscription Lifecycle

// Change plan
var updatedSubscription = await client.Subscriptions.UpdateAsync("sub_123",
new SubscriptionUpdateRequest
{
PlanId = "plan_enterprise",
ProrationBehavior = ProrationBehavior.CreateProrations
});

// Pause subscription
await client.Subscriptions.PauseAsync("sub_123");

// Resume subscription
await client.Subscriptions.ResumeAsync("sub_123");

// Cancel subscription
await client.Subscriptions.CancelAsync("sub_123", new SubscriptionCancelRequest
{
AtPeriodEnd = true,
CancellationReason = "customer_request"
});

Payment Processing

Payment Methods

// Create payment method
var paymentMethod = await client.PaymentMethods.CreateAsync(new PaymentMethodCreateRequest
{
CustomerId = "cus_123",
Type = PaymentMethodType.Card,
Card = new CardDetails
{
Number = "4111111111111111",
ExpMonth = 12,
ExpYear = 2025,
Cvc = "123"
}
});

// Set as default
await client.Customers.UpdateAsync("cus_123", new CustomerUpdateRequest
{
DefaultPaymentMethodId = paymentMethod.Id
});

One-time Payments

var payment = await client.Payments.CreateAsync(new PaymentCreateRequest
{
Amount = 2999, // $29.99 in cents
Currency = Currency.USD,
CustomerId = "cus_123",
PaymentMethodId = "pm_123",
Description = "One-time setup fee",
Confirm = true
});

if (payment.Status == PaymentStatus.Succeeded)
{
Console.WriteLine($"Payment successful: {payment.Id}");
}
else
{
Console.WriteLine($"Payment failed: {payment.FailureReason}");
}

Refunds

// Full refund
var refund = await client.Refunds.CreateAsync(new RefundCreateRequest
{
PaymentId = "pay_123",
Reason = RefundReason.RequestedByCustomer
});

// Partial refund
var partialRefund = await client.Refunds.CreateAsync(new RefundCreateRequest
{
PaymentId = "pay_123",
Amount = 1500, // $15.00 in cents
Reason = RefundReason.DefectiveProduct
});

Webhook Handling

ASP.NET Core Integration

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IAzotteClient, AzotteClient>();
services.AddScoped<WebhookProcessor>();
}

// WebhookController.cs
[ApiController]
[Route("api/webhooks")]
public class WebhookController : ControllerBase
{
private readonly IAzotteClient _azotteClient;
private readonly WebhookProcessor _processor;

public WebhookController(IAzotteClient azotteClient, WebhookProcessor processor)
{
_azotteClient = azotteClient;
_processor = processor;
}

[HttpPost("azotte")]
public async Task<IActionResult> HandleAzotteWebhook()
{
var payload = await new StreamReader(Request.Body).ReadToEndAsync();
var signature = Request.Headers["azotte-signature"].FirstOrDefault();

try
{
var webhookEvent = _azotteClient.Webhooks.ConstructEvent(
payload, signature, "whsec_..."
);

await _processor.ProcessEventAsync(webhookEvent);
return Ok();
}
catch (AzotteSignatureVerificationException)
{
return BadRequest("Invalid signature");
}
}
}

// WebhookProcessor.cs
public class WebhookProcessor
{
public async Task ProcessEventAsync(WebhookEvent webhookEvent)
{
switch (webhookEvent.Type)
{
case "subscription.created":
await HandleSubscriptionCreated(webhookEvent.Data.ToObject<Subscription>());
break;
case "payment.succeeded":
await HandlePaymentSucceeded(webhookEvent.Data.ToObject<Payment>());
break;
case "payment.failed":
await HandlePaymentFailed(webhookEvent.Data.ToObject<Payment>());
break;
}
}

private async Task HandleSubscriptionCreated(Subscription subscription)
{
Console.WriteLine($"New subscription: {subscription.Id}");
// Send welcome email, grant access, etc.
}

private async Task HandlePaymentSucceeded(Payment payment)
{
Console.WriteLine($"Payment received: ${payment.Amount / 100m}");
// Update user account, send receipt, etc.
}

private async Task HandlePaymentFailed(Payment payment)
{
Console.WriteLine($"Payment failed: {payment.FailureReason}");
// Send dunning email, suspend access, etc.
}
}

Background Processing with Hangfire

// Install-Package Hangfire.AspNetCore

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(x => x.UseSqlServerStorage(connectionString));
services.AddHangfireServer();
}

// WebhookController.cs
[HttpPost("azotte")]
public IActionResult HandleAzotteWebhook([FromServices] IBackgroundJobClient jobs)
{
var payload = await new StreamReader(Request.Body).ReadToEndAsync();
var signature = Request.Headers["azotte-signature"].FirstOrDefault();

// Queue for background processing
jobs.Enqueue<WebhookProcessor>(x => x.ProcessWebhookAsync(payload, signature));

return Ok();
}

// WebhookProcessor.cs
public class WebhookProcessor
{
public async Task ProcessWebhookAsync(string payload, string signature)
{
var webhookEvent = _azotteClient.Webhooks.ConstructEvent(
payload, signature, _webhookSecret
);

await ProcessEventAsync(webhookEvent);
}
}

Advanced Features

Metered Billing

// Report usage
var usageRecord = await client.SubscriptionItems.CreateUsageRecordAsync(
"si_123",
new UsageRecordCreateRequest
{
Quantity = 100,
Timestamp = DateTime.UtcNow,
Action = UsageRecordAction.Increment
});

// Get usage summary
var usageSummary = await client.SubscriptionItems.ListUsageRecordsAsync(
"si_123",
new UsageRecordListRequest
{
StartDate = DateTime.Parse("2024-01-01"),
EndDate = DateTime.Parse("2024-01-31")
});

Invoicing

// Create invoice
var invoice = await client.Invoices.CreateAsync(new InvoiceCreateRequest
{
CustomerId = "cus_123",
AutoAdvance = true,
CollectionMethod = CollectionMethod.ChargeAutomatically
});

// Add invoice items
await client.InvoiceItems.CreateAsync(new InvoiceItemCreateRequest
{
CustomerId = "cus_123",
Amount = 2500,
Currency = Currency.USD,
Description = "Professional services"
});

// Finalize and send
var finalizedInvoice = await client.Invoices.FinalizeAsync("in_123");
await client.Invoices.SendAsync(finalizedInvoice.Id);

Error Handling

Exception Hierarchy

using Azotte.Exceptions;

try
{
var customer = await client.Customers.CreateAsync(new CustomerCreateRequest
{
Name = "", // Invalid: empty name
Email = "invalid-email" // Invalid: bad format
});
}
catch (AzotteValidationException ex)
{
Console.WriteLine($"Validation failed: {ex.UserMessage}");
foreach (var fieldError in ex.FieldErrors)
{
Console.WriteLine($" {fieldError.Key}: {string.Join(", ", fieldError.Value)}");
}
}
catch (AzotteAuthenticationException ex)
{
Console.WriteLine($"Authentication failed: {ex.Message}");
// Handle invalid API key
}
catch (AzotteRateLimitException ex)
{
Console.WriteLine($"Rate limited. Retry after: {ex.RetryAfter} seconds");
// Implement exponential backoff
}
catch (AzotteApiException ex)
{
Console.WriteLine($"API error: {ex.Message}");
Console.WriteLine($"Request ID: {ex.RequestId}");
Console.WriteLine($"Status code: {ex.StatusCode}");
}
catch (AzotteException ex)
{
Console.WriteLine($"Azotte error: {ex.Message}");
// Handle other Azotte-specific errors
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
// Handle network errors, etc.
}

Retry Policies with Polly

// Install-Package Polly
using Polly;
using Polly.Extensions.Http;

public static class RetryPolicies
{
public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => (int)msg.StatusCode == 429)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount} after {timespan} seconds");
});
}
}

// Usage
var httpClient = new HttpClient();
httpClient.AddPolicyHandler(RetryPolicies.GetRetryPolicy());

var client = new AzotteClient(new AzotteConfiguration
{
ApiKey = "sk_live_...",
HttpClient = httpClient
});

Testing

Unit Testing with Moq

// Install-Package Moq
using Moq;
using Xunit;

public class SubscriptionServiceTests
{
[Fact]
public async Task CreateSubscription_ShouldReturnSubscription()
{
// Arrange
var mockClient = new Mock<IAzotteClient>();
var expectedSubscription = new Subscription
{
Id = "sub_test_123",
CustomerId = "cus_test",
Status = SubscriptionStatus.Active
};

mockClient.Setup(x => x.Subscriptions.CreateAsync(It.IsAny<SubscriptionCreateRequest>()))
.ReturnsAsync(expectedSubscription);

// Act
var service = new SubscriptionService(mockClient.Object);
var result = await service.CreateSubscriptionAsync("cus_test", "plan_basic");

// Assert
Assert.Equal(expectedSubscription.Id, result.Id);
mockClient.Verify(x => x.Subscriptions.CreateAsync(
It.Is<SubscriptionCreateRequest>(r => r.CustomerId == "cus_test")), Times.Once);
}
}

Integration Testing

public class AzotteIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;

public AzotteIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_client = _factory.CreateClient();
}

[Fact]
public async Task CreateCustomer_WithValidData_ReturnsSuccess()
{
// Use test API key
var azotteClient = new AzotteClient(new AzotteConfiguration
{
ApiKey = "sk_test_...",
Environment = Environment.Sandbox
});

var customer = await azotteClient.Customers.CreateAsync(new CustomerCreateRequest
{
Name = "Test Customer",
Email = "test@example.com"
});

Assert.NotNull(customer.Id);
Assert.StartsWith("cus_test_", customer.Id);
}
}

Performance Optimization

Connection Pooling

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Configure HTTP client with connection pooling
services.AddHttpClient<AzotteClient>(client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
MaxConnectionsPerServer = 20,
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
});
}

Async Patterns

public class CustomerService
{
private readonly IAzotteClient _client;

public CustomerService(IAzotteClient client)
{
_client = client;
}

// Parallel processing
public async Task<IEnumerable<Customer>> CreateMultipleCustomersAsync(
IEnumerable<CustomerCreateRequest> requests)
{
var tasks = requests.Select(request => _client.Customers.CreateAsync(request));
return await Task.WhenAll(tasks);
}

// Efficient pagination
public async Task ProcessAllCustomersAsync(Func<Customer, Task> processor)
{
await foreach (var customer in _client.Customers.ListAllAsync())
{
await processor(customer);
}
}
}

Best Practices

Dependency Injection

// Program.cs (.NET 6+)
var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<AzotteConfiguration>(
builder.Configuration.GetSection("Azotte"));

builder.Services.AddSingleton<IAzotteClient>(provider =>
{
var config = provider.GetRequiredService<IOptions<AzotteConfiguration>>().Value;
return new AzotteClient(config);
});

builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();

Configuration Management

public class AzotteConfigurationValidator : IValidateOptions<AzotteConfiguration>
{
public ValidateOptionsResult Validate(string name, AzotteConfiguration options)
{
if (string.IsNullOrEmpty(options.ApiKey))
return ValidateOptionsResult.Fail("ApiKey is required");

if (!options.ApiKey.StartsWith("sk_"))
return ValidateOptionsResult.Fail("ApiKey must start with 'sk_'");

return ValidateOptionsResult.Success;
}
}

// Register validator
services.AddSingleton<IValidateOptions<AzotteConfiguration>, AzotteConfigurationValidator>();

Logging Integration

// Install-Package Microsoft.Extensions.Logging
public class AzotteClient : IAzotteClient
{
private readonly ILogger<AzotteClient> _logger;

public AzotteClient(AzotteConfiguration config, ILogger<AzotteClient> logger)
{
_logger = logger;
// Configure client with logging
}

public async Task<Customer> CreateCustomerAsync(CustomerCreateRequest request)
{
_logger.LogInformation("Creating customer with email {Email}", request.Email);

try
{
var customer = await InternalCreateCustomerAsync(request);
_logger.LogInformation("Created customer {CustomerId}", customer.Id);
return customer;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create customer with email {Email}", request.Email);
throw;
}
}
}

Next Steps