How to Send OTP in .NET / C# (ASP.NET Core 2026 Guide)
Send and verify SMS OTPs from a .NET 8 / ASP.NET Core API using the StartMessaging REST API. Includes HttpClient, typed clients, controller actions, and DI setup.
StartMessaging Team
Engineering
Adding phone OTP to an ASP.NET Core API is a great fit for HttpClient and the typed-client pattern. This guide wires up the StartMessaging OTP API from a .NET 8 minimal API in under 100 lines of code.
Prerequisites
- .NET 6, 7, or 8 SDK installed.
- A free StartMessaging account and an API key.
Add the key to appsettings.json or user secrets:
{
"StartMessaging": {
"ApiKey": "sm_live_xxxxxxxxxxxxxxxxxxxx",
"BaseUrl": "https://api.startmessaging.com"
}
}Register a Typed HttpClient
// Program.cs
builder.Services.AddHttpClient<StartMessagingClient>(client =>
{
var cfg = builder.Configuration.GetSection("StartMessaging");
client.BaseAddress = new Uri(cfg["BaseUrl"]!);
client.DefaultRequestHeaders.Add("X-API-Key", cfg["ApiKey"]);
client.Timeout = TimeSpan.FromSeconds(10);
});Send an OTP
public sealed class StartMessagingClient
{
private readonly HttpClient _http;
public StartMessagingClient(HttpClient http) => _http = http;
public record SendOtpResult(string RequestId, DateTime ExpiresAt, int AttemptsLeft);
private record SendEnvelope(SendOtpResult Data);
public async Task<SendOtpResult> SendOtpAsync(string phoneNumber, CancellationToken ct = default)
{
var body = new { phoneNumber, idempotencyKey = Guid.NewGuid().ToString() };
var res = await _http.PostAsJsonAsync("/otp/send", body, ct);
res.EnsureSuccessStatusCode();
var env = await res.Content.ReadFromJsonAsync<SendEnvelope>(cancellationToken: ct);
return env!.Data;
}
}Verify the OTP
public record VerifyResult(bool Verified);
private record VerifyEnvelope(VerifyResult Data);
public async Task<bool> VerifyOtpAsync(string requestId, string code, CancellationToken ct = default)
{
var body = new { requestId, otpCode = code };
var res = await _http.PostAsJsonAsync("/otp/verify", body, ct);
if (!res.IsSuccessStatusCode) return false;
var env = await res.Content.ReadFromJsonAsync<VerifyEnvelope>(cancellationToken: ct);
return env!.Data.Verified;
}Minimal API Endpoints
app.MapPost("/auth/send-otp", async (SendOtpRequest req, StartMessagingClient sm) =>
{
var data = await sm.SendOtpAsync(req.PhoneNumber);
return Results.Ok(new { data.RequestId, data.ExpiresAt });
});
app.MapPost("/auth/verify-otp", async (VerifyOtpRequest req, StartMessagingClient sm) =>
{
var ok = await sm.VerifyOtpAsync(req.RequestId, req.OtpCode);
return ok ? Results.Ok(new { verified = true }) : Results.Unauthorized();
});
public record SendOtpRequest(string PhoneNumber);
public record VerifyOtpRequest(string RequestId, string OtpCode);Error Handling
Wrap calls in try/catch (HttpRequestException) and handle the common status codes: 400 invalid phone, 401 bad API key, 402 wallet empty, 429 rate limited. See our full OTP rate limiting guide for production patterns.
Best Practices
- Use IHttpClientFactory. Never
new HttpClient()per request. - Set a 10-second timeout so a slow upstream cannot stall your thread pool.
- Validate phone numbers using
libphonenumber-csharp. - Add idempotency keys to every send call — see why.
- Cache the typed client’s config via DI; do not read appsettings on every request.
FAQ
Compare against Twilio in our Twilio vs StartMessaging breakdown, or check pricing at Rs 0.25 per OTP.
Related Articles
Step-by-step Node.js tutorial to send and verify OTP via SMS using the StartMessaging API. Includes fetch examples, error handling, and verification flow.
Spring Boot 3 + RestClient calling a TRAI-compliant OTP SMS API: JSON, env-based keys, and patterns for DLT-backed transactional SMS from JVM backends.
Learn what idempotency keys are, why they matter for OTP APIs, and how to implement them correctly to prevent duplicate SMS charges and improve reliability.
Ready to Send OTPs?
Integrate StartMessaging in 5 minutes. No DLT registration required.