C# provides several ways to send email. Maybe you've used the SMTPClient class or perhaps you've used the Mailkit NuGet package, which is what Microsoft recommends for new code. These solutions work well if you need to send a few messages and if you have access to an SMTP server. However, if you need to send lots of messages, you're quickly going to run into things like SPAM filters, daily message send limits and, in the worst case, you might get your domain flagged or blocked. As a result, many people looking to do bulk email end up using a third-party solution to act as a message broker and send the mail on their behalf. In this article, I'm going to take a look at one of these solutions, SendGrid, which, in my experience, is relatively easy to use, relatively affordable, and which comes with some nice C# utility classes.

Getting Set Up

The first step in using SendGrid is to sign up for a SendGrid account, and SendGrid does provide a free trial. Go to https://sendgrid.com and click the Start For Free button. Then, simply follow the sign-up wizard to configure your Twilio account, which gives you access to SendGrid as well as some other Twilio tools.

Once you're logged into SendGrid, you'll land on your SendGrid Dashboard, as shown in Figure 1.

Figure 1: The SendGrid dashboard
Figure 1: The SendGrid dashboard

From this dashboard, you can start setting up your prerequisites.

Create an API Key

The SendGrid API is a REST API that is secured with an API key. On the SendGrid Dashboard, expand the Settings item in the sidebar on the left and click the API Keys menu item, as shown in Figure 2. Click the Create API Key button in the upper right corner and walk through the process for generating a new API key

Figure 2: Create an API Key.
Figure 2: Create an API Key.

Note that when you create a new API Key, SendGrid shows you the actual text of the Key in the confirmation screen, but never again. It's not like Azure Key Vault and other apps where you can click a button to reveal or copy the key value. You need to copy it at this initial step and save it somewhere else or else you'll be forced to create a new key.

When you create a new API Key, be sure to copy the actual text of the Key from the confirmation screen, because you'll never see it again.

Verify a “Single Sender”

A “Single Sender” is an email address that SendGrid uses to send your mail. Messages appear to come from this email address. You're required to set up at least one Single Sender and verify it. Under Settings, click the Sender Authentication menu item, click the Verify a Single Sender button as shown in Figure 3, and then follow the process.

Figure 3: Verify a Single Sender.
Figure 3: Verify a Single Sender.

With these prerequisites in place, you're ready to start sending messages.

Sending Your First Message

SendGrid comes with some utility classes that facilitate programming in C#. These classes are distributed in a NuGet package and they provide convenient C# wrappers for underlying SendGrid REST API calls. Before I dive into the C# code, I'd like to spend a little time helping you understand the REST API itself. Understanding the REST API directly makes writing the C# code simpler and more intuitive. To interact with a REST API, you need an HTTP client utility. The steps below use PostMan, but any HTTP client, such as CURL or NativeRest, will work.

Send a Message via PostMan

In PostMan, choose Add Request. Set the request type to POST and the URL to https://api.sendgrid.com/v3/mail/send. Then, switch to the Authorization tab and set Auth Type to Bearer Token. Last, paste your API Key into the Token box, as shown in Figure 4.

Figure 4: Set the Bearer Token.
Figure 4: Set the Bearer Token.

Next, switch to the Body tab, choose raw, and construct a JSON body like the one shown in Figure 5.

Figure 5: The JSON Body for a simple message
Figure 5: The JSON Body for a simple message

Let's break this JSON down a bit to understand the various pieces.

First, there's the from parameter. This must be one of your verified single senders.

Next there's the subject parameter that's the subject of the emails. Notice that the subject includes a substitution string "patient". More on substitution strings in a moment.

Next there's the content parameter. This is the body of your email and for this simple example, I'm using a brief HTML fragment that also includes some substitution strings, provider and apptdate.

Last, there's the personalizations parameter. This topic is the key to using SendGrid and deserves some in-depth discussion.

Personalizations

Looking at the JSON above, you might conclude that a personalization is just a fancy word for a recipient, but there's more to it. For starters, with a personalization, you can set multiple recipients. Notice that the to property is an array, so you can specify multiple people on the “To” line of your message. A personalization also has a cc property and a bcc property, both of which are also arrays. Just like a regular email, you can send a single message to multiple recipients of various kinds by using the To, CC and BCC lines. In this way, it's probably better to think of a personalization as an envelope that contains all the information needed to create and send a message.

Notice that the personalization contains a substitutions property. This property functions as a list of merge fields. SendGrid treats it as a string dictionary and it does exact string substitution. In other words, in the example, any place that the string patient shows up in the message subject or body, SendGrid substitutes the string “Lucinda.” Any place that the exact string apptdate shows up, SendGrid substitutes the string “August 15, 2025”.

