Build a Secure Signup API in .NET that Filters Emails and Verifies with Twilio SendGrid

October 01, 2025
Written by
Oghenevwede Emeni
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Build a Secure Signup API in .Net

Sometimes at work, we’d see what looked like a surge of new users on random mornings and get very excited, only to discover later that most were fake or spam signups using disposable or suspicious email addresses. This not only polluted our metrics but also opened the door to abuse. In this tutorial, you’ll learn how to build a secure signup API using .NET that blocks specific email addresses and domains, adds domain-level access control, and sends verification emails through Twilio SendGrid.

Prerequisites

To follow along with this tutorial, you’ll need:

  • A free Twilio account
  • A verified Twilio SendGrid API key
  • .NET 6 SDK or later
  • Basic knowledge of building REST APIs in ASP.NET Core
  • Swagger or a similar API testing tool
  • A text editor like VS Code

Setting up your .NET Web API project

To get started, open up your IDE of choice, and create a new ASP.NET Core Web API using the .NET CLI. In the terminal, type:

dotnet new webapi -n SecureSignupApi
cd SecureSignupApi

Then, run the app to confirm everything is working.

dotnet run

By default, your output should look like the screenshot below.

application running output
application running output

Setting Up Swagger UI

Swagger gives you a browser-based interface for testing your API endpoints, and is really helpful during development and when sharing API documentation with team members or clients.

Your project doesn’t support Swagger out of the box. You'll need to install the Swashbuckle.AspNetCore NuGet package first.

In your terminal, run:

dotnet add package Swashbuckle.AspNetCore

This adds Swagger and OpenAPI tooling that enables AddSwaggerGen() and UseSwagger() in your code.

swagger ui sample
swagger ui sample

Swagger is really useful when debugging API responses or demoing endpoint behavior without needing to use an external tool like Postman.

What’s in your Program.cs

When you create a new Web API project in .NET 6 or later using the CLI, your Program.csfile acts as the central hub. Instead of separate controller files, your routes and logic can live directly inside Program.cs. This is known as the minimal API pattern, which is okay for basic programs.

By default, the project comes with a sample endpoint called /weatherforecast. You'll need to remove this so you can replace it with your own custom logic for this project, starting with a /signup endpoint.

Clean out the template

  1. Open Program.cs.
  2. Delete or comment out everything related to /weatherforecast. That includes the summaries array, MapGet("/weatherforecast"), and the WeatherForecast record.

Your Program.cs should now be mostly empty except for the startup logic.

Adding Swagger Services

Now you are going to add the Swagger services so your API is visible in Swagger.

In Program.cs, look for this line:

var builder = WebApplication.CreateBuilder(args);

Below that line, add the services below. These lines of code will generate the OpenAPI specification and UI.

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

Here’s what each line does:

  • AddControllers() tells .NET to look for controller files like SignupController.cs and register the routes defined there.
  • AddEndpointsApiExplorer() enables minimal APIs or conventional routing to be discoverable.
  • AddSwaggerGen() generates the Swagger/OpenAPI documentation based on your registered endpoints.

Without AddControllers(), your custom routes like /signup will not appear in Swagger, which might cause you to see issues like " No operations defined in spec".

Now, look for this line in Program.cs.

var app = builder.Build();

Below that line, add the middleware configuration, which tells your app to serve the OpenAPI JSON and render the Swagger interface at /swagger.

app.UseSwagger();
app.UseSwaggerUI();
// Also enable authorization middleware, even if not configured yet
app.UseAuthorization();
// And map controller endpoints like POST /signup
app.MapControllers();

This configures your API to:

  • Serve Swagger UI at /swagger
  • Redirect all HTTP traffic to HTTPS
  • Load routes defined in controller classes like SignupController

Final Program.cs should look like this:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Then run your app again, using:

dotnet run

Visit http://localhost:5000/swagger (or whatever port your application is running on) and you’ll see a full UI where you can test API endpoints like POST /signup in real time.

You should now see the Swagger UI. If it says “No operations defined in spec,” don’t worry. This is expected at this stage, since you haven’t defined any custom endpoints yet. You’ll see your routes appear in Swagger once you create them in the following sections.

Creating the Signup Endpoint

Create the Models folder

