Best Practices
Guidelines for building reliable, secure, and compliant messaging applications with the Approved Contact Texting API. Following these practices will help ensure high delivery rates, maintain compliance, and provide excellent user experiences.
Security
Protect your API credentials and ensure all communications are secure. Security breaches can lead to unauthorized usage, data leaks, and compliance violations.
Credential Management
- Never Commit Credentials: Keep API credentials out of version control (use .gitignore)
- Use Environment Variables: Store credentials in environment variables or secret managers
- Rotate Regularly: Change passwords every 90 days minimum
- Limit Access: Create separate accounts for different environments (dev, staging, prod)
- Audit Access: Log all API credential usage and review regularly
Example: Secure Configuration (C#)
// appsettings.json - DO NOT store credentials here in production
{
"ApprovedContact": {
"ApiBaseUrl": "https://api.approvedcontact.com"
}
}
// Use User Secrets for development
// dotnet user-secrets set "ApprovedContact:Username" "your-email@example.com"
// dotnet user-secrets set "ApprovedContact:Password" "your-password"
// Use Azure Key Vault for production
public class ApprovedContactClient
{
private readonly IConfiguration _configuration;
public ApprovedContactClient(IConfiguration configuration)
{
_configuration = configuration;
}
private string GetUsername() =>
_configuration["ApprovedContact:Username"] ??
throw new InvalidOperationException("API username not configured");
private string GetPassword() =>
_configuration["ApprovedContact:Password"] ??
throw new InvalidOperationException("API password not configured");
}
Transport Security
- HTTPS Only: All API requests must use HTTPS (TLS 1.2+)
- Certificate Validation: Always validate SSL certificates
- No HTTP Fallback: Never fall back to HTTP on HTTPS failure
- Secure Webhooks: Validate webhook signatures (HMAC-SHA256)
Webhook Security
public bool ValidateWebhookSignature(string payload, string signature, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expectedSignature = Convert.ToBase64String(hash);
// Use timing-safe comparison to prevent timing attacks
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expectedSignature)
);
}
Reliability
Build resilient applications that handle failures gracefully and ensure message delivery even when services are temporarily unavailable.
Retry Logic
- Implement Exponential Backoff: Increase delay between retries (1s, 2s, 4s, 8s...)
- Set Max Retries: Limit to 3-5 attempts to avoid infinite loops
- Idempotency: Use idempotency keys for safe retries
- Circuit Breaker: Stop retrying after consecutive failures
Example: Polly Retry Policy (C#)
using Polly;
using Polly.Extensions.Http;
public static IAsyncPolicy GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError() // 5xx and 408
.Or()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount} after {timespan.TotalSeconds}s");
}
);
}
// Configure HttpClient with retry policy
services.AddHttpClient("ApprovedContactAPI")
.AddPolicyHandler(GetRetryPolicy())
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
Rate Limiting
- Respect 429 Responses: Honor Retry-After header
- Implement Client-Side Throttling: Limit concurrent requests
- Use Async Messaging: For high-volume sends (>100 messages)
- Monitor Rate Limits: Track API usage to avoid hitting limits
Example: Rate Limiting (C#)
using System.Threading.RateLimiting;
public class MessageService
{
private readonly RateLimiter _rateLimiter;
public MessageService()
{
// Limit to 10 requests per second
_rateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
TokenLimit = 10,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = 10,
AutoReplenishment = true
});
}
public async Task SendMessageAsync(MessageRequest message)
{
using var lease = await _rateLimiter.AcquireAsync();
if (!lease.IsAcquired)
{
throw new InvalidOperationException("Rate limit exceeded");
}
return await _httpClient.PostAsJsonAsync("/api/v1/messages", message);
}
}
Connection Management
- Connection Pooling: Reuse HttpClient instances
- Set Timeouts: Configure reasonable timeouts (30s for API calls)
- Handle Timeouts: Implement timeout-specific retry logic
- Connection Limits: Limit concurrent connections (default: 10)
Compliance
Follow legal requirements and industry standards to avoid penalties, maintain trust, and ensure your messaging program's longevity.
TCPA Compliance
- Obtain Consent: Get explicit, written consent before sending marketing messages
- Document Consent: Store consent records with timestamp and source
- Clear Opt-In: Use clear language: "I consent to receive SMS marketing"
- Separate Opt-Ins: Marketing consent must be separate from terms of service
Opt-Out Management
- Immediate Processing: Honor opt-out requests within minutes, not days
- Multiple Keywords: Accept STOP, UNSUBSCRIBE, CANCEL, END, QUIT
- Confirmation: Send immediate confirmation of opt-out
- Persist Across Channels: Opt-out applies to all message types
- Regular Audits: Review opt-out list weekly
Example: Opt-Out Handler
public async Task HandleOptOutAsync(IncomingMessage message)
{
var optOutKeywords = new[] { "STOP", "UNSUBSCRIBE", "CANCEL", "END", "QUIT" };
if (optOutKeywords.Contains(message.Message.ToUpperInvariant()))
{
// Add to opt-out list
await _optOutService.AddAsync(new OptOut
{
PhoneNumber = message.From,
Timestamp = DateTime.UtcNow,
Source = "SMS_KEYWORD"
});
// Send confirmation
await _messageService.SendAsync(new MessageRequest
{
From = message.TargetNumber,
To = new[] { message.From },
Body = "You have been unsubscribed. Reply START to resubscribe."
});
return true;
}
return false;
}
Message Content Requirements
- Identify Yourself: Include your business name in first message
- Provide Opt-Out: Include "Reply STOP to unsubscribe" in marketing messages
- Help Keyword: Respond to HELP with support contact info
- Frequency Disclosure: State message frequency (e.g., "Up to 4 msgs/month")
- Terms & Privacy: Provide links to terms and privacy policy
Example: Compliant Marketing Message
Hi Sarah! It's Acme Store. ?? 20% off this weekend only!
Show this text in-store or use code SMS20 online.
Shop now: acme.co/sale
Msg frequency varies. Reply HELP for help, STOP to unsubscribe.
Msg&data rates may apply. Terms: acme.co/terms
Performance
Optimize your integration for speed, efficiency, and scalability.
Caching Strategies
- Cache Credentials: Don't re-encode Basic Auth for every request
- Cache Phone Number Config: Tenant and phone number settings
- Cache Templates: Message templates and frequently used content
- TTL Management: Set appropriate cache expiration (5-15 min)
Example: Response Caching
public class CachedApprovedContactClient
{
private readonly IMemoryCache _cache;
private readonly HttpClient _httpClient;
public async Task GetPhoneNumberAsync(Guid phoneNumberId)
{
var cacheKey = $"phonenumber:{phoneNumberId}";
if (_cache.TryGetValue(cacheKey, out PhoneNumber cached))
{
return cached;
}
var response = await _httpClient.GetAsync($"/api/v1/phonenumbers/{phoneNumberId}");
var phoneNumber = await response.Content.ReadFromJsonAsync();
_cache.Set(cacheKey, phoneNumber, TimeSpan.FromMinutes(10));
return phoneNumber;
}
}
Batch Operations
- Batch Messages: Send to multiple recipients in one API call
- Use Async Mode: For sends >100 recipients
- Parallel Processing: Process webhook deliveries concurrently
- Database Batching: Bulk insert logs and analytics
Database Optimization
- Index Key Fields: messageId, threadId, phoneNumber, timestamp
- Partition Large Tables: By date for message logs
- Archive Old Data: Move messages >90 days to cold storage
- Async Writes: Queue non-critical writes (analytics, logs)
Messaging Guidelines
Message Timing
- Respect Time Zones: Send during business hours (9am-9pm local time)
- Avoid Weekends: Unless time-sensitive or expected
- No Late Night: Never send before 8am or after 9pm
- Frequency Limits: Max 1-2 marketing messages per week
Content Best Practices
- Keep It Short: Aim for <160 characters when possible
- Clear CTA: One clear call-to-action per message
- Personalize: Use recipient's name and relevant context
- Add Value: Every message should provide value
- Avoid Spam Triggers: No ALL CAPS, excessive emojis, or urgency
Deliverability Tips
- Warm Up Numbers: Start with low volume, gradually increase
- Monitor Opt-Out Rates: >5% indicates content issues
- Clean Your List: Remove invalid numbers monthly
- Segment Audiences: Targeted messages have higher engagement
- A/B Test: Test message content and timing
Monitoring & Observability
Key Metrics to Track
| Metric | Target | Action If Below Target |
|---|---|---|
| Delivery Rate | >95% | Check phone numbers, content, timing |
| API Success Rate | >99% | Review error logs, implement retries |
| Webhook Success Rate | >98% | Check endpoint health, increase timeout |
| Opt-Out Rate | <3% | Review content, reduce frequency |
| Response Latency (p95) | <500ms | Optimize queries, add caching |
Logging
- Log All API Calls: Request ID, timestamp, response code
- Log Webhook Deliveries: Success/failure with retry count
- Sanitize Logs: Never log credentials or full phone numbers
- Structured Logging: Use JSON format for easy parsing
- Log Levels: ERROR for failures, WARN for retries, INFO for success
Example: Structured Logging
public class MessageService
{
private readonly ILogger _logger;
public async Task SendAsync(MessageRequest request)
{
using var scope = _logger.BeginScope(new Dictionary
{
["MessageId"] = Guid.NewGuid(),
["From"] = MaskPhoneNumber(request.From),
["RecipientCount"] = request.To.Length
});
try
{
_logger.LogInformation("Sending message to {RecipientCount} recipients",
request.To.Length);
var response = await _httpClient.PostAsJsonAsync("/api/v1/messages", request);
_logger.LogInformation("Message sent successfully. Status: {StatusCode}",
response.StatusCode);
return await response.Content.ReadFromJsonAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send message");
throw;
}
}
private string MaskPhoneNumber(string phoneNumber) =>
phoneNumber.Length > 4
? $"+1****{phoneNumber.Substring(phoneNumber.Length - 4)}"
: "****";
}
Alerting
- API Errors: Alert on >1% error rate
- Webhook Failures: Alert on 3+ consecutive failures
- Delivery Rate Drop: Alert on <90% delivery
- High Opt-Out Rate: Alert on >5% in 24 hours
- Rate Limiting: Alert on 429 responses
Additional Resources
- Authentication Guide - Secure credential management
- Webhook Guide - Reliable webhook implementation
- Troubleshooting - Common issues and solutions
- API Reference - Complete endpoint documentation