Tutorials

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 Updated

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

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.

S

StartMessaging Team

StartMessaging Team

Related posts