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.
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.
StartMessaging Team
StartMessaging Team