You may be wondering about the “*” syntax. Is that required? The answer is no. SendGrid is simply doing direct string substitution. The asterisks don't have any special meaning. The important thing is to make sure you don't get unintended substitutions. Imagine, for example, that instead of patient you used patient without the asterisks and somewhere in your message body you said, “Please make sure the patient shows up fifteen minutes early.” In this case, you'd end up with, “Please make sure that the Lucinda shows up fifteen minutes early.” Using a special symbol like the asterisks minimizes the chance of an unintended substitution. Note that you don't have to use asterisks. I've seen examples where other characters were used such as dashes (e.g., -patient-) or percent symbols (e.g., %patient%), and I've seen examples where people used a special syntax like field_patient. It doesn't matter, as long as your substitution strings are unique enough to avoid unintentional substitution. Asterisks have worked for me.

Using a special symbol reduces the chance of an unintended substitution.

Once you have your JSON body completed, go ahead and click the Send button in PostMan to submit the request. If it works, you should get a 202 Accepted response. This means that the SendGrid server got your request and has queued your messages for sending. Figure 9 shows what the resulting message looks like when the above request shows up in the recipient's inbox.

Figure 6: The message, as received by the recipient
Figure 6: The message, as received by the recipient

Now with C#

Now that you understand the REST API, the next step is to do this with C#. One way to call the SendGrid REST API via C# is to use the HttpClient class as you would with any REST API. Here's a code snippet showing the general technique:

string body = GetJsonBody();

using HttpClient client = new();

HttpRequestMessage msg = new(HttpMethod.Post,
    "https://api.sendgrid.com/v3/mail/send";);

msg.Headers.Add("Authorization", "Bearer " +
ConfigurationManager.AppSettings["APIKey"]);

msg.Content = new StringContent(body, Encoding.UTF8, "application/json");

HttpResponseMessage response = await client.SendAsync(msg);

This sends a message just like you did earlier with PostMan. The tricky part is constructing the JSON body for the request. The general technique for dealing with JSON serialization like this is to define a class that represents the request, instantiates it, and then serializes it. The complex part is defining the data class. These kinds of data classes have subclasses and collections and it can take a bit of work to model it all correctly. Fortunately, the folks at SendGrid have done it for you. SendGrid provides some C# helper classes that make this all very easy.

The first step to using the SendGrid C# helper classes is to add the SendGrid NuGet package to your Visual Studio solution:

Install-Package SendGrid

Then, in any code unit where you interact with SendGrid to send mail, you'll likely need these two Using statements:

using SendGrid;
using SendGrid.Helpers.Mail;

Now you can write some C# code to send a message using the SendGrid classes. Begin by creating a SendGridClient using the API Key.

// create a SendGrid Client
string apikey = ConfigurationManager.AppSettings["APIKey"];

SendGridClient client = new(apikey);

Next, start a SendGridMessage and set up the From, Subject, and HtmlContent properties and also initialize the Personalizations collection.

// start a message
SendGridMessage msg = new()
{
    From = new EmailAddress()
    {
        Email = "csharpartisan@gmail.com"
    },

    Subject = "Appoint Reminder for *patient*",

    HtmlContent = "<p>Your appointment with " + 
    "*provider* is on *apptdate*</p>",

    Personalizations = new()
};

Now you need to populate the Personalizations. A key part of the setting up a personalization is providing the substitutions. In the SendGrid C# namespace, this is conveniently modeled as a string dictionary. In a live application, this would, of course, by dynamically generated, but for this illustration, I have just hard-coded some values:

// make a dictionary of substitutions
Dictionary<string, string> substitutions = new()
{
    { "*patient*", "Lucinda" },
    { "*provider*", "Dr. DiAngelo" },
    { "*apptdate*", "August 15, 2025" }
};

Now add a personalization to the message.

// make the EmailAddress for the "to"
EmailAddress address = new()
{
    Email = "valerie_lopez@notrealmail.com",
    Name = "Valerie Lopez"
};

// set up the personalization
msg.Personalizations.Add(new()
{
    Tos = new List<EmailAddress>() { address },
    Substitutions = substitutions
});

At this point, the message is ready to send:

// send
Response response = await client.SendEmailAsync(msg);

if (!response.IsSuccessStatusCode)
{
    // some error handling code
}

In a moment, I'll talk about adapting this code to make it more realistic so it can handle multiple recipients and dynamic substitutions, but for now, just appreciate how simple this is. The SendGrid helper classes match the REST API nicely and are intuitive and easy to use.