The first step is to create a Models folder in the root of your project. Then, inside it, create a file named SignupRequest.cs (projectname/Models/SignupRequest.cs). This is where you define all classes used to represent structured data in your API. In this case, you’ll create a class named SignupRequest that holds the email address submitted during signup.

namespace SecureSignupApi.Models
{
    public class SignupRequest
    {
        public string Email { get; set; }
    }
}

Create the controller

Next, create another folder in the root of your project and name it Controllers. In the Controllers folder, add a file named SignupController.cs. This controller defines your API logic and contains the /signup endpoint.

  • [ApiController] allows automatic model validation and better error messages.
  • [Route("[controller]")] sets the endpoint to match the controller name, i.e., /signup.
  • IActionResult Signup(...) is the method that handles POST requests with user email data.

It also contains a function that validates the email for emptiness and proper format using a regular expression.

using Microsoft.AspNetCore.Mvc;
using SecureSignupApi.Models;
using System.Text.RegularExpressions;
namespace SecureSignupApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class SignupController : ControllerBase
    {
        [HttpPost]
        public IActionResult Signup([FromBody] SignupRequest request)
        {
            if (string.IsNullOrWhiteSpace(request.Email))
            {
                return BadRequest("Email is required.");
            }
            var emailPattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
            if (!Regex.IsMatch(request.Email, emailPattern))
            {
                return BadRequest("Invalid email format.");
            }
            return Ok("Email is valid.");
        }
    }
}

Now you can check if the email validation works as it should. Run the app and visit http://localhost:5000/swagger to try the POST /signup endpoint with:

{
  "email": "test@example.com"
}

Your response should look like this:

Email is valid.

Blocking Disposable Email Domains

You've likely been here. Find a new product you wanted to test, or quickly download, and you toss in a throwaway email like user@mailinator.com. Of course, you think to yourself, no harm done. But imagine running a platform and waking up to thousands of those fake addresses distorting your growth metrics, giving you false signs of hope and alerts, and even opening doors to misuse. What started as someone skipping the friction becomes your team's data nightmare. To prevent this, you can try to be one step ahead and block common and suspicious looking disposable email domains using a local file.

Create a blocklist file

In the root of your project, add a file called blocked_domains.txt ( projectname/ blocked_domains.txt). In this file you can add a list of domains that should be flagged immediately if they try to sign up. The beauty of doing this in this separate file is the ease of adding to or removing from the list without requiring code changes.

mailinator.com
tempmail.com
yopmail.com
10minutemail.com

Create a domain checker class

Create a folder called Utils in the root of your project (project/Utils). This folder will house utility classes that support your application's logic but don't belong to the controller or model layers. Inside it, add a create a file called BlockedDomainChecker.cs. Like the name suggests, this class will handle the logic for checking whether an email's domain appears in your blocklist. You will make use of a Hashset by loading the list into it. This will ensure fast lookups, and make this solution efficient even as the list grows.

You’ll extract the domain from each submitted email, convert it to lowercase, and check if it exists in your blocked domains list. Paste the following code into your BlockedDomainChecker.cs file

using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace SecureSignupApi.Utils
{
    public class BlockedDomainChecker
    {
        private readonly HashSet<string> _blocked;
        public BlockedDomainChecker(string path)
        {
            _blocked = File.Exists(path)
                ? new HashSet<string>(File.ReadAllLines(path).Select(d => d.Trim().ToLower()))
                : new HashSet<string>();
        }
        public bool IsBlocked(string email)
        {
            var domain = email.Split('@').Last().ToLower();
            return _blocked.Contains(domain);
        }
    }
}

Next, register the BlockedDomainChecker in your Program.cs file under the Swagger and controller services. This will make sure the class is available to be injected into your controller.

Open your Program.cs and add the following line:

builder.Services.AddSingleton<BlockedDomainChecker>(_ =>
    new BlockedDomainChecker("blocked_domains.txt"));

Use it in your controller

First you need to call the file, so add this to the top of your Controllers/SignupController.cs :

using SecureSignupApi.Utils;

In your SignupController, define the checker as a private field and instantiate it:

private readonly BlockedDomainChecker _domainChecker = new("blocked_domains.txt");

Then, inside your Signup method, right after validating the email format, check if the domain is blocked:

