Open Sourcing Sendgrid Webhooks Library

Page content

It’s been a while since I wrote an open-source contribution. A while back, a mini-project as I was involved required parsing of Sendgrid Webhooks in C#. As it turned out, there wasn’t much around and Sendgrid didn’t have an official library. Although at the time I pretty much stopped writing any C#, it was a good opportunity for an open source project.

Since then, it was included on Sengrid’s library list and so it means we’re famous right ..? Also, people wrote about it on stack overflow so we’re def famous!

Sendgrid Webhooks

Sendgrid is a 3rd party service that provides email delivery on your behalf, providing you with an API. On top of that it can give you a functionality, where every user action can result in a callback to your endpoint. This effectively means that every time an email is processed, send, open, clicked, spam reported, unsubscribed or bounced - it will call your servers with additional details. I blogged about creating reporting from the sendgrid webhooks earlier on.

If you’re serious about your channel this is super important, because you get info such as clicked url, IP address or the user-agent of the client. Plus, you can pass custom arguments along with the API calls as email meta-data that make it to the callbacks as well.

Parse the Webhook Callbacks

I wanted to provide a library that would take the JSON in the callback and turned that into a polymorphic list of Sendgrid Webhook Events. Sendgrid tends to aggregate the callbacks into batches so you usually receive more than 1 event in a single callback.

 1    [
 2      {
 3        "email": "john.doe@sendgrid.com",
 4        "timestamp": 1337197600,
 5        "smtp-id": "<4FB4041F.6080505@sendgrid.com>",
 6        "event": "processed"
 7      },
 8      {
 9        "email": "john.doe@sendgrid.com",
10        "timestamp": 1337966815,
11        "category": "newuser",
12        "event": "click",
13        "url": "https://sendgrid.com"
14      }
15    ]

Using the library

The library allows you to easily deseralise the callbacks and create a list of typed events of a common base. All custom fields, date and list conversions are handled for you.

 1    var parser = new WebhookParser();
 2    var events = parser.ParseEvents(json);
 3
 4    var webhookEvent = events[0];
 5
 6    // Shared base properties
 7    webhookEvent.EventType; // Enum - type of the event as enum
 8    webhookEvent.Categories; // IList<string> - list of categories assigned ot the event
 9    webhookEvent.TimeStamp; // DateTime - datetime of the event converted from Unix time
10    webhookEvent.UniqueParameters; // IDictionary<string, string> - map of key-value unique parameters
11
12    // Event-specific properties
13    var clickEvent = webhookEvent as ClickEvent; // Cast to the parent based on EventType
14    clickEvent.Url; // string - URL on what the user has clicked

Serialisation

Since we’re dealing with JSON - I pulled in a single dependency on the project. It is the Newtonsoft.JSON library. ServiceStack.Text is my preferred option with a lower footprint and marginally higher performance, however, in this instance Newtonsoft.JSON is the one that is used more widely.

The custom serialisation used the type attribute of each event to determine the underlying event type. Each event was based of the same base WebhookEventBase.

 1  namespace Sendgrid.Webhooks.Events
 2  {
 3      public abstract class WebhookEventBase
 4      {
 5          public WebhookEventBase()
 6          {
 7              UniqueParameters = new Dictionary<string, string>();
 8          }
 9
10          [JsonProperty("event"), JsonConverter(typeof(StringEnumConverter))]
11          public WebhookEventType EventType { get; set; }
12
13          [JsonProperty("email")]
14          public string Email { get; set; }
15
16          [JsonProperty("category"), JsonConverter(typeof(WebhookCategoryConverter))]
17          public IList<string> Category { get; set; }
18
19          [JsonProperty("timestamp"), JsonConverter(typeof(EpochToDateTimeConverter))]
20          public DateTime Timestamp { get; set; }
21
22          public IDictionary<string, string> UniqueParameters { get; set; }
23      }
24  }

I have also added couple of custom serialisers that deal with custom date conversion or csv to array. That way you have access to a proper DateTime date and List of categories. You can also delcare the WebhookParser with your own version of the JsonConverter, allowing for virtually any changes possible.

The custom converters are the WebhookCategoryConverter

 1    namespace Sendgrid.Webhooks.Converters
 2    {
 3        public abstract class GenericListCreationJsonConverter<T> : JsonConverter
 4        {
 5
 6            public override bool CanConvert(Type objectType)
 7            {
 8                return true;
 9            }
10
11            public override bool CanRead
12            {
13                get { return true; }
14            }
15
16            public override bool CanWrite
17            {
18                get { return false; }
19            }
20
21            public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
22                JsonSerializer serializer)
23            {
24                if (reader.TokenType == JsonToken.StartArray)
25                {
26                    return serializer.Deserialize<List<T>>(reader);
27                }
28                else
29                {
30                    T t = serializer.Deserialize<T>(reader);
31                    return new List<T>(new[] {t});
32                }
33            }
34
35            public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
36            {
37                throw new NotImplementedException();
38            }
39        }
40    }

and EpochToDateTimeConverter

 1    namespace Sendgrid.Webhooks.Converters
 2    {
 3        public class EpochToDateTimeConverter : JsonConverter
 4        {
 5            private static readonly DateTime EpochDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
 6
 7            public override bool CanConvert(Type objectType)
 8            {
 9                return objectType == typeof(DateTime);
10            }
11
12            public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
13            {
14                if (value == null)
15                    return;
16
17                var date = (DateTime) value;
18                var diff = date - EpochDate;
19
20                var secondsSinceEpoch = (int) diff.TotalSeconds;
21                serializer.Serialize(writer, secondsSinceEpoch);
22            }
23
24            public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
25            {
26                var timestamp = (long) reader.Value;
27
28                return EpochDate.AddSeconds(timestamp);
29            }
30        }
31    }

Code

Check out the code at https://github.com/mirajavora/sendgrid-webhooks or pull down from nuget using

1    Install-Package Sendgrid.Webhooks

Contributions

A surprising number of people started using the code and we’ve had few contributions already, which is great! Thanks to Andy McCready, vanillajonathan, brianp101 and Petteroe.