Multiple Recipients

So far, you've learned to send a single message to a single recipient. In the real world, that's not very helpful. The whole point of having a bulk mail solution like SendGrid is to send lots of messages to lots of people. There are several ways you might consider doing this and some of them are not a good idea. Below, I explain a few techniques to avoid and then I show the best practice of using SendGrid personalizations.

All of the code below uses a little data class to encapsulate the details for a single recipient:

public class RecipientData
{
    public string Email { get; set; }

    public string Name { get; set; }

    Dictionary<string, string> Substitutions;
}

How Not to Do It: A Tight Send Loop

Looking at the C# code above, you can probably imagine wrapping it in a function that sends a single message and then calling it multiple times when you need to send a bulk mail. That code might look something like this:


// don't do this
static async Task SendBulkMail(string subject,
    string body, List<RecipientData> recipients)
{
    // loop the recipients and send to each one
    foreach (RecipientData rec in recipients)
    {
        await SendAMessage(subject, body, rec);
    }
}

This is a bad idea. Imagine you had a mailing list of 10,000 recipients. This code is going to result in 10,000 individual REST API calls to SendGrid. This is slow and inefficient and contrary to the SendGrid API intent. Simply put, don't do this.

How Not to Do It: Multiple To's

As you saw above, the SendGrid personalization construct includes several properties where you can list recipients. Like a real mail message, you have a To line, a CC line and a BCC line, each of which can contain multiple email addresses. You can probably imagine leveraging this to send a message to multiple recipients. That code might look something like this:

static async Task SendBulkMail(string subject,
    string body, List<RecipientData> recipients)
{
    // start a message with one personalization
    SendGridMessage msg = new(); // body etc...
    Personalization pers = new();
    pers.Tos = new List<EmailAddress>();
    msg.Personalizations.Add(pers);
    // add each recipient on the To line
    foreach (RecipientData recipient in recipients)
    {
        pers.Tos.Add(new EmailAddress()
        {
            Email = recipient.Email,
            Name = recipient.Name
        });
    }
}

This is probably not what you want to do, either. Perhaps you've received a bulk mail message like this where, instead of sending you a single, individual message, your email address was included along with a bunch of other people on the To line. This is considered an email faux pas because it inadvertently shares each recipient's address with the entire list. In addition, notice that with this technique, the substitutions don't work at all. Each recipient has its own substitution data but because you only have one personalization, the message can't be customized for each recipient.

Best Practice: Use Personalizations

The proper way to send bulk mail to multiple recipients in SendGrid is to use one personalization per recipient. This is what personalizations are for. The technique is to start a SendGridMessage and set up the properties that are common to all recipients such as From, Subject, and Body. Then, use a loop to add one personalization per recipient to the message with that recipient's email address and substitutions. The code to do that might look something like this:

static void AddRecipients(SendGridMessage msg, 
  List<RecipientData> recipients)
{
    foreach (RecipientData recipient in recipients)
    {
        msg.Personalizations.Add(
            new Personalization()
            {
                Tos = new List<EmailAddress>()
                {
                    new EmailAddress()
                    {
                        Email = recipient.Email,
                        Name = recipient.Name
                    }
                },
                Substitutions = recipient.Substitutions;
            }
        );
    }
}

Now when you send the message, each recipient gets their own message. They'll be alone on the To line and the message subject and body contain their personalization data (their substitution strings).

It turns out that SendGrid limits the number of personalizations to 1000 per message. That means that if you have more than 1000 recipients, you'll need to batch them into 1000-recipient chunks. This is still very efficient: You'd be able to send bulk mail to 10,000 recipients using 10 REST API calls rather than 10,000, and each recipient gets their own personalized message. For this kind of chunking problem, I like to use the List.Chunk extension method like this:

IEnumerable<RecipientData[]> chunks = recipients.Chunk(1000);

foreach (RecipientData[] chunk in chunks)
{
    // send to these 1000
}

The List.Chunk method breaks the initial List up into an IEnumerable structure of sub-arrays, each of which contains chunk size items, except the last one, which contains the remainder.

Templates

One of the selling points for many of the bulk mail providers is that they are more than just a message broker: Many also have CRM and marketing capabilities built in along with tools to support designing professional-quality HTML email templates. SendGrid is no exception. If you go to SendGrid's website, you can see example of dozens of free HTML email templates that they provide and allow you to customize. The best way to understand how SendGrid templates work is to make one.

Create a SendGrid Template

To design and manage your SendGrid templates, log onto your SendGrid account. From the Dashboard, expand the Email API group in the sidebar and choose Dynamic Templates. This takes you to the Dynamic Templates section, as shown in Figure 7.