if (_domainChecker.IsBlocked(request.Email))
{
    return BadRequest("Email domain is not allowed.");
}

Now you can go back to Swagger to test your endpoint. Run your app and test with:

{
  "email": "test@mailinator.com"
}

Your response should look like this:

blocked email check result
blocked email check result

Restricting signups to specific allowed domains

While blocking bad domains is helpful, sometimes you want to go a step further. For example, you might want to:

  • Only allow people from your company (like @mycompany.com) to register
  • Prevent anyone from signing up with a personal Gmail or Yahoo address

To achieve this successfully, you can handle this by setting up a configuration-based allowlist.

Add allowed domains to your config

Head over to your appsettings.json file and add the following after "AllowedHosts:":

"AllowedDomains": [
  "mycompany.com",
  "twilio.com"
]

This array represents the only domains that should be allowed. You can update it without changing your code.

Create the domain allowlist checker

Next, you need to create a new file under the Utils folder called Utils/AllowedDomainChecker.cs. In this file, you will communicate with the array of allowed domains and will have a service to check if the domain extracted from the inputted email address is one of the permitted domains, if not you prevent them from accessing.

using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.Linq;
namespace SecureSignupApi.Utils
{
    public class AllowedDomainChecker
    {
        private readonly HashSet<string> _allowed;
        public AllowedDomainChecker(IConfiguration config)
        {
            _allowed = new HashSet<string>(
                config.GetSection("AllowedDomains").Get<string[]>()?.Select(d => d.ToLower()) ?? []
            );
        }
        public bool IsAllowed(string email)
        {
            var domain = email.Split('@').Last().ToLower();
            return _allowed.Contains(domain);
        }
    }
}

Next, you need to register the checker in Program.cs under the swagger service you added earlier. This way the class is available to be injected into your controller.

Add the following line to the top of your Program.cs, under other using directives:

using SecureSignupApi.Utils;

Then, under the AddSwaggerGen(); line you added earlier, add this:

builder.Services.AddSingleton<AllowedDomainChecker>();

Use it in your controller

You need to inject the AllowedDomainChecker through the constructor in your SignupController.cs:

private readonly AllowedDomainChecker _allowedChecker;
public SignupController(AllowedDomainChecker allowedChecker)
{
    _domainChecker = new("blocked_domains.txt");
    _allowedChecker = allowedChecker;
}

Add allowlist validation inside the Signup method. Do this after the email format and blocklist check.

if (!_allowedChecker.IsAllowed(request.Email))
{
    return BadRequest("Only company emails are allowed.");
}

Now, even if someone uses a valid and non-disposable email (like user@gmail.com), this step ensures they’re still not allowed unless their domain is explicitly approved by you or your team. It's very useful for internal tools or closed betas.

Send a verification email with Twilio SendGrid

Once the email passes all validations, you want to send a confirmation message to the user’s inbox. We'll do this using Twilio SendGrid. First, head over to your terminal and run:

dotnet add package SendGrid
dotnet add package dotenv.net
  • The Sendgrid package lets you send emails through the Twilio Sendgrid API
  • dotenv.net package loads environment variables from an .env file

Configure Twilio SendGrid

Before you can send emails, you'll need a SendGrid API key. Here's how to generate one:

  • Log into your SendGrid Dashboard.
  • In the left-hand menu, click Settings, then API Keys.
  • Click the Create API Key button.
  • Give the key a name you’ll recognize (e.g., SecureSignupApi).
  • For simplicity in this tutorial, select Full Access (you can scope it down later for production use).
  • Click Create & View.

SendGrid will now show you the API key once. Copy it immediately and store it somewhere secure. If you close the page without copying the key, you'll need to generate a new one. In this project, you’ll store the key in a .env file.

Next, in the root of your project, create your .env file. Remember to add this file to your .gitignore to prevent it from being pushed when committing to Git. In this file define the variables below:

SENDGRID_API_KEY=Your sendgrid key
FROM_EMAIL=verified sender email on your dashboard
FROM_NAME=Preferred name

Create the email sending service

You’ll need to create a reusable service class that handles sending emails using the SendGrid API. Create a new folder in the root of your project called Service (projectname/service) and within it, a file called EmailService.cs.

