Developer 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.

17 April 20269 min read

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

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

  1. Use IHttpClientFactory. Never new HttpClient() per request.
  2. Set a 10-second timeout so a slow upstream cannot stall your thread pool.
  3. Validate phone numbers using libphonenumber-csharp.
  4. Add idempotency keys to every send call — see why.
  5. 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.

Ready to Send OTPs?

Integrate StartMessaging in 5 minutes. No DLT registration required.