Figure 7: Dynamic Templates
Figure 7: Dynamic Templates

Click Create a Dynamic Template. Give your template a name and click Create, as shown in Figure 8.

Figure 8: Create a Dynamic Template.
Figure 8: Create a Dynamic Template.

Next, you need to create a Version for your template. This step is a little confusing. You might think that the first version of your template would be created by default, but it isn't. Your template is versionless, to begin with. To add a version, click the Add Version button, as shown in Figure 9.

Figure 9: Add version
Figure 9: Add version

This takes you to a screen where you can select a pre-made design to base your template on. Some of these designs are provided by SendGrid and some you can make yourself and save to the Designs section of the product. For this demonstration, choose a Blank Template, as shown in Figure 10.

Figure 10: Select a design.
Figure 10: Select a design.

Last, choose the “editing experience” you wish to use. SendGrid provides both a drag-drop WYSIWYG style template editor and a direct HTML editor. For this demonstration, choose the drag-drop Design Editor, as shown in Figure 11.

Figure 11: Select Your Editing Experience.
Figure 11: Select Your Editing Experience.

Now you can begin editing your template. The first thing is to give your version a Name and set the Subject and optional Preheader of your messages, as shown in Figure 12.

Figure 12: Configure your template.
Figure 12: Configure your template.

Note the interesting double-braces (aka handlebars) syntax in the Subject, {{provider}}. When using SendGrid Dynamic Templates, the substitutions work a little bit differently than you saw above with non-templated messages. With non-templated messages, the engine uses simple string substitution and a string dictionary. With templates, it uses this special double-braces (handlebars) syntax and a string-to-object dictionary.

Next, click the Build tab in the designer and you can drag-drop design elements from the modules on the left into the design space on the right and set their properties. In Figure 13, I've used an Image & Text component. Note the use of the double-braces syntax in the message body.

Figure 13: Designing your template
Figure 13: Designing your template

Now you can preview the template and test the substitution fields. To do so, click the Preview tab and click {} Show Test Data, as shown in Figure 14.

Figure 14: Show Test Data
Figure 14: Show Test Data

This reveals the Test Data screen where you can input some JSON and verify your substitutions, as shown in Figure 15.

Figure 15: Preview with test data
Figure 15: Preview with test data

Once you're done designing your template version, save your changes and return to the Dynamic Templates screen. There, you can see your template. Note that a template can have multiple versions but only one of them is active. This is the version that gets used when you send messages. Also take note of your template's Template ID, as shown in Figure 16. You'll need this ID to send messages using your template.

Figure 16: Template ID and Active version
Figure 16: Template ID and Active version

Send a Message Using a Template via PostMan

Now that you have a template set up, you can use it. Go back to PostMan and modify your JSON body, as shown in Figure 17. Specifically, you can remove the subject and content parameters, as these are now provided by the template. Then, include a template_id parameter with the ID of your template. Last, replace the substitutions property of your personalization with a dynamic_template_data property.

Figure 17: JSON to use a template
Figure 17: JSON to use a template

Once your JSON is edited, you can click Send in PostMan to submit this request. If it succeeds, you'll once again get a 202 Accepted response and your message will be delivered.

Now with C#

Adapting the C# send message code to use templates is straightforward. Here's a revised block of code to start a new SendGridMessage. Note that the Subject and HtmlContent properties are gone, replaced by TemplateID.

SendGridMessage msg = new()
{
    From = new EmailAddress() { Email = "csharpartisan@gmail.com" },
      TemplateId = "d-e36d54f75edb4463b66177d04da8a00b",
      Personalizations = new()
};

Next, update the substitution dictionary to be a string-to-object dictionary:

// make a dictionary of template data
Dictionary<string, object> data = new()
{
    { "patient", "Lucinda" },
    { "provider", "Dr. DiAngelo" },
    { "apptdate", new DateTime(2025, 8, 15) }
};

Last, when you add the personalization to the message, set TemplateData rather than Substitutions.

// make the EmailAddress for the "to"
EmailAddress address = new()
{
    Email = "sallysnail99999@gmail.com",
    Name = "Valerie Lopez"
};

// set up the personalization
msg.Personalizations.Add(new()
{
    Tos = new List<EmailAddress>() { address },
    TemplateData = data
});

That's all there is to it. Now you can send your message and check the result. When you do, you may notice something interesting about how the appointment date is displayed. Note that the date is displayed in ISO 8601 format, as shown in Figure 18.

Figure 18: Default date format
Figure 18: Default date format