This service will read credentials from environment variables and send a standard verification message to any valid email. Here is what the service will do:

  • The service loads SendGrid credentials from environment variables (set via your .env file).
  • It uses SendGrid’s helper to format the message.
  • Success or failure is logged in your terminal.
using SendGrid;
using SendGrid.Helpers.Mail;
using System;
using System.Threading.Tasks;
namespace SecureSignupApi.Services
{
    public class EmailService
    {
        private readonly string _apiKey;
        private readonly string _fromEmail;
        private readonly string _fromName;
        public EmailService()
        {
            _apiKey = Environment.GetEnvironmentVariable("SENDGRID_API_KEY")!;
            _fromEmail = Environment.GetEnvironmentVariable("FROM_EMAIL")!;
            _fromName = Environment.GetEnvironmentVariable("FROM_NAME")!;
        }
        public async Task SendVerificationEmail(string toEmail)
{
    var client = new SendGridClient(_apiKey);
    var from = new EmailAddress(_fromEmail, _fromName);
    var to = new EmailAddress(toEmail);
    var subject = "Please Verify Your Email";
    var plainTextContent = "Thank you for signing up. Please verify your email address.";
    var htmlContent = "<strong>Thank you for signing up. Please verify your email address.</strong>";
    var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);
    var response = await client.SendEmailAsync(msg);
    if ((int)response.StatusCode >= 400)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine($"[ERROR] Failed to send verification email to {toEmail}. Status: {response.StatusCode}");
        Console.ResetColor();
        throw new Exception("Failed to send verification email.");
    }
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine($"[INFO] Verification email successfully sent to {toEmail}");
    Console.ResetColor();
}
    }
}

Register EmailService and load your .env file

To be able to use the EmailService in your controller, you need to register it with the dependency injection container and load your .env file at startup. Head to your Program.cs file, and at the top, alongside other imports, add this:

using dotenv.net;
using SecureSignupApi.Services;

Then, in your setup login in this same file, before the builder was created, add this to ensure that values like SENDGRID_API_KEY are available from your .env file.

DotEnv.Load();

And after all other service declarations, add this to make the email sender injectable in your controller.

builder.Services.AddSingleton<EmailService>();

Inject and use EmailService in your controller

Now that the service is registered, you can add it to your controller and use it after the email is validated. First, update your controller’s constructor:

private readonly EmailService _emailService;
public SignupController(AllowedDomainChecker allowedChecker, EmailService emailService)
{
    _domainChecker = new("blocked_domains.txt");
    _allowedChecker = allowedChecker;
    _emailService = emailService;
}

Then, call it at the end of the Signup method:

await _emailService.SendVerificationEmail(request.Email);
return Ok("Verification email sent.");

Now to test, run your app and head back to swagger. Test with:

{
  "email": "user@mycompany.com"
}

If the email is valid - meaning, it's not a blocked domain, and the domain is on your allowed list - the API sends a real email and logs a green [INFO] message in the terminal. Something like this:

Success image from swagger
Success image from swagger
Notification message indicating a verification email has been sent to a Gmail address.

Troubleshooting Tips

Here are a few common issues you might run into and tips to guide you:

  • If the /swagger page doesn't load, make sure UseSwagger() and UseSwaggerUI() are included in Program.cs.
  • If no email is received, check that your SendGrid API key is correct and your sender email is verified in your SendGrid dashboard.
  • If you are using Twilio's Sendgrid trial account, you might be restricted to sending 100 emails daily.
  • If environment variables aren't loading, ensure .env exists and DotEnv.Load() is called before service registration.

Conclusion

Congratulations on building a secure and production-ready signup API in .NET! You’ve learned how to validate emails, block suspicious domains, enforce domain-level access control, and send real-time verification emails using Twilio SendGrid. This foundation is perfect for internal tools, waitlisted platforms, or any application where user authenticity matters.

If you’d like to explore this project further or fork a working version, check out the GitHub repository.

Remember, email sending is tightly monitored. Avoid spamming users or sending content without consent. As a next step, explore the Twilio SendGrid documentation to learn about advanced features like sender reputation, domain authentication, and delivery optimization.

Thanks for reading!

Oghenevwede is a Senior Product Engineer with 8 years of obsessing about how things work on her phone and on the web. Her new obsession is health and wellness.