Why is this? When you used a string-to-string dictionary in the first examples, your code already converted the date to a string. In the template data dictionary, which is string-to-object, the SendGrid engine gets a data value that is strongly typed as a DateTime and it has to decide how to format it as a string. Fortunately, SendGrid does provide a way to control this using the formatDate function. To specify a string format for a date-time value, first modify your date field to have the three-part syntax, {{ formatDate <yourfield> <yourformat> }}, as shown in Figure 19.

Figure 19: The formatDate function
Figure 19: The formatDate function

Then, add a value to the template data for the format string such as “shortDateFormat” : “D MMM YYYY”. Now the SendGrid engine will format your date as 25 Aug 2025.

This raises an interesting question: What about other values that are not strings? For example, what if you have a Boolean, a double, a decimal, or an object in your template data dictionary? How will those be formatted? For those other types, you don't have any control over what the SendGrid engine will do. For example, if you have a floating-point number whose value is 33 1/3, SendGrid prints it in your message as 33.333333333333336. Similarly, a Boolean is going to print a true or false. If you want these values truncated, rounded, converted to Yes/No or formatted in any other way, you'll need to do it ahead of time in your C# code when you construct the template data dictionary.

Working with Templates Using C#

If you're building a solution using SendGrid, you may find that you have use cases where it would be helpful to be able to interact with SendGrid Templates in code. For example, maybe you'd like to retrieve a list of all the templates in the system or look up the ID of a template based on its name. These kinds of functions are supported through the SendGrid REST API which means, if you want, you can write direct REST API code using the HTTPClient class. But wouldn't it be nice if SendGrid provided some C# helper functions to streamline this just like it does for message sending?

The RequestAsync Method

Unfortunately, the SendGrid C# library provides only limited support for these kinds of functions. There are no explicit methods such as GetTemplate, GetTemplates, or GetTemplateVersion. Instead, SendGrid provides an all-purpose method, RequestAsync, that lets you submit any supported REST API request via the SendGridClient. When you use RequestAsync, you need to handle all the JSON serialization and deserialization which, in turn, means you need to define serialization support classes. This makes it a lot less convenient than the Message Send API. Next, I implement one of the use cases above to illustrate the general technique.

Retrieve a Template ID from a Name

To retrieve a list of Templates, you're going to need to submit a RequestAsync query that looks like this:

Response response = await client.RequestAsync(
{
    method: SendGridClient.Method.GET,
    urlPath: "templates",
    queryParams: requestJson
});

This illustrates the general technique for using RequestAsync: You supply a method, a urlPath and possibly some queryParams. The questions are: What do those queryParams looks like and what does the Response look like?

For this method, where you're requesting templates, the queryParams JSON looks like this:

{
    "generations": "dynamic",
    "page_size": 200
}

To generate the JSON, you can use a serialization class as follows:

class GetTemplatesRequest
{
    public string generations { get; set; }
    public int page_size { get; set; }
}

GetTemplatesRequest request = new()
{
    generations = "dynamic",
    page_size = 200
};

string requestJson = JsonSerializer.Serialize(request);

For a tiny class like that with just two fields, this is trivial. The Response you get back is quite a bit more involved. Figure 20 is a screenshot of what the Response JSON looks like:

Figure 20: Get the Templates resulting JSON
Figure 20: Get the Templates resulting JSON

As you can see, this schema is a lot more involved. Deserializing this requires defining three different serialization classes, one each for result, version, and metadata. This is a good amount of work. The sample project associated with this article that you can download from the CODE Magazine page associated with this article contains a full implementation of all these serialization classes, and others. (Note, an AI-based coding assistant is an excellent tool for generating these kinds of serialization classes if you need to).

One you've gotten the array of Result object in the Response from the call to RequestAsync, you enumerate them and find the template that matches the target name:

foreach (Result template in query.result)
{
    if (template.name == templateName)
    {
        return template.id;
    }
}

Again, the sample project associated with this article that you can download from the CODE Magazine website contains a function called GetTemplateIDByName that shows this process end-to-end. It also contains a function called GetTemplatesAsync that retrieves the active version for each template, including its subject, full HTML, and plain text.

Conclusion

Sending automated bulk email is a common requirement for many applications and using a third-party tool is almost surely required to handle many of the issues and complexities involved in sending bulk email. SendGrid provides a decent option. As you have seen, it has a well-documented REST API and some convenient C# helper classes that facilitate sending messages, and it also has a robust template creation tool that designers can use to develop professional quality email templates. For a programmer, the real key to using SendGrid is understanding how SendGrid personalizations work and how they allow you to customize each message for each recipient and send thousands of messages in bulk with just a handful of API calls.